🌟

WebFOCUS拡張グラフ開発でよく使うパターン

2025/03/07に公開

WebFOCUS拡張グラフの一般的なパターン

このドキュメントでは、WebFOCUS拡張グラフ開発でよく使われるコードパターンと実装例を紹介します。

GitHubにあるcom.ibi.の拡張グラフを一通り読んで作成したものです。

1. データ処理パターン

1.1 データの正規化

// シリーズブレークがない場合、データを2次元配列に正規化
if (renderConfig.dataBuckets.depth === 1) {
  data = [data];
}

// 1つの測定値しかない場合、タイトルを配列に正規化
if (renderConfig.dataBuckets.buckets.value && !Array.isArray(renderConfig.dataBuckets.buckets.value.title)) {
  renderConfig.dataBuckets.buckets.value.title = [renderConfig.dataBuckets.buckets.value.title];
}

1.2 バケットプロパティの安全な取得

// バケットが存在するか確認してから値を取得
function getBucketTitle(bucketName, renderConfig, defaultValue) {
  if (renderConfig.dataBuckets && 
      renderConfig.dataBuckets.buckets && 
      renderConfig.dataBuckets.buckets[bucketName]) {
    return renderConfig.dataBuckets.buckets[bucketName].title || defaultValue;
  }
  return defaultValue;
}

// 使用例
var valueTitle = getBucketTitle('value', renderConfig, 'Value');

1.3 データ配列の転置

// 2次元データ配列を転置する
function transposeData(data) {
  return data[0].map(function (_, colIndex) {
    return data.map(function(row) {
      return row[colIndex];
    });
  });
}

// 使用例
var transposedData = transposeData(renderConfig.data);

2. レンダリングパターン

2.1 SVG要素の基本設定

function setupSVG(container, width, height, margin) {
  // SVG要素のクリア
  container.selectAll('*').remove();
  
  // SVG要素の作成
  var svg = container.append('svg')
    .attr('width', width)
    .attr('height', height);
    
  // グラフエリアの作成
  var chartArea = svg.append('g')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
    
  return {
    svg: svg,
    chartArea: chartArea,
    width: width - margin.left - margin.right,
    height: height - margin.top - margin.bottom
  };
}

// 使用例
var margin = {top: 20, right: 30, bottom: 40, left: 50};
var chart = setupSVG(d3.select(renderConfig.container), renderConfig.width, renderConfig.height, margin);

2.2 軸の設定と描画

function createAxes(svg, x, y, chartWidth, chartHeight, margin) {
  // X軸の作成
  var xAxis = d3.svg.axis()
    .scale(x)
    .orient('bottom');
    
  // Y軸の作成
  var yAxis = d3.svg.axis()
    .scale(y)
    .orient('left');
    
  // X軸の描画
  svg.append('g')
    .attr('class', 'x-axis')
    .attr('transform', 'translate(0,' + chartHeight + ')')
    .call(xAxis);
    
  // Y軸の描画
  svg.append('g')
    .attr('class', 'y-axis')
    .call(yAxis);
    
  return {
    xAxis: xAxis,
    yAxis: yAxis
  };
}

// 使用例
var x = d3.scale.ordinal().domain(labels).rangeRoundBands([0, chart.width], 0.1);
var y = d3.scale.linear().domain([0, maxValue]).range([chart.height, 0]);
var axes = createAxes(chart.chartArea, x, y, chart.width, chart.height, margin);

2.3 レスポンシブフォントサイズ

function calculateResponsiveFontSize(width, height, baseSize, minSize, maxSize) {
  var size = Math.min(width, height) / baseSize;
  return Math.max(minSize, Math.min(maxSize, size)) + 'px';
}

// 使用例
var labelFontSize = calculateResponsiveFontSize(renderConfig.width, renderConfig.height, 40, 10, 16);
svg.selectAll('.axis-label')
  .style('font-size', labelFontSize);

3. ツールチップとデータ選択のパターン

3.1 ツールチップの追加

function addTooltips(elements, renderConfig, seriesIndex) {
  elements.each(function(d, i) {
    renderConfig.modules.tooltip.addDefaultToolTipContent(this, seriesIndex, i, d);
  });
  
  // 最後にツールチップを更新
  renderConfig.modules.tooltip.updateToolTips();
}

