@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,391 +1,391 @@
|
|
|
1
|
-
import type { RawData, RawDataColumn, Encoding, ModelDataGraph, ModelDatumGraphNode, ModelDatumGraphEdge, Theme } from '../types'
|
|
2
|
-
import { aggregate } from '../utils/aggregateUtils'
|
|
3
|
-
import { getColorByFrom } from '../utils/colorUtils'
|
|
4
|
-
|
|
5
|
-
export const createGraphData = (rawData: RawData, encoding: Encoding, theme: Theme): ModelDataGraph[] => {
|
|
6
|
-
// 判斷是一維陣列還是二維陣列
|
|
7
|
-
const is2DArray = Array.isArray(rawData[0])
|
|
8
|
-
|
|
9
|
-
// 分離 nodes 和 edges 資料
|
|
10
|
-
const nodeData: RawDataColumn[] = []
|
|
11
|
-
const edgeData: RawDataColumn[] = []
|
|
12
|
-
|
|
13
|
-
if (is2DArray) {
|
|
14
|
-
(rawData as RawDataColumn[][]).forEach(datasetArray => {
|
|
15
|
-
datasetArray.forEach(d => {
|
|
16
|
-
if (d.source && d.target) {
|
|
17
|
-
edgeData.push(d)
|
|
18
|
-
} else {
|
|
19
|
-
nodeData.push(d)
|
|
20
|
-
}
|
|
21
|
-
})
|
|
22
|
-
})
|
|
23
|
-
} else {
|
|
24
|
-
(rawData as RawDataColumn[]).forEach(d => {
|
|
25
|
-
if (d.source && d.target) {
|
|
26
|
-
edgeData.push(d)
|
|
27
|
-
} else {
|
|
28
|
-
nodeData.push(d)
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 依據 dataset 欄位將 node 資料分組
|
|
34
|
-
const datasetMap = new Map<string, RawDataColumn[]>()
|
|
35
|
-
|
|
36
|
-
if (is2DArray) {
|
|
37
|
-
// 二維陣列:需要追蹤每個 node 來自哪個子陣列
|
|
38
|
-
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
39
|
-
datasetArray.forEach((d) => {
|
|
40
|
-
if (!d.source || !d.target) {
|
|
41
|
-
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
42
|
-
if (!datasetMap.has(datasetKey)) {
|
|
43
|
-
datasetMap.set(datasetKey, [])
|
|
44
|
-
}
|
|
45
|
-
datasetMap.get(datasetKey)!.push(d)
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
} else {
|
|
50
|
-
nodeData.forEach((d) => {
|
|
51
|
-
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
52
|
-
if (!datasetMap.has(datasetKey)) {
|
|
53
|
-
datasetMap.set(datasetKey, [])
|
|
54
|
-
}
|
|
55
|
-
datasetMap.get(datasetKey)!.push(d)
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 建立排序後的 dataset 名稱陣列
|
|
60
|
-
let sortedDatasetNames: string[] = Array.from(datasetMap.keys())
|
|
61
|
-
if (Array.isArray(encoding.dataset.sort)) {
|
|
62
|
-
sortedDatasetNames = encoding.dataset.sort.filter(name => datasetMap.has(name))
|
|
63
|
-
.concat(sortedDatasetNames.filter(name => !encoding.dataset.sort.includes(name)))
|
|
64
|
-
} else if (encoding.dataset.sort === 'original') {
|
|
65
|
-
const datasetOrder: string[] = []
|
|
66
|
-
if (is2DArray) {
|
|
67
|
-
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
68
|
-
datasetArray.forEach((d) => {
|
|
69
|
-
if (!d.source || !d.target) {
|
|
70
|
-
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
71
|
-
if (!datasetOrder.includes(datasetKey)) {
|
|
72
|
-
datasetOrder.push(datasetKey)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
} else {
|
|
78
|
-
nodeData.forEach((d) => {
|
|
79
|
-
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
80
|
-
if (!datasetOrder.includes(datasetKey)) {
|
|
81
|
-
datasetOrder.push(datasetKey)
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
sortedDatasetNames = datasetOrder
|
|
86
|
-
} else if (encoding.dataset.sort === 'alphabetical') {
|
|
87
|
-
sortedDatasetNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 對每個 dataset 進行 graph 資料的處理
|
|
91
|
-
const result: ModelDataGraph[] = []
|
|
92
|
-
sortedDatasetNames.forEach((datasetName, datasetIndex) => {
|
|
93
|
-
const data = datasetMap.get(datasetName)!
|
|
94
|
-
|
|
95
|
-
// 建立 nodes
|
|
96
|
-
const nodes: ModelDatumGraphNode[] = []
|
|
97
|
-
|
|
98
|
-
// 依據 series 欄位將資料分組
|
|
99
|
-
const seriesMap = new Map<string, RawDataColumn[]>()
|
|
100
|
-
data.forEach((d) => {
|
|
101
|
-
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
102
|
-
if (!seriesMap.has(seriesKey)) {
|
|
103
|
-
seriesMap.set(seriesKey, [])
|
|
104
|
-
}
|
|
105
|
-
seriesMap.get(seriesKey)!.push(d)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
// 建立排序後的系列名稱陣列
|
|
109
|
-
let sortedSeriesNames: string[] = Array.from(seriesMap.keys())
|
|
110
|
-
if (Array.isArray(encoding.series.sort)) {
|
|
111
|
-
sortedSeriesNames = encoding.series.sort.filter(name => seriesMap.has(name))
|
|
112
|
-
.concat(sortedSeriesNames.filter(name => !encoding.series.sort.includes(name)))
|
|
113
|
-
} else if (encoding.series.sort === 'original') {
|
|
114
|
-
const seriesOrder: string[] = []
|
|
115
|
-
data.forEach((d) => {
|
|
116
|
-
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
117
|
-
if (!seriesOrder.includes(seriesKey)) {
|
|
118
|
-
seriesOrder.push(seriesKey)
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
sortedSeriesNames = seriesOrder
|
|
122
|
-
} else if (encoding.series.sort === 'alphabetical') {
|
|
123
|
-
sortedSeriesNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 依據排序後的系列名稱來建立 nodes
|
|
127
|
-
sortedSeriesNames.forEach((seriesName, seriesIndex) => {
|
|
128
|
-
const seriesItems = seriesMap.get(seriesName)!
|
|
129
|
-
|
|
130
|
-
// 依據 category 欄位將 series 內的資料分組
|
|
131
|
-
const categoryMap = new Map<string, RawDataColumn[]>()
|
|
132
|
-
seriesItems.forEach((d) => {
|
|
133
|
-
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
134
|
-
if (!categoryMap.has(categoryKey)) {
|
|
135
|
-
categoryMap.set(categoryKey, [])
|
|
136
|
-
}
|
|
137
|
-
categoryMap.get(categoryKey)!.push(d)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// 建立排序後的類別名稱陣列
|
|
141
|
-
let sortedCategoryNames: string[] = Array.from(categoryMap.keys())
|
|
142
|
-
if (Array.isArray(encoding.category.sort)) {
|
|
143
|
-
sortedCategoryNames = encoding.category.sort.filter(name => categoryMap.has(name))
|
|
144
|
-
.concat(sortedCategoryNames.filter(name => !encoding.category.sort.includes(name)))
|
|
145
|
-
} else if (encoding.category.sort === 'original') {
|
|
146
|
-
const categoryOrder: string[] = []
|
|
147
|
-
seriesItems.forEach((d) => {
|
|
148
|
-
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
149
|
-
if (!categoryOrder.includes(categoryKey)) {
|
|
150
|
-
categoryOrder.push(categoryKey)
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
sortedCategoryNames = categoryOrder
|
|
154
|
-
} else if (encoding.category.sort === 'alphabetical') {
|
|
155
|
-
sortedCategoryNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 處理每個 category
|
|
159
|
-
sortedCategoryNames.forEach((categoryName, categoryIndex) => {
|
|
160
|
-
const categoryItems = categoryMap.get(categoryName)!
|
|
161
|
-
|
|
162
|
-
if (encoding.value.aggregate === 'none') {
|
|
163
|
-
// 不聚合,保持原始資料結構
|
|
164
|
-
let modelNodes: ModelDatumGraphNode[] = categoryItems.map((d, index) => {
|
|
165
|
-
const value = (d as any)[encoding.value.from]
|
|
166
|
-
return {
|
|
167
|
-
id: d.id || `${datasetName}-${seriesName}-${categoryName}-${index}`,
|
|
168
|
-
index: nodes.length + index, // 在所有 nodes 中的索引
|
|
169
|
-
modelType: 'graph',
|
|
170
|
-
name: d.name || '',
|
|
171
|
-
data: d.data,
|
|
172
|
-
value: typeof value === 'number' ? value : null,
|
|
173
|
-
color: getColorByFrom(encoding.color.from, {
|
|
174
|
-
index: categoryIndex,
|
|
175
|
-
seriesIndex,
|
|
176
|
-
categoryIndex,
|
|
177
|
-
datasetIndex
|
|
178
|
-
}, theme),
|
|
179
|
-
series: seriesName,
|
|
180
|
-
seriesIndex,
|
|
181
|
-
category: categoryName,
|
|
182
|
-
categoryIndex,
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
// 根據 value.sort 進行排序
|
|
187
|
-
if (encoding.value.sort === 'asc') {
|
|
188
|
-
modelNodes.sort((a, b) => {
|
|
189
|
-
if (a.value === null && b.value === null) return 0
|
|
190
|
-
if (a.value === null) return 1
|
|
191
|
-
if (b.value === null) return -1
|
|
192
|
-
return a.value - b.value
|
|
193
|
-
})
|
|
194
|
-
} else if (encoding.value.sort === 'desc') {
|
|
195
|
-
modelNodes.sort((a, b) => {
|
|
196
|
-
if (a.value === null && b.value === null) return 0
|
|
197
|
-
if (a.value === null) return 1
|
|
198
|
-
if (b.value === null) return -1
|
|
199
|
-
return b.value - a.value
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
nodes.push(...modelNodes)
|
|
204
|
-
} else {
|
|
205
|
-
// 進行聚合,將相同 dataset, series, category 的資料合併為一筆
|
|
206
|
-
const values: (number | null)[] = categoryItems.map(d => {
|
|
207
|
-
if (encoding.value.aggregate === 'count') {
|
|
208
|
-
return 1 // count 聚合時每筆資料計為 1
|
|
209
|
-
}
|
|
210
|
-
const value = (d as any)[encoding.value.from]
|
|
211
|
-
return typeof value === 'number' ? value : null
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
const aggregatedValue = aggregate(values, encoding.value.aggregate)
|
|
215
|
-
|
|
216
|
-
// 合併其他欄位(使用第一筆資料的值)
|
|
217
|
-
const firstItem = categoryItems[0]
|
|
218
|
-
const modelNode: ModelDatumGraphNode = {
|
|
219
|
-
id: firstItem.id || `${datasetName}-${seriesName}-${categoryName}-aggregated`,
|
|
220
|
-
index: nodes.length, // 在所有 nodes 中的索引
|
|
221
|
-
modelType: 'graph',
|
|
222
|
-
name: firstItem.name || categoryName,
|
|
223
|
-
data: firstItem.data,
|
|
224
|
-
value: aggregatedValue,
|
|
225
|
-
color: getColorByFrom(encoding.color.from, {
|
|
226
|
-
index: categoryIndex,
|
|
227
|
-
seriesIndex,
|
|
228
|
-
categoryIndex,
|
|
229
|
-
datasetIndex
|
|
230
|
-
}, theme),
|
|
231
|
-
series: seriesName,
|
|
232
|
-
seriesIndex,
|
|
233
|
-
category: categoryName,
|
|
234
|
-
categoryIndex,
|
|
235
|
-
}
|
|
236
|
-
nodes.push(modelNode)
|
|
237
|
-
}
|
|
238
|
-
})
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// 建立 edges
|
|
242
|
-
const edges: ModelDatumGraphEdge[] = []
|
|
243
|
-
const nodeNameToIndexMap = new Map<string, number>()
|
|
244
|
-
nodes.forEach((node) => {
|
|
245
|
-
// 使用原始資料的 id 或 name 作為識別鍵
|
|
246
|
-
const originalData = nodeData.find(d => d.id === node.id || d.name === node.name)
|
|
247
|
-
if (originalData?.id) {
|
|
248
|
-
nodeNameToIndexMap.set(originalData.id, node.index)
|
|
249
|
-
}
|
|
250
|
-
if (originalData?.name) {
|
|
251
|
-
nodeNameToIndexMap.set(originalData.name, node.index)
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
// 建立 series/category 名稱到索引的映射(基於 nodes,共用相同的 seriesIndex/categoryIndex)
|
|
256
|
-
const seriesNameToIndexMap = new Map<string, number>()
|
|
257
|
-
const categoryNameToIndexMap = new Map<string, number>()
|
|
258
|
-
nodes.forEach((node) => {
|
|
259
|
-
if (!seriesNameToIndexMap.has(node.series)) {
|
|
260
|
-
seriesNameToIndexMap.set(node.series, node.seriesIndex)
|
|
261
|
-
}
|
|
262
|
-
if (!categoryNameToIndexMap.has(node.category)) {
|
|
263
|
-
categoryNameToIndexMap.set(node.category, node.categoryIndex)
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
let nextSeriesIndex = seriesNameToIndexMap.size
|
|
267
|
-
let nextCategoryIndex = categoryNameToIndexMap.size
|
|
268
|
-
const getEdgeSeriesIndex = (name: string): number => {
|
|
269
|
-
if (!seriesNameToIndexMap.has(name)) {
|
|
270
|
-
seriesNameToIndexMap.set(name, nextSeriesIndex++)
|
|
271
|
-
}
|
|
272
|
-
return seriesNameToIndexMap.get(name)!
|
|
273
|
-
}
|
|
274
|
-
const getEdgeCategoryIndex = (name: string): number => {
|
|
275
|
-
if (!categoryNameToIndexMap.has(name)) {
|
|
276
|
-
categoryNameToIndexMap.set(name, nextCategoryIndex++)
|
|
277
|
-
}
|
|
278
|
-
return categoryNameToIndexMap.get(name)!
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// 依據 dataset 分組處理 edges(這裡使用所有邊的資料,但只處理目前 dataset 的邊)
|
|
282
|
-
const datasetEdges = edgeData.filter(d => {
|
|
283
|
-
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
284
|
-
return datasetKey === datasetName
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
// 聚合 edges
|
|
288
|
-
const edgeGroupMap = new Map<string, RawDataColumn[]>()
|
|
289
|
-
datasetEdges.forEach((d) => {
|
|
290
|
-
const source = d.source!
|
|
291
|
-
const target = d.target!
|
|
292
|
-
const series = (d as any)[encoding.series.from] || 'default'
|
|
293
|
-
const category = (d as any)[encoding.category.from] || 'default'
|
|
294
|
-
const groupKey = `${source}-${target}-${series}-${category}`
|
|
295
|
-
|
|
296
|
-
if (!edgeGroupMap.has(groupKey)) {
|
|
297
|
-
edgeGroupMap.set(groupKey, [])
|
|
298
|
-
}
|
|
299
|
-
edgeGroupMap.get(groupKey)!.push(d)
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
Array.from(edgeGroupMap.entries()).forEach(([groupKey, groupEdges], edgeIndex) => {
|
|
303
|
-
const firstEdge = groupEdges[0]
|
|
304
|
-
const source = firstEdge.source!
|
|
305
|
-
const target = firstEdge.target!
|
|
306
|
-
const sourceIndex = nodeNameToIndexMap.get(source) ?? -1
|
|
307
|
-
const targetIndex = nodeNameToIndexMap.get(target) ?? -1
|
|
308
|
-
|
|
309
|
-
// 只有當 source 和 target 都存在於 nodes 中時才建立 edge
|
|
310
|
-
if (sourceIndex >= 0 && targetIndex >= 0) {
|
|
311
|
-
if (encoding.value.aggregate === 'none') {
|
|
312
|
-
// 不聚合,保持原始資料結構
|
|
313
|
-
groupEdges.forEach((d, index) => {
|
|
314
|
-
const value = (d as any)[encoding.value.from]
|
|
315
|
-
const edgeSeries = (d as any)[encoding.series.from] || 'default'
|
|
316
|
-
const edgeCategory = (d as any)[encoding.category.from] || 'default'
|
|
317
|
-
const edgeSeriesIndex = getEdgeSeriesIndex(edgeSeries)
|
|
318
|
-
const edgeCategoryIndex = getEdgeCategoryIndex(edgeCategory)
|
|
319
|
-
const edge: ModelDatumGraphEdge = {
|
|
320
|
-
id: d.id || `edge-${edgeIndex}-${index}`,
|
|
321
|
-
index: edges.length,
|
|
322
|
-
modelType: 'graph',
|
|
323
|
-
name: d.name || '',
|
|
324
|
-
data: d.data,
|
|
325
|
-
value: typeof value === 'number' ? value : null,
|
|
326
|
-
color: getColorByFrom(encoding.color.from, {
|
|
327
|
-
index: edges.length,
|
|
328
|
-
seriesIndex: edgeSeriesIndex,
|
|
329
|
-
categoryIndex: edgeCategoryIndex,
|
|
330
|
-
datasetIndex
|
|
331
|
-
}, theme),
|
|
332
|
-
series: edgeSeries,
|
|
333
|
-
seriesIndex: edgeSeriesIndex,
|
|
334
|
-
category: edgeCategory,
|
|
335
|
-
categoryIndex: edgeCategoryIndex,
|
|
336
|
-
source,
|
|
337
|
-
sourceIndex,
|
|
338
|
-
target,
|
|
339
|
-
targetIndex,
|
|
340
|
-
}
|
|
341
|
-
edges.push(edge)
|
|
342
|
-
})
|
|
343
|
-
} else {
|
|
344
|
-
// 進行聚合
|
|
345
|
-
const values: (number | null)[] = groupEdges.map(d => {
|
|
346
|
-
if (encoding.value.aggregate === 'count') {
|
|
347
|
-
return 1
|
|
348
|
-
}
|
|
349
|
-
const value = (d as any)[encoding.value.from]
|
|
350
|
-
return typeof value === 'number' ? value : null
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
const aggregatedValue = aggregate(values, encoding.value.aggregate)
|
|
354
|
-
|
|
355
|
-
const edgeSeries = (firstEdge as any)[encoding.series.from] || 'default'
|
|
356
|
-
const edgeCategory = (firstEdge as any)[encoding.category.from] || 'default'
|
|
357
|
-
const edgeSeriesIndex = getEdgeSeriesIndex(edgeSeries)
|
|
358
|
-
const edgeCategoryIndex = getEdgeCategoryIndex(edgeCategory)
|
|
359
|
-
|
|
360
|
-
const edge: ModelDatumGraphEdge = {
|
|
361
|
-
id: firstEdge.id || `edge-${edgeIndex}-aggregated`,
|
|
362
|
-
index: edges.length,
|
|
363
|
-
modelType: 'graph',
|
|
364
|
-
name: firstEdge.name || '',
|
|
365
|
-
data: firstEdge.data,
|
|
366
|
-
value: aggregatedValue,
|
|
367
|
-
color: getColorByFrom(encoding.color.from, {
|
|
368
|
-
index: edges.length,
|
|
369
|
-
seriesIndex: edgeSeriesIndex,
|
|
370
|
-
categoryIndex: edgeCategoryIndex,
|
|
371
|
-
datasetIndex
|
|
372
|
-
}, theme),
|
|
373
|
-
series: edgeSeries,
|
|
374
|
-
seriesIndex: edgeSeriesIndex,
|
|
375
|
-
category: edgeCategory,
|
|
376
|
-
categoryIndex: edgeCategoryIndex,
|
|
377
|
-
source,
|
|
378
|
-
sourceIndex,
|
|
379
|
-
target,
|
|
380
|
-
targetIndex,
|
|
381
|
-
}
|
|
382
|
-
edges.push(edge)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
result.push({ nodes, edges })
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
return result
|
|
391
|
-
}
|
|
1
|
+
import type { RawData, RawDataColumn, Encoding, ModelDataGraph, ModelDatumGraphNode, ModelDatumGraphEdge, Theme } from '../types'
|
|
2
|
+
import { aggregate } from '../utils/aggregateUtils'
|
|
3
|
+
import { getColorByFrom } from '../utils/colorUtils'
|
|
4
|
+
|
|
5
|
+
export const createGraphData = (rawData: RawData, encoding: Encoding, theme: Theme): ModelDataGraph[] => {
|
|
6
|
+
// 判斷是一維陣列還是二維陣列
|
|
7
|
+
const is2DArray = Array.isArray(rawData[0])
|
|
8
|
+
|
|
9
|
+
// 分離 nodes 和 edges 資料
|
|
10
|
+
const nodeData: RawDataColumn[] = []
|
|
11
|
+
const edgeData: RawDataColumn[] = []
|
|
12
|
+
|
|
13
|
+
if (is2DArray) {
|
|
14
|
+
(rawData as RawDataColumn[][]).forEach(datasetArray => {
|
|
15
|
+
datasetArray.forEach(d => {
|
|
16
|
+
if (d.source && d.target) {
|
|
17
|
+
edgeData.push(d)
|
|
18
|
+
} else {
|
|
19
|
+
nodeData.push(d)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
} else {
|
|
24
|
+
(rawData as RawDataColumn[]).forEach(d => {
|
|
25
|
+
if (d.source && d.target) {
|
|
26
|
+
edgeData.push(d)
|
|
27
|
+
} else {
|
|
28
|
+
nodeData.push(d)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 依據 dataset 欄位將 node 資料分組
|
|
34
|
+
const datasetMap = new Map<string, RawDataColumn[]>()
|
|
35
|
+
|
|
36
|
+
if (is2DArray) {
|
|
37
|
+
// 二維陣列:需要追蹤每個 node 來自哪個子陣列
|
|
38
|
+
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
39
|
+
datasetArray.forEach((d) => {
|
|
40
|
+
if (!d.source || !d.target) {
|
|
41
|
+
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
42
|
+
if (!datasetMap.has(datasetKey)) {
|
|
43
|
+
datasetMap.set(datasetKey, [])
|
|
44
|
+
}
|
|
45
|
+
datasetMap.get(datasetKey)!.push(d)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
} else {
|
|
50
|
+
nodeData.forEach((d) => {
|
|
51
|
+
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
52
|
+
if (!datasetMap.has(datasetKey)) {
|
|
53
|
+
datasetMap.set(datasetKey, [])
|
|
54
|
+
}
|
|
55
|
+
datasetMap.get(datasetKey)!.push(d)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 建立排序後的 dataset 名稱陣列
|
|
60
|
+
let sortedDatasetNames: string[] = Array.from(datasetMap.keys())
|
|
61
|
+
if (Array.isArray(encoding.dataset.sort)) {
|
|
62
|
+
sortedDatasetNames = encoding.dataset.sort.filter(name => datasetMap.has(name))
|
|
63
|
+
.concat(sortedDatasetNames.filter(name => !encoding.dataset.sort.includes(name)))
|
|
64
|
+
} else if (encoding.dataset.sort === 'original') {
|
|
65
|
+
const datasetOrder: string[] = []
|
|
66
|
+
if (is2DArray) {
|
|
67
|
+
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
68
|
+
datasetArray.forEach((d) => {
|
|
69
|
+
if (!d.source || !d.target) {
|
|
70
|
+
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
71
|
+
if (!datasetOrder.includes(datasetKey)) {
|
|
72
|
+
datasetOrder.push(datasetKey)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
} else {
|
|
78
|
+
nodeData.forEach((d) => {
|
|
79
|
+
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
80
|
+
if (!datasetOrder.includes(datasetKey)) {
|
|
81
|
+
datasetOrder.push(datasetKey)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
sortedDatasetNames = datasetOrder
|
|
86
|
+
} else if (encoding.dataset.sort === 'alphabetical') {
|
|
87
|
+
sortedDatasetNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 對每個 dataset 進行 graph 資料的處理
|
|
91
|
+
const result: ModelDataGraph[] = []
|
|
92
|
+
sortedDatasetNames.forEach((datasetName, datasetIndex) => {
|
|
93
|
+
const data = datasetMap.get(datasetName)!
|
|
94
|
+
|
|
95
|
+
// 建立 nodes
|
|
96
|
+
const nodes: ModelDatumGraphNode[] = []
|
|
97
|
+
|
|
98
|
+
// 依據 series 欄位將資料分組
|
|
99
|
+
const seriesMap = new Map<string, RawDataColumn[]>()
|
|
100
|
+
data.forEach((d) => {
|
|
101
|
+
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
102
|
+
if (!seriesMap.has(seriesKey)) {
|
|
103
|
+
seriesMap.set(seriesKey, [])
|
|
104
|
+
}
|
|
105
|
+
seriesMap.get(seriesKey)!.push(d)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// 建立排序後的系列名稱陣列
|
|
109
|
+
let sortedSeriesNames: string[] = Array.from(seriesMap.keys())
|
|
110
|
+
if (Array.isArray(encoding.series.sort)) {
|
|
111
|
+
sortedSeriesNames = encoding.series.sort.filter(name => seriesMap.has(name))
|
|
112
|
+
.concat(sortedSeriesNames.filter(name => !encoding.series.sort.includes(name)))
|
|
113
|
+
} else if (encoding.series.sort === 'original') {
|
|
114
|
+
const seriesOrder: string[] = []
|
|
115
|
+
data.forEach((d) => {
|
|
116
|
+
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
117
|
+
if (!seriesOrder.includes(seriesKey)) {
|
|
118
|
+
seriesOrder.push(seriesKey)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
sortedSeriesNames = seriesOrder
|
|
122
|
+
} else if (encoding.series.sort === 'alphabetical') {
|
|
123
|
+
sortedSeriesNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 依據排序後的系列名稱來建立 nodes
|
|
127
|
+
sortedSeriesNames.forEach((seriesName, seriesIndex) => {
|
|
128
|
+
const seriesItems = seriesMap.get(seriesName)!
|
|
129
|
+
|
|
130
|
+
// 依據 category 欄位將 series 內的資料分組
|
|
131
|
+
const categoryMap = new Map<string, RawDataColumn[]>()
|
|
132
|
+
seriesItems.forEach((d) => {
|
|
133
|
+
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
134
|
+
if (!categoryMap.has(categoryKey)) {
|
|
135
|
+
categoryMap.set(categoryKey, [])
|
|
136
|
+
}
|
|
137
|
+
categoryMap.get(categoryKey)!.push(d)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// 建立排序後的類別名稱陣列
|
|
141
|
+
let sortedCategoryNames: string[] = Array.from(categoryMap.keys())
|
|
142
|
+
if (Array.isArray(encoding.category.sort)) {
|
|
143
|
+
sortedCategoryNames = encoding.category.sort.filter(name => categoryMap.has(name))
|
|
144
|
+
.concat(sortedCategoryNames.filter(name => !encoding.category.sort.includes(name)))
|
|
145
|
+
} else if (encoding.category.sort === 'original') {
|
|
146
|
+
const categoryOrder: string[] = []
|
|
147
|
+
seriesItems.forEach((d) => {
|
|
148
|
+
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
149
|
+
if (!categoryOrder.includes(categoryKey)) {
|
|
150
|
+
categoryOrder.push(categoryKey)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
sortedCategoryNames = categoryOrder
|
|
154
|
+
} else if (encoding.category.sort === 'alphabetical') {
|
|
155
|
+
sortedCategoryNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 處理每個 category
|
|
159
|
+
sortedCategoryNames.forEach((categoryName, categoryIndex) => {
|
|
160
|
+
const categoryItems = categoryMap.get(categoryName)!
|
|
161
|
+
|
|
162
|
+
if (encoding.value.aggregate === 'none') {
|
|
163
|
+
// 不聚合,保持原始資料結構
|
|
164
|
+
let modelNodes: ModelDatumGraphNode[] = categoryItems.map((d, index) => {
|
|
165
|
+
const value = (d as any)[encoding.value.from]
|
|
166
|
+
return {
|
|
167
|
+
id: d.id || `${datasetName}-${seriesName}-${categoryName}-${index}`,
|
|
168
|
+
index: nodes.length + index, // 在所有 nodes 中的索引
|
|
169
|
+
modelType: 'graph',
|
|
170
|
+
name: d.name || '',
|
|
171
|
+
data: d.data,
|
|
172
|
+
value: typeof value === 'number' ? value : null,
|
|
173
|
+
color: getColorByFrom(encoding.color.from, {
|
|
174
|
+
index: categoryIndex,
|
|
175
|
+
seriesIndex,
|
|
176
|
+
categoryIndex,
|
|
177
|
+
datasetIndex
|
|
178
|
+
}, theme),
|
|
179
|
+
series: seriesName,
|
|
180
|
+
seriesIndex,
|
|
181
|
+
category: categoryName,
|
|
182
|
+
categoryIndex,
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// 根據 value.sort 進行排序
|
|
187
|
+
if (encoding.value.sort === 'asc') {
|
|
188
|
+
modelNodes.sort((a, b) => {
|
|
189
|
+
if (a.value === null && b.value === null) return 0
|
|
190
|
+
if (a.value === null) return 1
|
|
191
|
+
if (b.value === null) return -1
|
|
192
|
+
return a.value - b.value
|
|
193
|
+
})
|
|
194
|
+
} else if (encoding.value.sort === 'desc') {
|
|
195
|
+
modelNodes.sort((a, b) => {
|
|
196
|
+
if (a.value === null && b.value === null) return 0
|
|
197
|
+
if (a.value === null) return 1
|
|
198
|
+
if (b.value === null) return -1
|
|
199
|
+
return b.value - a.value
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
nodes.push(...modelNodes)
|
|
204
|
+
} else {
|
|
205
|
+
// 進行聚合,將相同 dataset, series, category 的資料合併為一筆
|
|
206
|
+
const values: (number | null)[] = categoryItems.map(d => {
|
|
207
|
+
if (encoding.value.aggregate === 'count') {
|
|
208
|
+
return 1 // count 聚合時每筆資料計為 1
|
|
209
|
+
}
|
|
210
|
+
const value = (d as any)[encoding.value.from]
|
|
211
|
+
return typeof value === 'number' ? value : null
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const aggregatedValue = aggregate(values, encoding.value.aggregate)
|
|
215
|
+
|
|
216
|
+
// 合併其他欄位(使用第一筆資料的值)
|
|
217
|
+
const firstItem = categoryItems[0]
|
|
218
|
+
const modelNode: ModelDatumGraphNode = {
|
|
219
|
+
id: firstItem.id || `${datasetName}-${seriesName}-${categoryName}-aggregated`,
|
|
220
|
+
index: nodes.length, // 在所有 nodes 中的索引
|
|
221
|
+
modelType: 'graph',
|
|
222
|
+
name: firstItem.name || categoryName,
|
|
223
|
+
data: firstItem.data,
|
|
224
|
+
value: aggregatedValue,
|
|
225
|
+
color: getColorByFrom(encoding.color.from, {
|
|
226
|
+
index: categoryIndex,
|
|
227
|
+
seriesIndex,
|
|
228
|
+
categoryIndex,
|
|
229
|
+
datasetIndex
|
|
230
|
+
}, theme),
|
|
231
|
+
series: seriesName,
|
|
232
|
+
seriesIndex,
|
|
233
|
+
category: categoryName,
|
|
234
|
+
categoryIndex,
|
|
235
|
+
}
|
|
236
|
+
nodes.push(modelNode)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// 建立 edges
|
|
242
|
+
const edges: ModelDatumGraphEdge[] = []
|
|
243
|
+
const nodeNameToIndexMap = new Map<string, number>()
|
|
244
|
+
nodes.forEach((node) => {
|
|
245
|
+
// 使用原始資料的 id 或 name 作為識別鍵
|
|
246
|
+
const originalData = nodeData.find(d => d.id === node.id || d.name === node.name)
|
|
247
|
+
if (originalData?.id) {
|
|
248
|
+
nodeNameToIndexMap.set(originalData.id, node.index)
|
|
249
|
+
}
|
|
250
|
+
if (originalData?.name) {
|
|
251
|
+
nodeNameToIndexMap.set(originalData.name, node.index)
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// 建立 series/category 名稱到索引的映射(基於 nodes,共用相同的 seriesIndex/categoryIndex)
|
|
256
|
+
const seriesNameToIndexMap = new Map<string, number>()
|
|
257
|
+
const categoryNameToIndexMap = new Map<string, number>()
|
|
258
|
+
nodes.forEach((node) => {
|
|
259
|
+
if (!seriesNameToIndexMap.has(node.series)) {
|
|
260
|
+
seriesNameToIndexMap.set(node.series, node.seriesIndex)
|
|
261
|
+
}
|
|
262
|
+
if (!categoryNameToIndexMap.has(node.category)) {
|
|
263
|
+
categoryNameToIndexMap.set(node.category, node.categoryIndex)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
let nextSeriesIndex = seriesNameToIndexMap.size
|
|
267
|
+
let nextCategoryIndex = categoryNameToIndexMap.size
|
|
268
|
+
const getEdgeSeriesIndex = (name: string): number => {
|
|
269
|
+
if (!seriesNameToIndexMap.has(name)) {
|
|
270
|
+
seriesNameToIndexMap.set(name, nextSeriesIndex++)
|
|
271
|
+
}
|
|
272
|
+
return seriesNameToIndexMap.get(name)!
|
|
273
|
+
}
|
|
274
|
+
const getEdgeCategoryIndex = (name: string): number => {
|
|
275
|
+
if (!categoryNameToIndexMap.has(name)) {
|
|
276
|
+
categoryNameToIndexMap.set(name, nextCategoryIndex++)
|
|
277
|
+
}
|
|
278
|
+
return categoryNameToIndexMap.get(name)!
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 依據 dataset 分組處理 edges(這裡使用所有邊的資料,但只處理目前 dataset 的邊)
|
|
282
|
+
const datasetEdges = edgeData.filter(d => {
|
|
283
|
+
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
284
|
+
return datasetKey === datasetName
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// 聚合 edges
|
|
288
|
+
const edgeGroupMap = new Map<string, RawDataColumn[]>()
|
|
289
|
+
datasetEdges.forEach((d) => {
|
|
290
|
+
const source = d.source!
|
|
291
|
+
const target = d.target!
|
|
292
|
+
const series = (d as any)[encoding.series.from] || 'default'
|
|
293
|
+
const category = (d as any)[encoding.category.from] || 'default'
|
|
294
|
+
const groupKey = `${source}-${target}-${series}-${category}`
|
|
295
|
+
|
|
296
|
+
if (!edgeGroupMap.has(groupKey)) {
|
|
297
|
+
edgeGroupMap.set(groupKey, [])
|
|
298
|
+
}
|
|
299
|
+
edgeGroupMap.get(groupKey)!.push(d)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
Array.from(edgeGroupMap.entries()).forEach(([groupKey, groupEdges], edgeIndex) => {
|
|
303
|
+
const firstEdge = groupEdges[0]
|
|
304
|
+
const source = firstEdge.source!
|
|
305
|
+
const target = firstEdge.target!
|
|
306
|
+
const sourceIndex = nodeNameToIndexMap.get(source) ?? -1
|
|
307
|
+
const targetIndex = nodeNameToIndexMap.get(target) ?? -1
|
|
308
|
+
|
|
309
|
+
// 只有當 source 和 target 都存在於 nodes 中時才建立 edge
|
|
310
|
+
if (sourceIndex >= 0 && targetIndex >= 0) {
|
|
311
|
+
if (encoding.value.aggregate === 'none') {
|
|
312
|
+
// 不聚合,保持原始資料結構
|
|
313
|
+
groupEdges.forEach((d, index) => {
|
|
314
|
+
const value = (d as any)[encoding.value.from]
|
|
315
|
+
const edgeSeries = (d as any)[encoding.series.from] || 'default'
|
|
316
|
+
const edgeCategory = (d as any)[encoding.category.from] || 'default'
|
|
317
|
+
const edgeSeriesIndex = getEdgeSeriesIndex(edgeSeries)
|
|
318
|
+
const edgeCategoryIndex = getEdgeCategoryIndex(edgeCategory)
|
|
319
|
+
const edge: ModelDatumGraphEdge = {
|
|
320
|
+
id: d.id || `edge-${edgeIndex}-${index}`,
|
|
321
|
+
index: edges.length,
|
|
322
|
+
modelType: 'graph',
|
|
323
|
+
name: d.name || '',
|
|
324
|
+
data: d.data,
|
|
325
|
+
value: typeof value === 'number' ? value : null,
|
|
326
|
+
color: getColorByFrom(encoding.color.from, {
|
|
327
|
+
index: edges.length,
|
|
328
|
+
seriesIndex: edgeSeriesIndex,
|
|
329
|
+
categoryIndex: edgeCategoryIndex,
|
|
330
|
+
datasetIndex
|
|
331
|
+
}, theme),
|
|
332
|
+
series: edgeSeries,
|
|
333
|
+
seriesIndex: edgeSeriesIndex,
|
|
334
|
+
category: edgeCategory,
|
|
335
|
+
categoryIndex: edgeCategoryIndex,
|
|
336
|
+
source,
|
|
337
|
+
sourceIndex,
|
|
338
|
+
target,
|
|
339
|
+
targetIndex,
|
|
340
|
+
}
|
|
341
|
+
edges.push(edge)
|
|
342
|
+
})
|
|
343
|
+
} else {
|
|
344
|
+
// 進行聚合
|
|
345
|
+
const values: (number | null)[] = groupEdges.map(d => {
|
|
346
|
+
if (encoding.value.aggregate === 'count') {
|
|
347
|
+
return 1
|
|
348
|
+
}
|
|
349
|
+
const value = (d as any)[encoding.value.from]
|
|
350
|
+
return typeof value === 'number' ? value : null
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const aggregatedValue = aggregate(values, encoding.value.aggregate)
|
|
354
|
+
|
|
355
|
+
const edgeSeries = (firstEdge as any)[encoding.series.from] || 'default'
|
|
356
|
+
const edgeCategory = (firstEdge as any)[encoding.category.from] || 'default'
|
|
357
|
+
const edgeSeriesIndex = getEdgeSeriesIndex(edgeSeries)
|
|
358
|
+
const edgeCategoryIndex = getEdgeCategoryIndex(edgeCategory)
|
|
359
|
+
|
|
360
|
+
const edge: ModelDatumGraphEdge = {
|
|
361
|
+
id: firstEdge.id || `edge-${edgeIndex}-aggregated`,
|
|
362
|
+
index: edges.length,
|
|
363
|
+
modelType: 'graph',
|
|
364
|
+
name: firstEdge.name || '',
|
|
365
|
+
data: firstEdge.data,
|
|
366
|
+
value: aggregatedValue,
|
|
367
|
+
color: getColorByFrom(encoding.color.from, {
|
|
368
|
+
index: edges.length,
|
|
369
|
+
seriesIndex: edgeSeriesIndex,
|
|
370
|
+
categoryIndex: edgeCategoryIndex,
|
|
371
|
+
datasetIndex
|
|
372
|
+
}, theme),
|
|
373
|
+
series: edgeSeries,
|
|
374
|
+
seriesIndex: edgeSeriesIndex,
|
|
375
|
+
category: edgeCategory,
|
|
376
|
+
categoryIndex: edgeCategoryIndex,
|
|
377
|
+
source,
|
|
378
|
+
sourceIndex,
|
|
379
|
+
target,
|
|
380
|
+
targetIndex,
|
|
381
|
+
}
|
|
382
|
+
edges.push(edge)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
result.push({ nodes, edges })
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return result
|
|
391
|
+
}
|