ある企業では、従業員のシフトスケジュールを表示するガント図の作成が必要でした。大量のカスタム要素と複雑なインタラクションを扱うため、初期版ではDOMと仮想リスト(Virtual List)を組み合わせたアプローチを採用しました。表示とインタラクションの両方で良好な結果を得られましたが、データ量が増えると、仮想リストであってもスクロール時にカクつきが発生するようになりました。そこで、EChartsのcanvasを使用してガント図を描画するという新しいアイデアが浮かびました。主にEChartsのrenderItemを使用してカスタムチャートを実装しようと試みたところ、様々な問題に直面しました。
option設定
yAxis: {
type: 'value'
},
xAxis: {
type: 'time'
},
series: [
{
id: 'flightData',
type: 'custom',
renderItem: this.renderGanttItem,
dimensions: [null, { type: 'time' }, { type: 'time' }, { type: 'ordinal' }],
encode: {
x: [1, 2],
y: 0,
},
data: echarts.util.map(_missions, (item, index) => {
let startMoment = new Date(this.root.options.startTime)
let endMoment = new Date(this.root.options.endTime)
return [index, startMoment, endMoment].concat(item);
}),
},
{
type: 'custom',
renderItem: this.renderAxisLabelItem,
dimensions: [null, { type: 'ordinal' }],
encode: {
x: -1, // このシリーズはx軸に制御されません
y: 0
},
data: echarts.util.map(_staffList, function (item, index) {
return [index].concat(item);
}),
}
],
dataZoom:[
{
id: 'slider_x',
type: 'slider',
xAxisIndex: 0,
filterMode: 'none',
height: 20,
bottom: 0,
start: this.zoom.x_left,
end: this.zoom.x_right,
handleIcon: dragIcon,
handleSize: '80%',
showDetail: false,
backgroundColor:'#E4E7ED9E',
throttle: 100
},
{
id: 'slider_y',
type: 'slider',
filterMode: 'weakFilter',
fillerColor:'#d2d9e4',
yAxisIndex: 0,
zoomLock: true,
width: 20,
right: 0,
start: this.zoom.y_top,
end: this.zoom.y_bottom,
handleSize: 0,
showDetail: false,
backgroundColor:'#E4E7ED9E',
throttle: 100
},
{
type: 'inside',
id: 'insideX',
xAxisIndex: 0,
throttle: 100,
zoomOnMouseWheel: false,
moveOnMouseMove: true
},
{
type: 'inside',
id: 'insideY',
yAxisIndex: 0,
throttle: 100,
zoomOnMouseWheel: false,
moveOnMouseMove: true,
moveOnMouseWheel: true
}
]
ここで問題が発生しました:renderGanttItemが返すgroupの要素数が変動するためです。時間軸の移動に伴って表示される要素が絶えず変化し、その数も当然変化します。EChartsは要素が減少または追加される際に、インターフェースに残像やアニメーションのジャンプ問題が発生します。ドキュメントを厳密に参照し、一意のseries-custom.renderItem.return_rect.idを定義しても問題は解決しませんでした。
解決策1
series-custom.renderItem.return_rect.ignoreを設定し、ノードが完全に無視されるかどうか(レンダリングもイベント処理も行われない)を制御します。ビューポートから要素が外れると、その要素のignoreをtrueに設定します。要素が追加または削除される際には、まずthis.myChart.clear()を呼び出し、renderItem内での要素の増減が発生しないように保証します。
この方法は有効ですが、各行の要素数が多い場合、ignoreを設定してもカクつきが見られ、DOMと比較してcanvasの滑らかさに欠けます。dataZoom-slider.throttleの値を増やしてスロットリングを強化することで、カクつきを軽減するしかありませんでした。
解決策2
ソースコードを調査したところ、次のようなコメントが見つかりました:
// 使用方法:
// (1) デフォルトでは、`elOption.$mergeChildren`は'byIndex'に設定されています。これは
// 既存の子要素は削除されず、renderItemから返される子要素を以下のように
// 構成することで、一部の子要素のプロパティを更新する機能が有効になります:
// `var children = group.children = []; children[3] = {opacity: 0.5};`
// (2) `elOption.$mergeChildren`が'byName'の場合、child.nameによって子要素の
// 追加/更新/削除が行われます。ただし、パフォーマンスは低下する可能性があります。
// (3) `elOption.$mergeChildren`が`false`の場合、既存の子要素は完全に置き換えられます。
// (4) `!elOption.children`の場合、「マージ」の原則に従い、何も行われません。
//
// 実装の簡略化のため、単一の子要素を直接削除する方法は提供されていません
// (そうしないと、子要素配列の全インデックスを修正する必要があります)。
// ユーザーは単一の子要素をその`ignore`を`true`に設定するか、
// 別の要素に置き換えることで削除できます。必要に応じて、その`$merge`を
// `true`に設定できます。
renderItemが返すgroup要素に$mergeChildren='byName'を設定し、各種類の要素にnameを設定します。これにより、毎回nameに基づいて要素が更新されます。
renderGanttItem = (params, api) => {<br></br> ...<br></br>
return {
type: 'group',
name: 'gantt-group',
id: categoryIndex,
info: {data:item},
children: allChildren,
$mergeChildren: 'byName'
};
}
この設定により、大量の要素がある場合でもスクロールが非常にスムーズになり、問題が解決しました。