// 使用例
var bars = svg.selectAll('.bar')
  .data(data)
  .enter().append('rect')
  .attr('class', function(d, i) {
    return chart.buildClassName('riser', 0, i, 'bar');
  });
  
addTooltips(bars, renderConfig, 0);

3.2 カスタムツールチップ内容

var config = {
  // ...
  modules: {
    tooltip: {
      supported: true,
      autoContent: function(target, s, g, d, data) {
        var content = '<div class="tooltip-title">' + d.labels + '</div>';
        content += '<div class="tooltip-body">';
        content += '<div class="tooltip-row">';
        content += '<span class="tooltip-label">値:</span>';
        content += '<span class="tooltip-value">' + chart.formatNumber(d.value, "#,###.##") + '</span>';
        content += '</div>';
        content += '</div>';
        return content;
      }
    }
  }
};

3.3 データ選択の有効化

function setupDataSelection(renderConfig, elements) {
  // データ選択のサポートに必要なクラス名を追加
  elements.attr('class', function(d, s, g) {
    return chart.buildClassName('riser', s, g, 'bar');
  });
  
  // レンダリング完了後にデータ選択を有効化
  renderConfig.renderComplete();
  renderConfig.modules.dataSelection.activateSelection();
}

// 使用例
var bars = svg.selectAll('rect')
  .data(data)
  .enter().append('rect')
  // ... その他の属性設定
  
setupDataSelection(renderConfig, bars);

4. エラー処理とデバッグ

4.1 try-catchでの安全なレンダリング

function renderCallback(renderConfig) {
  try {
    // チャートのレンダリング処理
    var chart = renderConfig.moonbeamInstance;
    var data = renderConfig.data;
    
    // ... レンダリングコード
    
    // 正常に完了
    renderConfig.renderComplete();
    
  } catch (e) {
    // エラーをログ出力
    console.error("レンダリングエラー:", e);
    
    // エラーメッセージを表示
    var container = d3.select(renderConfig.container);
    container.selectAll('*').remove();
    container.append('text')
      .attr('x', renderConfig.width / 2)
      .attr('y', renderConfig.height / 2)
      .attr('text-anchor', 'middle')
      .text('エラーが発生しました: ' + e.message);
    
    // エラーが発生した場合も完了を通知
    renderConfig.renderComplete();
  }
}

4.2 デバッグ情報の表示

function showDebugInfo(container, renderConfig) {
  if (!renderConfig.properties.debug) return;
  
  var debugPanel = container.append('div')
    .attr('class', 'debug-panel')
    .style('position', 'absolute')
    .style('top', '0')
    .style('right', '0')
    .style('background', 'rgba(255,255,255,0.9)')
    .style('border', '1px solid #ccc')
    .style('padding', '5px')
    .style('font-size', '10px')
    .style('max-width', '300px');
  
  debugPanel.append('div')
    .text('データ構造:')
    .style('font-weight', 'bold');
  
  debugPanel.append('pre')
    .text(JSON.stringify(renderConfig.data, null, 2))
    .style('max-height', '200px')
    .style('overflow', 'auto');
  
  debugPanel.append('div')
    .text('プロパティ:')
    .style('font-weight', 'bold');
  
  debugPanel.append('pre')
    .text(JSON.stringify(renderConfig.properties, null, 2));
}

// 使用例
function renderCallback(renderConfig) {
  var container = d3.select(renderConfig.container);
  // ... レンダリングコード
  
  showDebugInfo(container, renderConfig);
  renderConfig.renderComplete();
}

5. アニメーションパターン

5.1 バーチャートのアニメーション

function animateBars(bars, y, height, duration) {
  bars.attr('y', height)
    .attr('height', 0)
    .transition()
    .duration(duration)
    .delay(function(d, i) { return i * 50; })
    .attr('y', function(d) { return y(d.value); })
    .attr('height', function(d) { return height - y(d.value); });
}

// 使用例
var bars = svg.selectAll('.bar')
  .data(data)
  .enter().append('rect')
  .attr('x', function(d) { return x(d.label); })
  .attr('width', x.rangeBand());

animateBars(bars, y, chartHeight, 800);

5.2 円グラフのアニメーション

