🌟

WebFOCUSの拡張グラフ開発ガイドを自分で書いた

2025/03/07に公開

https://github.com/shimokado/webfocus-extensions

WebFOCUS拡張グラフ開発ガイド

1. 拡張グラフ開発の基本

1.1 プロジェクト構成

拡張グラフの標準的なディレクトリ構造は以下のとおりです:

com.mycompany.mychart/
├── com.mycompany.mychart.js   (メインJSファイル)
├── properties.json            (プロパティ設定)
├── css/
│   └── style.css              (スタイルシート)
└── lib/
    └── chart_utils.js         (補助スクリプト)

1.2 拡張グラフ開発の流れ

  1. npm run create-extensionコマンドを実行して既存の拡張グラフを複製する
  2. 複製したフォルダのproperties.jsonを編集する
  3. 拡張グラフのメインJSファイルを編集する
  4. test.htmlを使ってローカルでテストする
  5. npm run deployコマンドを使ってWebFOCUSにデプロイする

2. 主要なコンポーネント

2.1 プロパティファイル (properties.json)

properties.jsonファイルは、拡張グラフのメタデータと設定を定義します。

{
  "info": {
    "version": "1.0",
    "implements_api_version": "1.0",
    "author": "My Company",
    "copyright": "My Company Inc.",
    "url": "https://www.mycompany.com",
    "icons": {
      "medium": "icons/medium.png"
    }
  },
  "properties": {
    "barColor": "#1f77b4",
    "barSpacing": 2
  },
  "propertyAnnotations": {
    "barColor": "str",
    "barSpacing": "number"
  },
  "dataBuckets": {
    "tooltip": true,
    "buckets": [
      {
        "id": "value",
        "type": "measure",
        "count": {"min": 1, "max": 1}
      },
      {
        "id": "labels",
        "type": "dimension",
        "count": {"min": 1, "max": 1}
      }
    ]
  },
  "translations": {
    "en": {
      "name": "My Chart",
      "description": "My custom chart description",
      "icon_tooltip": "My Chart",
      "value_name": "Value Bucket",
      "value_tooltip": "Drop a measure here",
      "labels_name": "Label Bucket",
      "labels_tooltip": "Drop a dimension here"
    },
    "ja": {
      "name": "マイチャート",
      "description": "カスタムチャートの説明",
      "icon_tooltip": "マイチャート",
      "value_name": "値バケット",
      "value_tooltip": "ここに測定値をドロップ",
      "labels_name": "ラベルバケット",
      "labels_tooltip": "ここにディメンションをドロップ"
    }
  }
}

2.2 メインJSファイル

メインJSファイルには、拡張グラフのロジックが含まれます。

