Skip to content

データ可視化

Japan Map Selectorを使用して、様々なデータを地図上に可視化する方法を紹介します。

基本的な可視化

人口データの可視化

javascript
// 都道府県別人口データ(2020年国勢調査)
const populationData = {
  '13': 14047594, // 東京都
  '27': 8837685,  // 大阪府
  '14': 9237337,  // 神奈川県
  '23': 7542415,  // 愛知県
  '11': 7344765,  // 埼玉県
  // ... 他の都道府県
};

// 人口に基づいて色を決定
function getColorByPopulation(prefectureCode) {
  const population = populationData[prefectureCode] || 0;
  
  if (population > 10000000) return '#800026';
  if (population > 7000000) return '#BD0026';
  if (population > 5000000) return '#E31A1C';
  if (population > 3000000) return '#FC4E2A';
  if (population > 2000000) return '#FD8D3C';
  if (population > 1000000) return '#FEB24C';
  if (population > 500000) return '#FED976';
  return '#FFEDA0';
}

// カスタムテーマを作成
const populationTheme = {
  name: 'population',
  prefectureFill: '#FFEDA0', // デフォルト色
  prefectureStroke: '#666',
  prefectureHoverFill: '#666',
  prefectureSelectedFill: '#333',
  municipalityFill: '#ccc',
  municipalityStroke: '#999',
  municipalityHoverFill: '#aaa',
  municipalitySelectedFill: '#888',
  backgroundColor: '#f7f7f7',
  strokeWidth: 1
};

コロプレスマップの実装

javascript
class ChoroplethMap {
  constructor(containerId) {
    this.map = new JapanMapSelector({
      width: 800,
      height: 600,
      theme: populationTheme
    });
    
    this.data = {};
    this.colorScale = null;
  }
  
  async initialize() {
    await this.map.initialize(
      '/data/prefectures.json',
      '/data/municipalities.json'
    );
    
    this.setupColorScale();
    this.render();
  }
  
  setData(data) {
    this.data = data;
    this.setupColorScale();
    this.render();
  }
  
  setupColorScale() {
    const values = Object.values(this.data);
    const min = Math.min(...values);
    const max = Math.max(...values);
    
    // D3.jsのようなスケール関数
    this.colorScale = (value) => {
      const normalized = (value - min) / (max - min);
      return this.interpolateColor(normalized);
    };
  }
  
  interpolateColor(t) {
    // 青から赤へのグラデーション
    const r = Math.round(255 * t);
    const b = Math.round(255 * (1 - t));
    return `rgb(${r}, 0, ${b})`;
  }
  
  render() {
    // 各都道府県に色を適用
    const prefectures = this.map.getPrefectures();
    prefectures.forEach(prefecture => {
      const value = this.data[prefecture.code];
      if (value !== undefined) {
        const color = this.colorScale(value);
        this.applyColor(prefecture.code, color);
      }
    });
  }
}

時系列データの可視化

アニメーション付き変化

javascript
class TimeSeriesMap {
  constructor() {
    this.map = new JapanMapSelector({
      width: 800,
      height: 600
    });
    
    this.timeSeriesData = {};
    this.currentYear = 2020;
    this.isPlaying = false;
  }
  
  async loadTimeSeriesData() {
    // 年ごとのデータを読み込み
    for (let year = 2010; year <= 2020; year++) {
      const response = await fetch(`/data/population-${year}.json`);
      this.timeSeriesData[year] = await response.json();
    }
  }
  
  play() {
    this.isPlaying = true;
    this.animate();
  }
  
  pause() {
    this.isPlaying = false;
  }
  
  animate() {
    if (!this.isPlaying) return;
    
    // 現在の年のデータを表示
    this.updateVisualization(this.currentYear);
    
    // 年を進める
    this.currentYear++;
    if (this.currentYear > 2020) {
      this.currentYear = 2010;
    }
    
    // 次のフレーム
    setTimeout(() => this.animate(), 1000);
  }
  