function animatePie(arcs, arc, duration) {
  arcs.each(function(d) {
    d.startAngle = 0;
    d.endAngle = 0;
  })
  .attr('d', arc)
  .transition()
  .duration(duration)
  .attrTween('d', function(d) {
    var interpolate = d3.interpolate(
      {startAngle: 0, endAngle: 0},
      {startAngle: d.startAngle, endAngle: d.endAngle}
    );
    return function(t) {
      return arc(interpolate(t));
    };
  });
}

// 使用例
var arc = d3.svg.arc()
  .innerRadius(0)
  .outerRadius(radius);
  
var arcs = svg.selectAll('.arc')
  .data(pie(data))
  .enter().append('path')
  .attr('class', 'arc')
  .attr('fill', function(d, i) { return colors(i); });
  
animatePie(arcs, arc, 1000);

6. レスポンシブデザインパターン

6.1 可変マージン計算

function calculateMargin(width, height) {
  return {
    top: Math.max(10, height * 0.05),
    right: Math.max(15, width * 0.05),
    bottom: Math.max(30, height * 0.1),
    left: Math.max(40, width * 0.1)
  };
}

// 使用例
var margin = calculateMargin(renderConfig.width, renderConfig.height);
var chartWidth = renderConfig.width - margin.left - margin.right;
var chartHeight = renderConfig.height - margin.top - margin.bottom;

6.2 コンテナサイズに応じたレイアウト調整

function adjustLayout(width, height) {
  var layout = {};
  
  // 小さいサイズの場合のレイアウト
  if (width < 400 || height < 300) {
    layout.showLabels = false;
    layout.fontSize = '8px';
    layout.legendPosition = 'bottom';
  } 
  // 中くらいのサイズの場合のレイアウト
  else if (width < 600 || height < 400) {
    layout.showLabels = true;
    layout.fontSize = '10px';
    layout.legendPosition = 'right';
  } 
  // 大きいサイズの場合のレイアウト
  else {
    layout.showLabels = true;
    layout.fontSize = '12px';
    layout.legendPosition = 'right';
  }
  
  return layout;
}

// 使用例
var layout = adjustLayout(renderConfig.width, renderConfig.height);
if (layout.showLabels) {
  // ラベルの描画処理
}

7. 配色パターン

7.1 ダイナミックカラースケール

function createColorScale(data, colorRange) {
  var min = d3.min(data, function(d) { return d.value; });
  var max = d3.max(data, function(d) { return d.value; });
  
  return d3.scale.linear()
    .domain([min, min + (max - min) / 2, max])
    .range(colorRange || ['#2c7bb6', '#ffffbf', '#d7191c']);
}

// 使用例
var colorScale = createColorScale(data, ['#9ecae1', '#4292c6', '#084594']);
bars.attr('fill', function(d) { return colorScale(d.value); });

7.2 カテゴリ別の配色

function createCategoryColorScale(categories) {
  return d3.scale.ordinal()
    .domain(categories)
    .range(d3.scale.category10().range());
}

// 使用例
var categories = data.map(function(d) { return d.category; });
var colorScale = createCategoryColorScale(categories);

svg.selectAll('.bar')
  .data(data)
  .attr('fill', function(d) { return colorScale(d.category); });

8. パフォーマンスパターン

8.1 大量のデータポイントの効率的な描画

function renderLargeDataSet(container, data, width, height) {
  // データをビニングして描画数を減らす
  var binSize = Math.ceil(data.length / 1000);
  var binnedData = [];
  
  for (var i = 0; i < data.length; i += binSize) {
    var binValues = data.slice(i, i + binSize);
    var sum = binValues.reduce(function(acc, val) { return acc + val.value; }, 0);
    binnedData.push({
      x: i,
      value: sum / binValues.length // 平均値
    });
  }
  
  // 縮小したデータセットを描画
  container.selectAll('.point')
    .data(binnedData)
    .enter().append('circle')
    .attr('class', 'point')
    .attr('cx', function(d) { return xScale(d.x); })
    .attr('cy', function(d) { return yScale(d.value); })
    .attr('r', 2);
}

// 使用例
if (data.length > 1000) {
  renderLargeDataSet(svg, data, width, height);
} else {
  // 通常のレンダリング
}