(function() {
  // 初期化コールバック
  function initCallback(successCallback, initConfig) {
    successCallback(true);
  }

  // データがない場合の前処理コールバック
  function noDataPreRenderCallback(preRenderConfig) {
    var chart = preRenderConfig.moonbeamInstance;
    chart.legend.visible = false;
  }

  // データがない場合のレンダリングコールバック
  function noDataRenderCallback(renderConfig) {
    var container = d3.select(renderConfig.container);
    container.append("text")
      .attr("x", renderConfig.width / 2)
      .attr("y", renderConfig.height / 2)
      .attr("text-anchor", "middle")
      .text("データがありません");
    
    renderConfig.renderComplete();
  }

  // レンダリング前コールバック
  function preRenderCallback(preRenderConfig) {
    var chart = preRenderConfig.moonbeamInstance;
    chart.title.visible = true;
    chart.title.text = "マイカスタムチャート";
  }

  // メインレンダリングコールバック
  function renderCallback(renderConfig) {
    var chart = renderConfig.moonbeamInstance;
    var data = renderConfig.data;
    var props = renderConfig.properties;
    
    var container = d3.select(renderConfig.container)
      .attr("class", "com_mycompany_chart");
    
    // データの正規化
    if (renderConfig.dataBuckets.depth === 1) {
      data = [data];
    }
    
    // スケールの設定
    var width = renderConfig.width;
    var height = renderConfig.height;
    var margin = {top: 20, right: 20, bottom: 40, left: 50};
    
    var x = d3.scale.ordinal()
      .domain(data.map(function(d) { return d.labels; }))
      .rangeRoundBands([margin.left, width - margin.right], 0.1);
      
    var yMax = d3.max(data, function(d) { return d.value[0]; });
    var y = d3.scale.linear()
      .domain([0, yMax * 1.1])
      .range([height - margin.bottom, margin.top]);
    
    // 軸の描画
    var xAxis = d3.svg.axis().scale(x).orient("bottom");
    var yAxis = d3.svg.axis().scale(y).orient("left");
    
    container.append("g")
      .attr("class", "x-axis")
      .attr("transform", "translate(0," + (height - margin.bottom) + ")")
      .call(xAxis);
      
    container.append("g")
      .attr("class", "y-axis")
      .attr("transform", "translate(" + margin.left + ",0)")
      .call(yAxis);
    
    // バーの描画
    var bars = container.selectAll(".bar")
      .data(data)
      .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.labels); })
      .attr("width", x.rangeBand() - props.barSpacing)
      .attr("y", function(d) { return y(d.value[0]); })
      .attr("height", function(d) { return height - margin.bottom - y(d.value[0]); })
      .attr("fill", props.barColor)
      .attr("class", function(d, i) {
        return chart.buildClassName("riser", 0, i, "bar");
      })
      .each(function(d, i) {
        renderConfig.modules.tooltip.addDefaultToolTipContent(this, 0, i, d);
      });
    
    // データラベルの追加
    container.selectAll(".bar-label")
      .data(data)
      .enter().append("text")
      .attr("class", "bar-label")
      .attr("x", function(d) { return x(d.labels) + x.rangeBand() / 2; })
      .attr("y", function(d) { return y(d.value[0]) - 5; })
      .attr("text-anchor", "middle")
      .text(function(d) { return chart.formatNumber(d.value[0], "#,###.##"); });
    
    // レンダリング完了を通知
    renderConfig.renderComplete();
    renderConfig.modules.tooltip.updateToolTips();
  }
  
  // 拡張グラフの設定
  var config = {
    id: 'com.mycompany.mychart',
    containerType: 'svg',
    initCallback: initCallback,
    preRenderCallback: preRenderCallback,
    renderCallback: renderCallback,
    noDataPreRenderCallback: noDataPreRenderCallback,
    noDataRenderCallback: noDataRenderCallback,
    resources: {
      script: ['lib/chart_utils.js'],
      css: ['css/style.css']
    },
    modules: {
      tooltip: {
        supported: true
      },
      dataSelection: {
        supported: true,
        needSVGEventPanel: false
      }
    }
  };
  
  // 拡張グラフの登録
  tdgchart.extensionManager.register(config);
})();

3. データバケットの操作

3.1 バケットの定義

拡張グラフのデータバケットは、properties.jsonで定義されます:

"dataBuckets": {
  "tooltip": true,
  "buckets": [
    {
      "id": "value",
      "type": "measure",
      "count": {"min": 1, "max": 1}
    },
    {
      "id": "labels",
      "type": "dimension",
      "count": {"min": 1, "max": 1}
    }
  ]
}

3.2 バケットデータへのアクセス

function renderCallback(renderConfig) {
  var dataBuckets = renderConfig.dataBuckets;
  
  // バケットの存在を確認
  if (dataBuckets && dataBuckets.buckets) {
    // valueバケットの情報を取得
    if (dataBuckets.buckets.value) {
      var valueTitle = dataBuckets.buckets.value.title;
      var valueFieldName = dataBuckets.buckets.value.fieldName;
      var valueNumberFormat = dataBuckets.buckets.value.numberFormat;
    }
    
    // labelsバケットの情報を取得
    if (dataBuckets.buckets.labels) {
      var labelsTitle = dataBuckets.buckets.labels.title;
      var labelsFieldName = dataBuckets.buckets.labels.fieldName;
    }
  }
  
  // データの処理
  var data = renderConfig.data;
  // ...
}

3.3 配列形式への正規化

バケットのプロパティは、データ構造に応じて文字列または配列で返されることがあります。一貫した処理を行うために、常に配列形式に正規化することをお勧めします:

// バケットのプロパティを常に配列として扱う
var labelsTitles = buckets.labels ? (Array.isArray(buckets.labels.title) ? buckets.labels.title : [buckets.labels.title]) : [];
var labelsFieldNames = buckets.labels ? (Array.isArray(buckets.labels.fieldName) ? buckets.labels.fieldName : [buckets.labels.fieldName]) : [];
var valueTitles = buckets.value ? (Array.isArray(buckets.value.title) ? buckets.value.title : [buckets.value.title]) : [];
var valueFieldNames = buckets.value ? (Array.isArray(buckets.value.fieldName) ? buckets.value.fieldName : [buckets.value.fieldName]) : [];
var valueNumberFormats = buckets.value ? (Array.isArray(buckets.value.numberFormat) ? buckets.value.numberFormat : [buckets.value.numberFormat]) : [];

4. 拡張グラフのモジュール

4.1 ツールチップモジュール

ツールチップを実装するには:

var config = {
  // ...
  modules: {
    tooltip: {
      supported: true,
      autoContent: function(target, s, g, d) {
        return d.labels + ': ' + d.value;
      }
    }
  }
};

レンダリング時に各要素にツールチップを追加:

svg.selectAll("rect")
  .data(data)
  .enter().append("rect")
  .attr("class", function(d, i) {
    return chart.buildClassName("riser", 0, i, "bar");
  })
  .each(function(d, i) {
    renderConfig.modules.tooltip.addDefaultToolTipContent(this, 0, i, d);
  });

// 最後にツールチップを更新
renderConfig.modules.tooltip.updateToolTips();

4.2 データ選択モジュール

データ選択を実装するには:

var config = {
  // ...
  modules: {
    dataSelection: {
      supported: true,
      needSVGEventPanel: false
    }
  }
};

レンダリング後に有効化:

// グラフが完全に描画された後に呼び出す
renderConfig.modules.dataSelection.activateSelection();

5. よくある問題と解決方法

5.1 データが表示されない

問題: データがchartに表示されない

解決方法:

  1. console.log(renderConfig.data)でデータが正しく渡されているか確認
  2. データバケットが正しく定義されているか確認
  3. renderConfig.dataBuckets.depthをチェックし、必要に応じてデータを正規化

5.2 外部リソースが読み込まれない

問題: 外部JSやCSSが読み込まれない

解決方法:

  1. resourcesプロパティで正しいパスが指定されているか確認
  2. 相対パスが正しいか確認(通常は拡張フォルダからの相対パス)

5.3 拡張グラフが認識されない

問題: WebFOCUSで拡張グラフが表示されない

解決方法:

  1. html5chart_extensions.jsonに拡張グラフが正しく登録されているか確認
  2. WebFOCUS管理コンソールでキャッシュをクリアする

6. デバッグテクニック

6.1 コンソールログ

各コールバック関数の開始時にログを出力することで、処理の流れを確認できます:

function renderCallback(renderConfig) {
  console.log('renderCallback:', renderConfig);
  
  // データをログ出力
  console.log('Data:', renderConfig.data);
  console.log('Buckets:', renderConfig.dataBuckets);
  
  // 処理を続行...
}

6.2 テスト用HTML

ローカル環境でテストするためのHTMLファイルを作成:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>拡張グラフテスト</title>
  <script src="tdgchart-min-for-test.js"></script>
  <script src="https://d3js.org/d3.v3.min.js"></script>
  <script src="com.mycompany.mychart.js"></script>
  <style>
    #chart {
      width: 800px;
      height: 500px;
      border: 1px solid #ccc;
      margin: 20px;
    }
  </style>
</head>
<body>
  <div id="chart"></div>
  <script>
    // テストデータ
    var testData = [
      {labels: "A", value: [100]},
      {labels: "B", value: [150]},
      {labels: "C", value: [200]},
      {labels: "D", value: [125]},
      {labels: "E", value: [175]}
    ];
    
    // テスト用レンダリング設定
    var renderConfig = {
      moonbeamInstance: new tdgchart(),
      data: testData,
      properties: {
        barColor: "#1f77b4",
        barSpacing: 2
      },
      width: 800,
      height: 500,
      container: document.getElementById('chart'),
      dataBuckets: {
        buckets: {
          value: {
            title: "値",
            fieldName: "VALUE"
          },
          labels: {
            title: "ラベル",
            fieldName: "LABELS"
          }
        }
      },
      modules: {
        tooltip: {
          supported: true,
          addDefaultToolTipContent: function() {},
          updateToolTips: function() {}
        },
        dataSelection: {
          supported: true,
          activateSelection: function() {}
        }
      },
      renderComplete: function() {
        console.log("レンダリング完了");
      }
    };
    
    // 拡張グラフのレンダリング呼び出し
    // コールバック関数の名前を正しく指定する必要があります
    renderCallback(renderConfig);
  </script>