  updateVisualization(year) {
    const data = this.timeSeriesData[year];
    
    // タイトルを更新
    document.getElementById('year-display').textContent = `${year}年`;
    
    // 地図を更新
    this.map.getPrefectures().forEach(prefecture => {
      const value = data[prefecture.code];
      const color = this.getColorByValue(value);
      this.updatePrefectureColor(prefecture.code, color);
    });
  }
}

// 使用例
const timeSeriesMap = new TimeSeriesMap();
await timeSeriesMap.loadTimeSeriesData();

// コントロール
document.getElementById('play-btn').addEventListener('click', () => {
  timeSeriesMap.play();
});

document.getElementById('pause-btn').addEventListener('click', () => {
  timeSeriesMap.pause();
});

複数指標の可視化

バブルマップ

javascript
class BubbleMap {
  constructor() {
    this.map = new JapanMapSelector({
      width: 800,
      height: 600
    });
  }
  
  renderBubbles(data) {
    const svg = document.querySelector('.japan-map-selector svg');
    
    // バブル用のグループを作成
    const bubbleGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    bubbleGroup.setAttribute('class', 'bubbles');
    
    data.forEach(item => {
      const prefecture = this.map.getPrefectures().find(p => p.code === item.prefectureCode);
      if (!prefecture) return;
      
      // 都道府県の中心点を計算
      const center = this.calculateCenter(prefecture);
      
      // バブルを作成
      const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
      circle.setAttribute('cx', center.x);
      circle.setAttribute('cy', center.y);
      circle.setAttribute('r', this.scaleRadius(item.value));
      circle.setAttribute('fill', this.getColorByCategory(item.category));
      circle.setAttribute('fill-opacity', '0.7');
      circle.setAttribute('stroke', '#fff');
      circle.setAttribute('stroke-width', '1');
      
      // ツールチップ
      circle.addEventListener('mouseenter', (e) => {
        this.showTooltip(e, item);
      });
      
      bubbleGroup.appendChild(circle);
    });
    
    svg.appendChild(bubbleGroup);
  }
  
  scaleRadius(value) {
    // 値を半径にスケーリング
    const maxRadius = 30;
    const minRadius = 5;
    const maxValue = 1000000;
    
    const scaled = Math.sqrt(value / maxValue) * maxRadius;
    return Math.max(minRadius, Math.min(maxRadius, scaled));
  }
}

ヒートマップ

密度表現

javascript
class HeatmapVisualization {
  constructor() {
    this.map = new JapanMapSelector({
      width: 800,
      height: 600
    });
    
    this.heatmapData = [];
  }
  
  generateHeatmap(points) {
    const canvas = document.createElement('canvas');
    canvas.width = 800;
    canvas.height = 600;
    const ctx = canvas.getContext('2d');
    
    // グラデーションを作成
    const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 20);
    gradient.addColorStop(0, 'rgba(255, 0, 0, 1)');
    gradient.addColorStop(0.5, 'rgba(255, 255, 0, 0.5)');
    gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
    
    // 各ポイントを描画
    points.forEach(point => {
      ctx.save();
      ctx.translate(point.x, point.y);
      ctx.fillStyle = gradient;
      ctx.fillRect(-20, -20, 40, 40);
      ctx.restore();
    });
    
    // ヒートマップレイヤーとして追加
    return canvas;
  }
  
  overlay(canvas) {
    const container = document.querySelector('.map-container');
    canvas.style.position = 'absolute';
    canvas.style.top = '0';
    canvas.style.left = '0';
    canvas.style.pointerEvents = 'none';
    canvas.style.opacity = '0.6';
    container.appendChild(canvas);
  }
}

インタラクティブな凡例

動的な凡例コンポーネント

javascript
class InteractiveLegend {
  constructor(map, data) {
    this.map = map;
    this.data = data;
    this.container = document.getElementById('legend');
  }
  
  render() {
    const values = Object.values(this.data);
    const min = Math.min(...values);
    const max = Math.max(...values);
    
    const legendHTML = `
      <div class="legend-title">人口(万人)</div>
      <div class="legend-scale">
        <div class="legend-gradient"></div>
        <div class="legend-labels">
          <span>${(min / 10000).toFixed(0)}</span>
          <span>${(max / 10000).toFixed(0)}</span>
        </div>
      </div>
      <div class="legend-items">
        ${this.createLegendItems()}
      </div>
    `;
    
    this.container.innerHTML = legendHTML;
    this.addInteractivity();
  }
  