8.2 キャンバスを使用した高速描画

function renderWithCanvas(container, data, width, height) {
  // HTMLキャンバスを作成
  var canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  container.node().appendChild(canvas);
  
  var ctx = canvas.getContext('2d');
  
  // データポイントを描画
  ctx.fillStyle = 'steelblue';
  data.forEach(function(d) {
    var x = xScale(d.x);
    var y = yScale(d.y);
    
    ctx.beginPath();
    ctx.arc(x, y, 2, 0, 2 * Math.PI);
    ctx.fill();
  });
}

// 使用例
if (data.length > 5000) {
  renderWithCanvas(d3.select(renderConfig.container), data, width, height);
} else {
  // SVGを使用した通常のレンダリング
}

9. データ変換パターン

9.1 階層データの平坦化

function flattenHierarchy(data) {
  var result = [];
  
  function traverse(node, parentPath) {
    var currentPath = parentPath ? parentPath + '.' + node.name : node.name;
    
    result.push({
      name: node.name,
      path: currentPath,
      value: node.value || 0,
      level: parentPath ? parentPath.split('.').length : 0
    });
    
    if (node.children && node.children.length) {
      node.children.forEach(function(child) {
        traverse(child, currentPath);
      });
    }
  }
  
  data.forEach(function(root) {
    traverse(root, '');
  });
  
  return result;
}

// 使用例
var hierarchyData = [
  {
    name: "A",
    children: [
      {name: "A1", value: 5},
      {name: "A2", value: 3}
    ]
  }
];

var flatData = flattenHierarchy(hierarchyData);

9.2 時系列データの集計

function aggregateTimeData(data, interval) {
  // データをタイムスタンプでソート
  data.sort(function(a, b) {
    return a.timestamp - b.timestamp;
  });
  
  var result = [];
  var currentBucket = null;
  var currentSum = 0;
  var currentCount = 0;
  
  data.forEach(function(d) {
    // タイムスタンプを指定された間隔で丸める
    var bucketTime = Math.floor(d.timestamp / interval) * interval;
    
    if (currentBucket === null) {
      currentBucket = bucketTime;
    }
    
    // 新しいバケットの場合、前のバケットを結果に追加
    if (bucketTime !== currentBucket) {
      result.push({
        timestamp: currentBucket,
        value: currentSum / currentCount
      });
      
      currentBucket = bucketTime;
      currentSum = d.value;
      currentCount = 1;
    } else {
      currentSum += d.value;
      currentCount++;
    }
  });
  
  // 最後のバケットを追加
  if (currentCount > 0) {
    result.push({
      timestamp: currentBucket,
      value: currentSum / currentCount
    });
  }
  
  return result;
}

// 使用例
var hourlyData = aggregateTimeData(rawData, 3600000); // 1時間ごとに集計

10. その他の便利なユーティリティパターン

10.1 データの欠損値の処理

function handleMissingValues(data, defaultValue) {
  return data.map(function(d) {
    if (d.value === null || d.value === undefined || isNaN(d.value)) {
      d.value = defaultValue;
      d.isMissing = true;
    }
    return d;
  });
}

// 使用例
var cleanData = handleMissingValues(data, 0);
bars.attr('class', function(d) {
  return d.isMissing ? 'bar missing-data' : 'bar';
});

10.2 カスタムフォーマッタ

function createCustomFormatter(format, options) {
  return function(value) {
    if (value === null || value === undefined || isNaN(value)) {
      return options.nullRepresentation || '';
    }
    
    var formattedValue = chart.formatNumber(value, format);
    
    if (options.prefix) {
      formattedValue = options.prefix + formattedValue;
    }
    
    if (options.suffix) {
      formattedValue += options.suffix;
    }
    
    return formattedValue;
  };
}

// 使用例
var currencyFormatter = createCustomFormatter('#,###.00', {
  prefix: '¥',
  nullRepresentation: 'N/A'
});

dataLabels.text(function(d) {
  return currencyFormatter(d.value);
});

これらのパターンと例を参考にして、WebFOCUS拡張グラフの効率的な開発を行ってください。パターンを適切に組み合わせることで、保守性が高く、再利用可能なコードを作成できます。

Discussion