</body>
</html>

7. WebFOCUSでの利用

7.1 拡張グラフのインストール

  1. 拡張グラフのフォルダを以下に配置します:

    C:\ibi\WebFOCUS93\config\web_resource\extensions\com.mycompany.mychart
    
  2. html5chart_extensions.jsonファイルを編集:

    {
      "com.mycompany.mychart": {"enabled": true},
      // 他の拡張グラフ設定
    }
    
  3. 管理コンソールでキャッシュをクリアします

7.2 FOCEXECでの使用例

GRAPH FILE WF_RETAIL_LITE
  SUM COGS_US 
  BY PRODUCT_CATEGORY
  ON GRAPH PCHOLD FORMAT JSCHART
  ON GRAPH SET LOOKGRAPH EXTENSION
  ON GRAPH SET AUTOFIT ON
  ON GRAPH SET STYLE *
  INCLUDE=IBFS:/FILE/IBI_HTML_DIR/javaassist/intl/EN/combine_templates/ENWarm.sty,$
  TYPE=DATA, COLUMN=PRODUCT_CATEGORY, BUCKET= >labels, $
  TYPE=DATA, COLUMN=COGS_US, BUCKET= >value, $
  *GRAPH_JS
  chartType: "com.mycompany.mychart",
  barColor: "#3366CC",
  barSpacing: 5
  *END
  ENDSTYLE
  END

8. ベストプラクティス

8.1 コード構成

  • 匿名関数で全体を囲み、グローバル名前空間を汚染しない
  • 各コールバック関数を明確に分離する
  • 共通の処理は別関数として抽出する

8.2 エラーハンドリング

function renderCallback(renderConfig) {
  try {
    // チャートのレンダリングコード
    
    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();
  }
}

8.3 レスポンシブデザイン

グラフのサイズに応じて自動的に調整される設計を心がけます:

function renderCallback(renderConfig) {
  var width = renderConfig.width;
  var height = renderConfig.height;
  
  // サイズに基づいてマージンを調整
  var margin = {
    top: Math.max(10, height * 0.05),
    right: Math.max(10, width * 0.05),
    bottom: Math.max(30, height * 0.1),
    left: Math.max(40, width * 0.1)
  };
  
  // フォントサイズも調整
  var fontSize = Math.max(8, Math.min(width, height) / 40);
  
  // 以降、これらの値を使用してグラフを描画
}

9. 高度な実装例

9.1 アニメーション

D3.jsのトランジションを使用したアニメーション:

// バーのアニメーション
bars.attr("y", height - margin.bottom)
    .attr("height", 0)
    .transition()
    .duration(800)
    .delay(function(d, i) { return i * 50; })
    .attr("y", function(d) { return y(d.value[0]); })
    .attr("height", function(d) { return height - margin.bottom - y(d.value[0]); });

9.2 インタラクティブ機能

クリックイベントの実装:

bars.on("click", function(d, i) {
  console.log("クリック:", d, i);
  
  // 選択状態を視覚的に表示
  d3.select(this).classed("selected", !d3.select(this).classed("selected"));
  
  // カスタムイベントの発火
  if (typeof renderConfig.properties.onClick === "function") {
    renderConfig.properties.onClick(d, i);
  }
});

9.3 カラースケール

データに基づいて色を動的に生成:

var colorScale = d3.scale.linear()
  .domain([0, d3.max(data, function(d) { return d.value[0]; })])
  .range(["#9ecae1", "#08519c"]);

bars.attr("fill", function(d) { return colorScale(d.value[0]); });

この開発ガイドを参考に、WebFOCUSの拡張グラフ開発を効率的に行ってください。

Discussion