@orbcharts/core 4.0.0-alpha.0 → 4.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +200 -200
- package/dist/orbcharts-core.es.js +876 -865
- package/dist/orbcharts-core.umd.js +3 -3
- package/dist/src/types/Plugin.d.ts +1 -1
- package/package.json +1 -1
- package/src/OrbCharts.ts +34 -34
- package/src/chart/createChart.ts +1013 -996
- package/src/chart/createGraphData.ts +391 -391
- package/src/chart/createGridData.ts +247 -247
- package/src/chart/createMultivariateData.ts +181 -181
- package/src/chart/createSeriesData.ts +297 -297
- package/src/chart/createTreeData.ts +344 -344
- package/src/chart/defaults.ts +119 -119
- package/src/defineCanvasLayer.ts +23 -23
- package/src/defineCanvasPlugin.ts +38 -38
- package/src/defineSVGLayer.ts +23 -23
- package/src/defineSVGPlugin.ts +38 -38
- package/src/index.ts +8 -8
- package/src/layer/createLayer.ts +137 -137
- package/src/plugin/createPlugin.ts +487 -469
- package/src/test/createGraphData.test.ts +103 -103
- package/src/test/createTreeData.test.ts +97 -97
- package/src/test/simple-graph-test.js +51 -51
- package/src/test/simple-tree-test.js +58 -58
- package/src/types/Chart.ts +62 -62
- package/src/types/ChartContext.ts +41 -41
- package/src/types/Common.ts +4 -4
- package/src/types/Encoding.ts +42 -42
- package/src/types/Event.ts +25 -25
- package/src/types/Layers.ts +92 -92
- package/src/types/ModelData.ts +94 -94
- package/src/types/Plugin.ts +101 -98
- package/src/types/RawData.ts +67 -67
- package/src/types/RenderData.ts +15 -15
- package/src/types/Theme.ts +20 -20
- package/src/types/Validator.ts +35 -35
- package/src/types/index.ts +12 -12
- package/src/utils/aggregateUtils.ts +99 -99
- package/src/utils/colorUtils.ts +63 -63
- package/src/utils/commonUtils.ts +56 -56
- package/src/utils/dom-lifecycle.ts +164 -164
- package/src/utils/dom.ts +54 -54
- package/src/utils/errorMessage.ts +40 -40
- package/src/utils/index.ts +7 -7
- package/src/utils/observables.ts +16 -16
- package/src/utils/orbchartsUtils.ts +8 -8
- package/src/utils/validator.ts +127 -127
|
@@ -1,344 +1,344 @@
|
|
|
1
|
-
import type { RawData, RawDataColumn, Encoding, ModelDataTree, ModelDatumTree, Theme } from '../types'
|
|
2
|
-
import { aggregate } from '../utils/aggregateUtils'
|
|
3
|
-
import { getColorByFrom } from '../utils/colorUtils'
|
|
4
|
-
|
|
5
|
-
export const createTreeData = (rawData: RawData, encoding: Encoding, theme: Theme): ModelDataTree[] => {
|
|
6
|
-
// 依據 dataset 欄位將資料分組
|
|
7
|
-
const datasetMap = new Map<string, RawDataColumn[]>()
|
|
8
|
-
|
|
9
|
-
// 判斷是一維陣列還是二維陣列
|
|
10
|
-
const is2DArray = Array.isArray(rawData[0])
|
|
11
|
-
|
|
12
|
-
if (is2DArray) {
|
|
13
|
-
// 二維陣列:每個子陣列代表一個 dataset
|
|
14
|
-
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
15
|
-
datasetArray.forEach((d) => {
|
|
16
|
-
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
17
|
-
if (!datasetMap.has(datasetKey)) {
|
|
18
|
-
datasetMap.set(datasetKey, [])
|
|
19
|
-
}
|
|
20
|
-
datasetMap.get(datasetKey)!.push(d)
|
|
21
|
-
})
|
|
22
|
-
})
|
|
23
|
-
} else {
|
|
24
|
-
// 一維陣列:依據 dataset 欄位分組
|
|
25
|
-
(rawData as RawDataColumn[]).forEach((d) => {
|
|
26
|
-
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
27
|
-
if (!datasetMap.has(datasetKey)) {
|
|
28
|
-
datasetMap.set(datasetKey, [])
|
|
29
|
-
}
|
|
30
|
-
datasetMap.get(datasetKey)!.push(d)
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 建立排序後的 dataset 名稱陣列
|
|
35
|
-
let sortedDatasetNames: string[] = Array.from(datasetMap.keys())
|
|
36
|
-
if (Array.isArray(encoding.dataset.sort)) {
|
|
37
|
-
sortedDatasetNames = encoding.dataset.sort.filter(name => datasetMap.has(name))
|
|
38
|
-
.concat(sortedDatasetNames.filter(name => !encoding.dataset.sort.includes(name)))
|
|
39
|
-
} else if (encoding.dataset.sort === 'original') {
|
|
40
|
-
const datasetOrder: string[] = []
|
|
41
|
-
if (is2DArray) {
|
|
42
|
-
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
43
|
-
datasetArray.forEach((d) => {
|
|
44
|
-
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
45
|
-
if (!datasetOrder.includes(datasetKey)) {
|
|
46
|
-
datasetOrder.push(datasetKey)
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
} else {
|
|
51
|
-
(rawData as RawDataColumn[]).forEach((d) => {
|
|
52
|
-
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
53
|
-
if (!datasetOrder.includes(datasetKey)) {
|
|
54
|
-
datasetOrder.push(datasetKey)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
sortedDatasetNames = datasetOrder
|
|
59
|
-
} else if (encoding.dataset.sort === 'alphabetical') {
|
|
60
|
-
sortedDatasetNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 對每個 dataset 進行 tree 資料的處理
|
|
64
|
-
const result: ModelDataTree[] = []
|
|
65
|
-
sortedDatasetNames.forEach((datasetName, datasetIndex) => {
|
|
66
|
-
const data = datasetMap.get(datasetName)!
|
|
67
|
-
|
|
68
|
-
// 建立 dataset 內排序後的 series 名稱陣列(僅作為屬性與色彩來源,不做拆樹)
|
|
69
|
-
const seriesMap = new Map<string, RawDataColumn[]>()
|
|
70
|
-
data.forEach((d) => {
|
|
71
|
-
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
72
|
-
if (!seriesMap.has(seriesKey)) {
|
|
73
|
-
seriesMap.set(seriesKey, [])
|
|
74
|
-
}
|
|
75
|
-
seriesMap.get(seriesKey)!.push(d)
|
|
76
|
-
})
|
|
77
|
-
let sortedSeriesNames: string[] = Array.from(seriesMap.keys())
|
|
78
|
-
if (Array.isArray(encoding.series.sort)) {
|
|
79
|
-
sortedSeriesNames = encoding.series.sort.filter(name => seriesMap.has(name))
|
|
80
|
-
.concat(sortedSeriesNames.filter(name => !encoding.series.sort.includes(name)))
|
|
81
|
-
} else if (encoding.series.sort === 'original') {
|
|
82
|
-
const seriesOrder: string[] = []
|
|
83
|
-
data.forEach((d) => {
|
|
84
|
-
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
85
|
-
if (!seriesOrder.includes(seriesKey)) {
|
|
86
|
-
seriesOrder.push(seriesKey)
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
sortedSeriesNames = seriesOrder
|
|
90
|
-
} else if (encoding.series.sort === 'alphabetical') {
|
|
91
|
-
sortedSeriesNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 建立 dataset 內排序後的 category 名稱陣列(僅作為屬性與色彩來源,不做拆樹)
|
|
95
|
-
const categoryMap = new Map<string, RawDataColumn[]>()
|
|
96
|
-
data.forEach((d) => {
|
|
97
|
-
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
98
|
-
if (!categoryMap.has(categoryKey)) {
|
|
99
|
-
categoryMap.set(categoryKey, [])
|
|
100
|
-
}
|
|
101
|
-
categoryMap.get(categoryKey)!.push(d)
|
|
102
|
-
})
|
|
103
|
-
let sortedCategoryNames: string[] = Array.from(categoryMap.keys())
|
|
104
|
-
if (Array.isArray(encoding.category.sort)) {
|
|
105
|
-
sortedCategoryNames = encoding.category.sort.filter(name => categoryMap.has(name))
|
|
106
|
-
.concat(sortedCategoryNames.filter(name => !encoding.category.sort.includes(name)))
|
|
107
|
-
} else if (encoding.category.sort === 'original') {
|
|
108
|
-
const categoryOrder: string[] = []
|
|
109
|
-
data.forEach((d) => {
|
|
110
|
-
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
111
|
-
if (!categoryOrder.includes(categoryKey)) {
|
|
112
|
-
categoryOrder.push(categoryKey)
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
sortedCategoryNames = categoryOrder
|
|
116
|
-
} else if (encoding.category.sort === 'alphabetical') {
|
|
117
|
-
sortedCategoryNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 收集所有節點並依據 id 進行聚合(同一 dataset 內)
|
|
121
|
-
const nodeMap = new Map<string, RawDataColumn[]>()
|
|
122
|
-
data.forEach((d) => {
|
|
123
|
-
const nodeId = d.id || 'unknown'
|
|
124
|
-
if (!nodeMap.has(nodeId)) {
|
|
125
|
-
nodeMap.set(nodeId, [])
|
|
126
|
-
}
|
|
127
|
-
nodeMap.get(nodeId)!.push(d)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
let globalNodeIndex = 0
|
|
131
|
-
const defaultSeriesName = sortedSeriesNames[0] || 'default'
|
|
132
|
-
const defaultSeriesIndex = sortedSeriesNames.indexOf(defaultSeriesName)
|
|
133
|
-
|
|
134
|
-
// 為每個 id 建立 ModelDatumTree,並進行聚合
|
|
135
|
-
const treeNodeMap = new Map<string, ModelDatumTree>()
|
|
136
|
-
|
|
137
|
-
Array.from(nodeMap.entries()).forEach(([nodeId, nodeItems]) => {
|
|
138
|
-
const firstItem = nodeItems[0]
|
|
139
|
-
const seriesName = (firstItem as any)[encoding.series.from] || 'default'
|
|
140
|
-
const seriesIndex = sortedSeriesNames.indexOf(seriesName)
|
|
141
|
-
const categoryName = (firstItem as any)[encoding.category.from] || 'default'
|
|
142
|
-
const categoryIndex = sortedCategoryNames.indexOf(categoryName)
|
|
143
|
-
|
|
144
|
-
if (encoding.value.aggregate === 'none') {
|
|
145
|
-
// 不聚合,使用第一筆資料
|
|
146
|
-
const value = (firstItem as any)[encoding.value.from]
|
|
147
|
-
const treeNode: ModelDatumTree = {
|
|
148
|
-
id: nodeId,
|
|
149
|
-
index: globalNodeIndex++,
|
|
150
|
-
modelType: 'tree',
|
|
151
|
-
name: firstItem.name || nodeId,
|
|
152
|
-
data: firstItem.data,
|
|
153
|
-
value: typeof value === 'number' ? value : null,
|
|
154
|
-
color: getColorByFrom(encoding.color.from, {
|
|
155
|
-
index: globalNodeIndex - 1,
|
|
156
|
-
seriesIndex,
|
|
157
|
-
categoryIndex,
|
|
158
|
-
datasetIndex
|
|
159
|
-
}, theme),
|
|
160
|
-
parent: firstItem.parent || null,
|
|
161
|
-
parentIndex: null, // 稍後設定
|
|
162
|
-
depth: 0, // 稍後計算
|
|
163
|
-
seq: 0, // 稍後計算
|
|
164
|
-
children: [],
|
|
165
|
-
series: seriesName,
|
|
166
|
-
seriesIndex,
|
|
167
|
-
category: categoryName,
|
|
168
|
-
categoryIndex,
|
|
169
|
-
}
|
|
170
|
-
treeNodeMap.set(nodeId, treeNode)
|
|
171
|
-
} else {
|
|
172
|
-
// 進行聚合,將相同 dataset, id 的資料合併
|
|
173
|
-
const values: (number | null)[] = nodeItems.map(d => {
|
|
174
|
-
if (encoding.value.aggregate === 'count') {
|
|
175
|
-
return 1 // count 聚合時每筆資料計為 1
|
|
176
|
-
}
|
|
177
|
-
const value = (d as any)[encoding.value.from]
|
|
178
|
-
return typeof value === 'number' ? value : null
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const aggregatedValue = aggregate(values, encoding.value.aggregate)
|
|
182
|
-
|
|
183
|
-
const treeNode: ModelDatumTree = {
|
|
184
|
-
id: nodeId,
|
|
185
|
-
index: globalNodeIndex++,
|
|
186
|
-
modelType: 'tree',
|
|
187
|
-
name: firstItem.name || nodeId,
|
|
188
|
-
data: firstItem.data,
|
|
189
|
-
value: aggregatedValue,
|
|
190
|
-
color: getColorByFrom(encoding.color.from, {
|
|
191
|
-
index: globalNodeIndex - 1,
|
|
192
|
-
seriesIndex,
|
|
193
|
-
categoryIndex,
|
|
194
|
-
datasetIndex
|
|
195
|
-
}, theme),
|
|
196
|
-
parent: firstItem.parent || null,
|
|
197
|
-
parentIndex: null, // 稍後設定
|
|
198
|
-
depth: 0, // 稍後計算
|
|
199
|
-
seq: 0, // 稍後計算
|
|
200
|
-
children: [],
|
|
201
|
-
series: seriesName,
|
|
202
|
-
seriesIndex,
|
|
203
|
-
category: categoryName,
|
|
204
|
-
categoryIndex,
|
|
205
|
-
}
|
|
206
|
-
treeNodeMap.set(nodeId, treeNode)
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
// 建立樹狀結構
|
|
211
|
-
const allNodes = Array.from(treeNodeMap.values())
|
|
212
|
-
const nodeIndexMap = new Map<string, number>()
|
|
213
|
-
allNodes.forEach(node => {
|
|
214
|
-
nodeIndexMap.set(node.id, node.index)
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
// 設定 parentIndex
|
|
218
|
-
allNodes.forEach(node => {
|
|
219
|
-
if (node.parent && nodeIndexMap.has(node.parent)) {
|
|
220
|
-
node.parentIndex = nodeIndexMap.get(node.parent)!
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
// 建立父子關係
|
|
225
|
-
const rootNodes: ModelDatumTree[] = []
|
|
226
|
-
const childrenMap = new Map<string, ModelDatumTree[]>()
|
|
227
|
-
|
|
228
|
-
allNodes.forEach(node => {
|
|
229
|
-
if (node.parent === null) {
|
|
230
|
-
rootNodes.push(node)
|
|
231
|
-
} else {
|
|
232
|
-
if (!childrenMap.has(node.parent)) {
|
|
233
|
-
childrenMap.set(node.parent, [])
|
|
234
|
-
}
|
|
235
|
-
childrenMap.get(node.parent)!.push(node)
|
|
236
|
-
}
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
// 遞迴建立完整樹狀結構並計算 depth 和 seq
|
|
240
|
-
function buildTree(node: ModelDatumTree, depth: number): ModelDatumTree {
|
|
241
|
-
node.depth = depth
|
|
242
|
-
const children = childrenMap.get(node.id) || []
|
|
243
|
-
|
|
244
|
-
// 根據 value.sort 對子節點進行排序
|
|
245
|
-
if (encoding.value.sort === 'asc') {
|
|
246
|
-
children.sort((a, b) => {
|
|
247
|
-
if (a.value === null && b.value === null) return 0
|
|
248
|
-
if (a.value === null) return 1
|
|
249
|
-
if (b.value === null) return -1
|
|
250
|
-
return a.value - b.value
|
|
251
|
-
})
|
|
252
|
-
} else if (encoding.value.sort === 'desc') {
|
|
253
|
-
children.sort((a, b) => {
|
|
254
|
-
if (a.value === null && b.value === null) return 0
|
|
255
|
-
if (a.value === null) return 1
|
|
256
|
-
if (b.value === null) return -1
|
|
257
|
-
return b.value - a.value
|
|
258
|
-
})
|
|
259
|
-
}
|
|
260
|
-
// 'original' 不需要額外排序,保持原始順序
|
|
261
|
-
|
|
262
|
-
// 設定 seq
|
|
263
|
-
children.forEach((child, index) => {
|
|
264
|
-
child.seq = index
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
// 遞迴處理子節點
|
|
268
|
-
node.children = children.map(child => buildTree(child, depth + 1))
|
|
269
|
-
|
|
270
|
-
return node
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 建立完整的樹
|
|
274
|
-
const trees = rootNodes.map(root => buildTree(root, 0))
|
|
275
|
-
|
|
276
|
-
// 如果有多個根節點,建立一個虛擬根節點
|
|
277
|
-
if (trees.length === 0) {
|
|
278
|
-
// 沒有資料,建立空樹
|
|
279
|
-
result.push({
|
|
280
|
-
id: `empty-${datasetName}`,
|
|
281
|
-
index: globalNodeIndex++,
|
|
282
|
-
modelType: 'tree',
|
|
283
|
-
name: 'Empty Tree',
|
|
284
|
-
data: {},
|
|
285
|
-
value: null,
|
|
286
|
-
color: getColorByFrom(encoding.color.from, {
|
|
287
|
-
index: globalNodeIndex - 1,
|
|
288
|
-
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
289
|
-
categoryIndex: 0,
|
|
290
|
-
datasetIndex
|
|
291
|
-
}, theme),
|
|
292
|
-
parent: null,
|
|
293
|
-
parentIndex: null,
|
|
294
|
-
depth: 0,
|
|
295
|
-
seq: 0,
|
|
296
|
-
children: [],
|
|
297
|
-
series: defaultSeriesName,
|
|
298
|
-
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
299
|
-
category: 'default',
|
|
300
|
-
categoryIndex: 0,
|
|
301
|
-
})
|
|
302
|
-
} else if (trees.length === 1) {
|
|
303
|
-
// 單一根節點
|
|
304
|
-
result.push(trees[0])
|
|
305
|
-
} else {
|
|
306
|
-
const virtualRoot: ModelDatumTree = {
|
|
307
|
-
id: `virtual-root-${datasetName}`,
|
|
308
|
-
index: globalNodeIndex++,
|
|
309
|
-
modelType: 'tree',
|
|
310
|
-
name: `Virtual Root (${datasetName})`,
|
|
311
|
-
data: {},
|
|
312
|
-
value: null,
|
|
313
|
-
color: getColorByFrom(encoding.color.from, {
|
|
314
|
-
index: globalNodeIndex - 1,
|
|
315
|
-
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
316
|
-
categoryIndex: 0,
|
|
317
|
-
datasetIndex
|
|
318
|
-
}, theme),
|
|
319
|
-
parent: null,
|
|
320
|
-
parentIndex: null,
|
|
321
|
-
depth: 0,
|
|
322
|
-
seq: 0,
|
|
323
|
-
children: trees.map((tree, index) => {
|
|
324
|
-
const updateDepth = (node: ModelDatumTree, newDepth: number): ModelDatumTree => {
|
|
325
|
-
node.depth = newDepth
|
|
326
|
-
if (newDepth === 1) {
|
|
327
|
-
node.seq = index
|
|
328
|
-
}
|
|
329
|
-
node.children = node.children.map(child => updateDepth(child, newDepth + 1))
|
|
330
|
-
return node
|
|
331
|
-
}
|
|
332
|
-
return updateDepth(tree, 1)
|
|
333
|
-
}),
|
|
334
|
-
series: defaultSeriesName,
|
|
335
|
-
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
336
|
-
category: 'default',
|
|
337
|
-
categoryIndex: 0,
|
|
338
|
-
}
|
|
339
|
-
result.push(virtualRoot)
|
|
340
|
-
}
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
return result
|
|
344
|
-
}
|
|
1
|
+
import type { RawData, RawDataColumn, Encoding, ModelDataTree, ModelDatumTree, Theme } from '../types'
|
|
2
|
+
import { aggregate } from '../utils/aggregateUtils'
|
|
3
|
+
import { getColorByFrom } from '../utils/colorUtils'
|
|
4
|
+
|
|
5
|
+
export const createTreeData = (rawData: RawData, encoding: Encoding, theme: Theme): ModelDataTree[] => {
|
|
6
|
+
// 依據 dataset 欄位將資料分組
|
|
7
|
+
const datasetMap = new Map<string, RawDataColumn[]>()
|
|
8
|
+
|
|
9
|
+
// 判斷是一維陣列還是二維陣列
|
|
10
|
+
const is2DArray = Array.isArray(rawData[0])
|
|
11
|
+
|
|
12
|
+
if (is2DArray) {
|
|
13
|
+
// 二維陣列:每個子陣列代表一個 dataset
|
|
14
|
+
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
15
|
+
datasetArray.forEach((d) => {
|
|
16
|
+
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
17
|
+
if (!datasetMap.has(datasetKey)) {
|
|
18
|
+
datasetMap.set(datasetKey, [])
|
|
19
|
+
}
|
|
20
|
+
datasetMap.get(datasetKey)!.push(d)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
} else {
|
|
24
|
+
// 一維陣列:依據 dataset 欄位分組
|
|
25
|
+
(rawData as RawDataColumn[]).forEach((d) => {
|
|
26
|
+
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
27
|
+
if (!datasetMap.has(datasetKey)) {
|
|
28
|
+
datasetMap.set(datasetKey, [])
|
|
29
|
+
}
|
|
30
|
+
datasetMap.get(datasetKey)!.push(d)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 建立排序後的 dataset 名稱陣列
|
|
35
|
+
let sortedDatasetNames: string[] = Array.from(datasetMap.keys())
|
|
36
|
+
if (Array.isArray(encoding.dataset.sort)) {
|
|
37
|
+
sortedDatasetNames = encoding.dataset.sort.filter(name => datasetMap.has(name))
|
|
38
|
+
.concat(sortedDatasetNames.filter(name => !encoding.dataset.sort.includes(name)))
|
|
39
|
+
} else if (encoding.dataset.sort === 'original') {
|
|
40
|
+
const datasetOrder: string[] = []
|
|
41
|
+
if (is2DArray) {
|
|
42
|
+
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
43
|
+
datasetArray.forEach((d) => {
|
|
44
|
+
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
45
|
+
if (!datasetOrder.includes(datasetKey)) {
|
|
46
|
+
datasetOrder.push(datasetKey)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
} else {
|
|
51
|
+
(rawData as RawDataColumn[]).forEach((d) => {
|
|
52
|
+
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
53
|
+
if (!datasetOrder.includes(datasetKey)) {
|
|
54
|
+
datasetOrder.push(datasetKey)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
sortedDatasetNames = datasetOrder
|
|
59
|
+
} else if (encoding.dataset.sort === 'alphabetical') {
|
|
60
|
+
sortedDatasetNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 對每個 dataset 進行 tree 資料的處理
|
|
64
|
+
const result: ModelDataTree[] = []
|
|
65
|
+
sortedDatasetNames.forEach((datasetName, datasetIndex) => {
|
|
66
|
+
const data = datasetMap.get(datasetName)!
|
|
67
|
+
|
|
68
|
+
// 建立 dataset 內排序後的 series 名稱陣列(僅作為屬性與色彩來源,不做拆樹)
|
|
69
|
+
const seriesMap = new Map<string, RawDataColumn[]>()
|
|
70
|
+
data.forEach((d) => {
|
|
71
|
+
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
72
|
+
if (!seriesMap.has(seriesKey)) {
|
|
73
|
+
seriesMap.set(seriesKey, [])
|
|
74
|
+
}
|
|
75
|
+
seriesMap.get(seriesKey)!.push(d)
|
|
76
|
+
})
|
|
77
|
+
let sortedSeriesNames: string[] = Array.from(seriesMap.keys())
|
|
78
|
+
if (Array.isArray(encoding.series.sort)) {
|
|
79
|
+
sortedSeriesNames = encoding.series.sort.filter(name => seriesMap.has(name))
|
|
80
|
+
.concat(sortedSeriesNames.filter(name => !encoding.series.sort.includes(name)))
|
|
81
|
+
} else if (encoding.series.sort === 'original') {
|
|
82
|
+
const seriesOrder: string[] = []
|
|
83
|
+
data.forEach((d) => {
|
|
84
|
+
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
85
|
+
if (!seriesOrder.includes(seriesKey)) {
|
|
86
|
+
seriesOrder.push(seriesKey)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
sortedSeriesNames = seriesOrder
|
|
90
|
+
} else if (encoding.series.sort === 'alphabetical') {
|
|
91
|
+
sortedSeriesNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 建立 dataset 內排序後的 category 名稱陣列(僅作為屬性與色彩來源,不做拆樹)
|
|
95
|
+
const categoryMap = new Map<string, RawDataColumn[]>()
|
|
96
|
+
data.forEach((d) => {
|
|
97
|
+
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
98
|
+
if (!categoryMap.has(categoryKey)) {
|
|
99
|
+
categoryMap.set(categoryKey, [])
|
|
100
|
+
}
|
|
101
|
+
categoryMap.get(categoryKey)!.push(d)
|
|
102
|
+
})
|
|
103
|
+
let sortedCategoryNames: string[] = Array.from(categoryMap.keys())
|
|
104
|
+
if (Array.isArray(encoding.category.sort)) {
|
|
105
|
+
sortedCategoryNames = encoding.category.sort.filter(name => categoryMap.has(name))
|
|
106
|
+
.concat(sortedCategoryNames.filter(name => !encoding.category.sort.includes(name)))
|
|
107
|
+
} else if (encoding.category.sort === 'original') {
|
|
108
|
+
const categoryOrder: string[] = []
|
|
109
|
+
data.forEach((d) => {
|
|
110
|
+
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
111
|
+
if (!categoryOrder.includes(categoryKey)) {
|
|
112
|
+
categoryOrder.push(categoryKey)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
sortedCategoryNames = categoryOrder
|
|
116
|
+
} else if (encoding.category.sort === 'alphabetical') {
|
|
117
|
+
sortedCategoryNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 收集所有節點並依據 id 進行聚合(同一 dataset 內)
|
|
121
|
+
const nodeMap = new Map<string, RawDataColumn[]>()
|
|
122
|
+
data.forEach((d) => {
|
|
123
|
+
const nodeId = d.id || 'unknown'
|
|
124
|
+
if (!nodeMap.has(nodeId)) {
|
|
125
|
+
nodeMap.set(nodeId, [])
|
|
126
|
+
}
|
|
127
|
+
nodeMap.get(nodeId)!.push(d)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
let globalNodeIndex = 0
|
|
131
|
+
const defaultSeriesName = sortedSeriesNames[0] || 'default'
|
|
132
|
+
const defaultSeriesIndex = sortedSeriesNames.indexOf(defaultSeriesName)
|
|
133
|
+
|
|
134
|
+
// 為每個 id 建立 ModelDatumTree,並進行聚合
|
|
135
|
+
const treeNodeMap = new Map<string, ModelDatumTree>()
|
|
136
|
+
|
|
137
|
+
Array.from(nodeMap.entries()).forEach(([nodeId, nodeItems]) => {
|
|
138
|
+
const firstItem = nodeItems[0]
|
|
139
|
+
const seriesName = (firstItem as any)[encoding.series.from] || 'default'
|
|
140
|
+
const seriesIndex = sortedSeriesNames.indexOf(seriesName)
|
|
141
|
+
const categoryName = (firstItem as any)[encoding.category.from] || 'default'
|
|
142
|
+
const categoryIndex = sortedCategoryNames.indexOf(categoryName)
|
|
143
|
+
|
|
144
|
+
if (encoding.value.aggregate === 'none') {
|
|
145
|
+
// 不聚合,使用第一筆資料
|
|
146
|
+
const value = (firstItem as any)[encoding.value.from]
|
|
147
|
+
const treeNode: ModelDatumTree = {
|
|
148
|
+
id: nodeId,
|
|
149
|
+
index: globalNodeIndex++,
|
|
150
|
+
modelType: 'tree',
|
|
151
|
+
name: firstItem.name || nodeId,
|
|
152
|
+
data: firstItem.data,
|
|
153
|
+
value: typeof value === 'number' ? value : null,
|
|
154
|
+
color: getColorByFrom(encoding.color.from, {
|
|
155
|
+
index: globalNodeIndex - 1,
|
|
156
|
+
seriesIndex,
|
|
157
|
+
categoryIndex,
|
|
158
|
+
datasetIndex
|
|
159
|
+
}, theme),
|
|
160
|
+
parent: firstItem.parent || null,
|
|
161
|
+
parentIndex: null, // 稍後設定
|
|
162
|
+
depth: 0, // 稍後計算
|
|
163
|
+
seq: 0, // 稍後計算
|
|
164
|
+
children: [],
|
|
165
|
+
series: seriesName,
|
|
166
|
+
seriesIndex,
|
|
167
|
+
category: categoryName,
|
|
168
|
+
categoryIndex,
|
|
169
|
+
}
|
|
170
|
+
treeNodeMap.set(nodeId, treeNode)
|
|
171
|
+
} else {
|
|
172
|
+
// 進行聚合,將相同 dataset, id 的資料合併
|
|
173
|
+
const values: (number | null)[] = nodeItems.map(d => {
|
|
174
|
+
if (encoding.value.aggregate === 'count') {
|
|
175
|
+
return 1 // count 聚合時每筆資料計為 1
|
|
176
|
+
}
|
|
177
|
+
const value = (d as any)[encoding.value.from]
|
|
178
|
+
return typeof value === 'number' ? value : null
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const aggregatedValue = aggregate(values, encoding.value.aggregate)
|
|
182
|
+
|
|
183
|
+
const treeNode: ModelDatumTree = {
|
|
184
|
+
id: nodeId,
|
|
185
|
+
index: globalNodeIndex++,
|
|
186
|
+
modelType: 'tree',
|
|
187
|
+
name: firstItem.name || nodeId,
|
|
188
|
+
data: firstItem.data,
|
|
189
|
+
value: aggregatedValue,
|
|
190
|
+
color: getColorByFrom(encoding.color.from, {
|
|
191
|
+
index: globalNodeIndex - 1,
|
|
192
|
+
seriesIndex,
|
|
193
|
+
categoryIndex,
|
|
194
|
+
datasetIndex
|
|
195
|
+
}, theme),
|
|
196
|
+
parent: firstItem.parent || null,
|
|
197
|
+
parentIndex: null, // 稍後設定
|
|
198
|
+
depth: 0, // 稍後計算
|
|
199
|
+
seq: 0, // 稍後計算
|
|
200
|
+
children: [],
|
|
201
|
+
series: seriesName,
|
|
202
|
+
seriesIndex,
|
|
203
|
+
category: categoryName,
|
|
204
|
+
categoryIndex,
|
|
205
|
+
}
|
|
206
|
+
treeNodeMap.set(nodeId, treeNode)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// 建立樹狀結構
|
|
211
|
+
const allNodes = Array.from(treeNodeMap.values())
|
|
212
|
+
const nodeIndexMap = new Map<string, number>()
|
|
213
|
+
allNodes.forEach(node => {
|
|
214
|
+
nodeIndexMap.set(node.id, node.index)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// 設定 parentIndex
|
|
218
|
+
allNodes.forEach(node => {
|
|
219
|
+
if (node.parent && nodeIndexMap.has(node.parent)) {
|
|
220
|
+
node.parentIndex = nodeIndexMap.get(node.parent)!
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// 建立父子關係
|
|
225
|
+
const rootNodes: ModelDatumTree[] = []
|
|
226
|
+
const childrenMap = new Map<string, ModelDatumTree[]>()
|
|
227
|
+
|
|
228
|
+
allNodes.forEach(node => {
|
|
229
|
+
if (node.parent === null) {
|
|
230
|
+
rootNodes.push(node)
|
|
231
|
+
} else {
|
|
232
|
+
if (!childrenMap.has(node.parent)) {
|
|
233
|
+
childrenMap.set(node.parent, [])
|
|
234
|
+
}
|
|
235
|
+
childrenMap.get(node.parent)!.push(node)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// 遞迴建立完整樹狀結構並計算 depth 和 seq
|
|
240
|
+
function buildTree(node: ModelDatumTree, depth: number): ModelDatumTree {
|
|
241
|
+
node.depth = depth
|
|
242
|
+
const children = childrenMap.get(node.id) || []
|
|
243
|
+
|
|
244
|
+
// 根據 value.sort 對子節點進行排序
|
|
245
|
+
if (encoding.value.sort === 'asc') {
|
|
246
|
+
children.sort((a, b) => {
|
|
247
|
+
if (a.value === null && b.value === null) return 0
|
|
248
|
+
if (a.value === null) return 1
|
|
249
|
+
if (b.value === null) return -1
|
|
250
|
+
return a.value - b.value
|
|
251
|
+
})
|
|
252
|
+
} else if (encoding.value.sort === 'desc') {
|
|
253
|
+
children.sort((a, b) => {
|
|
254
|
+
if (a.value === null && b.value === null) return 0
|
|
255
|
+
if (a.value === null) return 1
|
|
256
|
+
if (b.value === null) return -1
|
|
257
|
+
return b.value - a.value
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
// 'original' 不需要額外排序,保持原始順序
|
|
261
|
+
|
|
262
|
+
// 設定 seq
|
|
263
|
+
children.forEach((child, index) => {
|
|
264
|
+
child.seq = index
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// 遞迴處理子節點
|
|
268
|
+
node.children = children.map(child => buildTree(child, depth + 1))
|
|
269
|
+
|
|
270
|
+
return node
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 建立完整的樹
|
|
274
|
+
const trees = rootNodes.map(root => buildTree(root, 0))
|
|
275
|
+
|
|
276
|
+
// 如果有多個根節點,建立一個虛擬根節點
|
|
277
|
+
if (trees.length === 0) {
|
|
278
|
+
// 沒有資料,建立空樹
|
|
279
|
+
result.push({
|
|
280
|
+
id: `empty-${datasetName}`,
|
|
281
|
+
index: globalNodeIndex++,
|
|
282
|
+
modelType: 'tree',
|
|
283
|
+
name: 'Empty Tree',
|
|
284
|
+
data: {},
|
|
285
|
+
value: null,
|
|
286
|
+
color: getColorByFrom(encoding.color.from, {
|
|
287
|
+
index: globalNodeIndex - 1,
|
|
288
|
+
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
289
|
+
categoryIndex: 0,
|
|
290
|
+
datasetIndex
|
|
291
|
+
}, theme),
|
|
292
|
+
parent: null,
|
|
293
|
+
parentIndex: null,
|
|
294
|
+
depth: 0,
|
|
295
|
+
seq: 0,
|
|
296
|
+
children: [],
|
|
297
|
+
series: defaultSeriesName,
|
|
298
|
+
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
299
|
+
category: 'default',
|
|
300
|
+
categoryIndex: 0,
|
|
301
|
+
})
|
|
302
|
+
} else if (trees.length === 1) {
|
|
303
|
+
// 單一根節點
|
|
304
|
+
result.push(trees[0])
|
|
305
|
+
} else {
|
|
306
|
+
const virtualRoot: ModelDatumTree = {
|
|
307
|
+
id: `virtual-root-${datasetName}`,
|
|
308
|
+
index: globalNodeIndex++,
|
|
309
|
+
modelType: 'tree',
|
|
310
|
+
name: `Virtual Root (${datasetName})`,
|
|
311
|
+
data: {},
|
|
312
|
+
value: null,
|
|
313
|
+
color: getColorByFrom(encoding.color.from, {
|
|
314
|
+
index: globalNodeIndex - 1,
|
|
315
|
+
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
316
|
+
categoryIndex: 0,
|
|
317
|
+
datasetIndex
|
|
318
|
+
}, theme),
|
|
319
|
+
parent: null,
|
|
320
|
+
parentIndex: null,
|
|
321
|
+
depth: 0,
|
|
322
|
+
seq: 0,
|
|
323
|
+
children: trees.map((tree, index) => {
|
|
324
|
+
const updateDepth = (node: ModelDatumTree, newDepth: number): ModelDatumTree => {
|
|
325
|
+
node.depth = newDepth
|
|
326
|
+
if (newDepth === 1) {
|
|
327
|
+
node.seq = index
|
|
328
|
+
}
|
|
329
|
+
node.children = node.children.map(child => updateDepth(child, newDepth + 1))
|
|
330
|
+
return node
|
|
331
|
+
}
|
|
332
|
+
return updateDepth(tree, 1)
|
|
333
|
+
}),
|
|
334
|
+
series: defaultSeriesName,
|
|
335
|
+
seriesIndex: defaultSeriesIndex < 0 ? 0 : defaultSeriesIndex,
|
|
336
|
+
category: 'default',
|
|
337
|
+
categoryIndex: 0,
|
|
338
|
+
}
|
|
339
|
+
result.push(virtualRoot)
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
return result
|
|
344
|
+
}
|