  createLegendItems() {
    const ranges = [
      { min: 0, max: 500000, color: '#FFEDA0', label: '〜50万' },
      { min: 500000, max: 1000000, color: '#FED976', label: '50万〜100万' },
      { min: 1000000, max: 2000000, color: '#FEB24C', label: '100万〜200万' },
      { min: 2000000, max: 5000000, color: '#FD8D3C', label: '200万〜500万' },
      { min: 5000000, max: Infinity, color: '#E31A1C', label: '500万〜' }
    ];
    
    return ranges.map(range => `
      <div class="legend-item" data-min="${range.min}" data-max="${range.max}">
        <span class="legend-color" style="background-color: ${range.color}"></span>
        <span class="legend-label">${range.label}</span>
      </div>
    `).join('');
  }
  
  addInteractivity() {
    // 凡例項目をクリックすると該当する都道府県をハイライト
    document.querySelectorAll('.legend-item').forEach(item => {
      item.addEventListener('click', () => {
        const min = parseInt(item.dataset.min);
        const max = parseInt(item.dataset.max);
        this.highlightPrefecturesInRange(min, max);
      });
    });
  }
  
  highlightPrefecturesInRange(min, max) {
    const prefectures = this.map.getPrefectures();
    
    prefectures.forEach(prefecture => {
      const value = this.data[prefecture.code] || 0;
      const isInRange = value >= min && value < max;
      
      // 該当する都道府県を強調表示
      const element = document.querySelector(`[data-prefecture="${prefecture.code}"]`);
      if (element) {
        element.style.opacity = isInRange ? '1' : '0.3';
        element.style.strokeWidth = isInRange ? '2' : '1';
      }
    });
  }
}

データのエクスポート

可視化結果の保存

javascript
class VisualizationExporter {
  constructor(map) {
    this.map = map;
  }
  
  exportAsPNG() {
    const svg = document.querySelector('.japan-map-selector svg');
    const svgData = new XMLSerializer().serializeToString(svg);
    
    const canvas = document.createElement('canvas');
    canvas.width = 800;
    canvas.height = 600;
    const ctx = canvas.getContext('2d');
    
    const img = new Image();
    img.onload = () => {
      ctx.drawImage(img, 0, 0);
      canvas.toBlob((blob) => {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `japan-map-${Date.now()}.png`;
        a.click();
      });
    };
    
    img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
  }
  
  exportAsCSV() {
    const data = this.collectVisualizationData();
    const csv = this.convertToCSV(data);
    
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `japan-map-data-${Date.now()}.csv`;
    a.click();
  }
  
  collectVisualizationData() {
    const prefectures = this.map.getPrefectures();
    return prefectures.map(prefecture => ({
      code: prefecture.code,
      name: prefecture.name,
      value: this.getCurrentValue(prefecture.code)
    }));
  }
  
  convertToCSV(data) {
    const header = 'コード,都道府県名,値\n';
    const rows = data.map(row => 
      `${row.code},${row.name},${row.value}`
    ).join('\n');
    
    return header + rows;
  }
}

実装のヒント

パフォーマンス最適化

javascript
// 大量のデータポイントを扱う場合
class OptimizedVisualization {
  constructor() {
    this.renderQueue = [];
    this.isRendering = false;
  }
  
  queueUpdate(prefectureCode, data) {
    this.renderQueue.push({ prefectureCode, data });
    
    if (!this.isRendering) {
      this.processQueue();
    }
  }
  
  async processQueue() {
    this.isRendering = true;
    
    while (this.renderQueue.length > 0) {
      const batch = this.renderQueue.splice(0, 10); // 10件ずつ処理
      
      await new Promise(resolve => {
        requestAnimationFrame(() => {
          batch.forEach(item => {
            this.updatePrefecture(item.prefectureCode, item.data);
          });
          resolve();
        });
      });
    }
    
    this.isRendering = false;
  }
}

MIT License