@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,247 +1,247 @@
|
|
|
1
|
-
import type { RawData, RawDataColumn, Encoding, ModelDataGrid, ModelDatumGrid, Theme } from '../types'
|
|
2
|
-
import { aggregate } from '../utils/aggregateUtils'
|
|
3
|
-
import { getColorByFrom } from '../utils/colorUtils'
|
|
4
|
-
|
|
5
|
-
export const createGridData = (rawData: RawData, encoding: Encoding, theme: Theme): ModelDataGrid[] => {
|
|
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
|
-
// original 排序:依照原始資料中 dataset 名稱出現的順序
|
|
41
|
-
const datasetOrder: string[] = []
|
|
42
|
-
if (is2DArray) {
|
|
43
|
-
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
44
|
-
datasetArray.forEach((d) => {
|
|
45
|
-
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
46
|
-
if (!datasetOrder.includes(datasetKey)) {
|
|
47
|
-
datasetOrder.push(datasetKey)
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
} else {
|
|
52
|
-
(rawData as RawDataColumn[]).forEach((d) => {
|
|
53
|
-
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
54
|
-
if (!datasetOrder.includes(datasetKey)) {
|
|
55
|
-
datasetOrder.push(datasetKey)
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
sortedDatasetNames = datasetOrder
|
|
60
|
-
} else if (encoding.dataset.sort === 'alphabetical') {
|
|
61
|
-
// alphabetical 排序:依照字母順序
|
|
62
|
-
sortedDatasetNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 對每個 dataset 進行 grid 資料的處理
|
|
66
|
-
const result: ModelDataGrid[] = []
|
|
67
|
-
sortedDatasetNames.forEach((datasetName, datasetIndex) => {
|
|
68
|
-
const data = datasetMap.get(datasetName)!
|
|
69
|
-
|
|
70
|
-
// 依據 series 欄位將資料分組
|
|
71
|
-
const seriesMap = new Map<string, RawDataColumn[]>()
|
|
72
|
-
data.forEach((d) => {
|
|
73
|
-
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
74
|
-
if (!seriesMap.has(seriesKey)) {
|
|
75
|
-
seriesMap.set(seriesKey, [])
|
|
76
|
-
}
|
|
77
|
-
seriesMap.get(seriesKey)!.push(d)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
// 建立排序後的系列名稱陣列
|
|
81
|
-
let sortedSeriesNames: string[] = Array.from(seriesMap.keys())
|
|
82
|
-
if (Array.isArray(encoding.series.sort)) {
|
|
83
|
-
sortedSeriesNames = encoding.series.sort.filter(name => seriesMap.has(name))
|
|
84
|
-
.concat(sortedSeriesNames.filter(name => !encoding.series.sort.includes(name)))
|
|
85
|
-
} else if (encoding.series.sort === 'original') {
|
|
86
|
-
// original 排序:依照原始資料中 series 名稱出現的順序
|
|
87
|
-
const seriesOrder: string[] = []
|
|
88
|
-
data.forEach((d) => {
|
|
89
|
-
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
90
|
-
if (!seriesOrder.includes(seriesKey)) {
|
|
91
|
-
seriesOrder.push(seriesKey)
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
sortedSeriesNames = seriesOrder
|
|
95
|
-
} else if (encoding.series.sort === 'alphabetical') {
|
|
96
|
-
// alphabetical 排序:依照字母順序
|
|
97
|
-
sortedSeriesNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 建立全局的 category 排序(跨該 dataset 下所有 series)
|
|
101
|
-
const globalCategoryOrder: string[] = []
|
|
102
|
-
data.forEach((d) => {
|
|
103
|
-
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
104
|
-
if (!globalCategoryOrder.includes(categoryKey)) {
|
|
105
|
-
globalCategoryOrder.push(categoryKey)
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
const globalCategorySet = new Set<string>(globalCategoryOrder)
|
|
109
|
-
let globalSortedCategoryNames: string[] = Array.from(globalCategorySet)
|
|
110
|
-
if (Array.isArray(encoding.category.sort)) {
|
|
111
|
-
globalSortedCategoryNames = (encoding.category.sort as string[]).filter(name => globalCategorySet.has(name))
|
|
112
|
-
.concat(globalSortedCategoryNames.filter(name => !(encoding.category.sort as string[]).includes(name)))
|
|
113
|
-
} else if (encoding.category.sort === 'original') {
|
|
114
|
-
// original 排序:依照原始資料中 category 名稱出現的順序
|
|
115
|
-
globalSortedCategoryNames = globalCategoryOrder
|
|
116
|
-
} else if (encoding.category.sort === 'alphabetical') {
|
|
117
|
-
// alphabetical 排序:依照字母順序
|
|
118
|
-
globalSortedCategoryNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 依據排序後的系列名稱來建立最終的資料結構
|
|
122
|
-
const gridData: ModelDatumGrid[][] = []
|
|
123
|
-
sortedSeriesNames.forEach((seriesName, seriesIndex) => {
|
|
124
|
-
const seriesItems = seriesMap.get(seriesName)!
|
|
125
|
-
|
|
126
|
-
// 依據 category 欄位將 series 內的資料分組
|
|
127
|
-
const categoryMap = new Map<string, RawDataColumn[]>()
|
|
128
|
-
seriesItems.forEach((d) => {
|
|
129
|
-
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
130
|
-
if (!categoryMap.has(categoryKey)) {
|
|
131
|
-
categoryMap.set(categoryKey, [])
|
|
132
|
-
}
|
|
133
|
-
categoryMap.get(categoryKey)!.push(d)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// 處理每個 category(使用全局排序後的 category 名稱,確保所有 series 的 category 順序一致)
|
|
137
|
-
const seriesData: ModelDatumGrid[] = []
|
|
138
|
-
globalSortedCategoryNames.forEach((categoryName, categoryIndex) => {
|
|
139
|
-
const categoryItems = categoryMap.get(categoryName)
|
|
140
|
-
|
|
141
|
-
if (!categoryItems) {
|
|
142
|
-
// 該 series 缺少此 category,以 value 為 null 的預設資料補上
|
|
143
|
-
seriesData.push({
|
|
144
|
-
id: `${datasetName}-${seriesName}-${categoryName}-null`,
|
|
145
|
-
index: categoryIndex,
|
|
146
|
-
modelType: 'grid',
|
|
147
|
-
name: categoryName,
|
|
148
|
-
data: undefined,
|
|
149
|
-
value: null,
|
|
150
|
-
color: getColorByFrom(encoding.color.from, {
|
|
151
|
-
index: categoryIndex,
|
|
152
|
-
seriesIndex,
|
|
153
|
-
categoryIndex,
|
|
154
|
-
datasetIndex
|
|
155
|
-
}, theme),
|
|
156
|
-
series: seriesName,
|
|
157
|
-
seriesIndex,
|
|
158
|
-
category: categoryName,
|
|
159
|
-
categoryIndex,
|
|
160
|
-
})
|
|
161
|
-
} else if (encoding.value.aggregate === 'none') {
|
|
162
|
-
// 不聚合,保持原始資料結構
|
|
163
|
-
let modelData: ModelDatumGrid[] = categoryItems.map((d, index) => {
|
|
164
|
-
const value = (d as any)[encoding.value.from]
|
|
165
|
-
return {
|
|
166
|
-
id: d.id || `${datasetName}-${seriesName}-${categoryName}-${index}`,
|
|
167
|
-
index: categoryIndex, // Grid 模式下 index 對應 categoryIndex
|
|
168
|
-
modelType: 'grid',
|
|
169
|
-
name: d.name || '',
|
|
170
|
-
data: d.data,
|
|
171
|
-
value: typeof value === 'number' ? value : null,
|
|
172
|
-
color: getColorByFrom(encoding.color.from, {
|
|
173
|
-
index: categoryIndex,
|
|
174
|
-
seriesIndex,
|
|
175
|
-
categoryIndex,
|
|
176
|
-
datasetIndex
|
|
177
|
-
}, theme),
|
|
178
|
-
series: seriesName,
|
|
179
|
-
seriesIndex,
|
|
180
|
-
category: categoryName,
|
|
181
|
-
categoryIndex,
|
|
182
|
-
}
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
// 根據 value.sort 進行排序
|
|
186
|
-
if (encoding.value.sort === 'asc') {
|
|
187
|
-
modelData.sort((a, b) => {
|
|
188
|
-
if (a.value === null && b.value === null) return 0
|
|
189
|
-
if (a.value === null) return 1
|
|
190
|
-
if (b.value === null) return -1
|
|
191
|
-
return a.value - b.value
|
|
192
|
-
})
|
|
193
|
-
} else if (encoding.value.sort === 'desc') {
|
|
194
|
-
modelData.sort((a, b) => {
|
|
195
|
-
if (a.value === null && b.value === null) return 0
|
|
196
|
-
if (a.value === null) return 1
|
|
197
|
-
if (b.value === null) return -1
|
|
198
|
-
return b.value - a.value
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
// 'original' 不需要額外排序,保持原始順序
|
|
202
|
-
|
|
203
|
-
seriesData.push(...modelData)
|
|
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 modelData: ModelDatumGrid = {
|
|
219
|
-
id: firstItem.id || `${datasetName}-${seriesName}-${categoryName}-aggregated`,
|
|
220
|
-
index: categoryIndex, // Grid 模式下 index 對應 categoryIndex
|
|
221
|
-
modelType: 'grid',
|
|
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
|
-
seriesData.push(modelData)
|
|
237
|
-
}
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
gridData.push(seriesData)
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
result.push(gridData)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
return result
|
|
247
|
-
}
|
|
1
|
+
import type { RawData, RawDataColumn, Encoding, ModelDataGrid, ModelDatumGrid, Theme } from '../types'
|
|
2
|
+
import { aggregate } from '../utils/aggregateUtils'
|
|
3
|
+
import { getColorByFrom } from '../utils/colorUtils'
|
|
4
|
+
|
|
5
|
+
export const createGridData = (rawData: RawData, encoding: Encoding, theme: Theme): ModelDataGrid[] => {
|
|
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
|
+
// original 排序:依照原始資料中 dataset 名稱出現的順序
|
|
41
|
+
const datasetOrder: string[] = []
|
|
42
|
+
if (is2DArray) {
|
|
43
|
+
(rawData as RawDataColumn[][]).forEach((datasetArray, datasetIndex) => {
|
|
44
|
+
datasetArray.forEach((d) => {
|
|
45
|
+
const datasetKey = (d as any)[encoding.dataset.from] || `dataset-${datasetIndex}`
|
|
46
|
+
if (!datasetOrder.includes(datasetKey)) {
|
|
47
|
+
datasetOrder.push(datasetKey)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
} else {
|
|
52
|
+
(rawData as RawDataColumn[]).forEach((d) => {
|
|
53
|
+
const datasetKey = (d as any)[encoding.dataset.from] || 'default'
|
|
54
|
+
if (!datasetOrder.includes(datasetKey)) {
|
|
55
|
+
datasetOrder.push(datasetKey)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
sortedDatasetNames = datasetOrder
|
|
60
|
+
} else if (encoding.dataset.sort === 'alphabetical') {
|
|
61
|
+
// alphabetical 排序:依照字母順序
|
|
62
|
+
sortedDatasetNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 對每個 dataset 進行 grid 資料的處理
|
|
66
|
+
const result: ModelDataGrid[] = []
|
|
67
|
+
sortedDatasetNames.forEach((datasetName, datasetIndex) => {
|
|
68
|
+
const data = datasetMap.get(datasetName)!
|
|
69
|
+
|
|
70
|
+
// 依據 series 欄位將資料分組
|
|
71
|
+
const seriesMap = new Map<string, RawDataColumn[]>()
|
|
72
|
+
data.forEach((d) => {
|
|
73
|
+
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
74
|
+
if (!seriesMap.has(seriesKey)) {
|
|
75
|
+
seriesMap.set(seriesKey, [])
|
|
76
|
+
}
|
|
77
|
+
seriesMap.get(seriesKey)!.push(d)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// 建立排序後的系列名稱陣列
|
|
81
|
+
let sortedSeriesNames: string[] = Array.from(seriesMap.keys())
|
|
82
|
+
if (Array.isArray(encoding.series.sort)) {
|
|
83
|
+
sortedSeriesNames = encoding.series.sort.filter(name => seriesMap.has(name))
|
|
84
|
+
.concat(sortedSeriesNames.filter(name => !encoding.series.sort.includes(name)))
|
|
85
|
+
} else if (encoding.series.sort === 'original') {
|
|
86
|
+
// original 排序:依照原始資料中 series 名稱出現的順序
|
|
87
|
+
const seriesOrder: string[] = []
|
|
88
|
+
data.forEach((d) => {
|
|
89
|
+
const seriesKey = (d as any)[encoding.series.from] || 'default'
|
|
90
|
+
if (!seriesOrder.includes(seriesKey)) {
|
|
91
|
+
seriesOrder.push(seriesKey)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
sortedSeriesNames = seriesOrder
|
|
95
|
+
} else if (encoding.series.sort === 'alphabetical') {
|
|
96
|
+
// alphabetical 排序:依照字母順序
|
|
97
|
+
sortedSeriesNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 建立全局的 category 排序(跨該 dataset 下所有 series)
|
|
101
|
+
const globalCategoryOrder: string[] = []
|
|
102
|
+
data.forEach((d) => {
|
|
103
|
+
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
104
|
+
if (!globalCategoryOrder.includes(categoryKey)) {
|
|
105
|
+
globalCategoryOrder.push(categoryKey)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
const globalCategorySet = new Set<string>(globalCategoryOrder)
|
|
109
|
+
let globalSortedCategoryNames: string[] = Array.from(globalCategorySet)
|
|
110
|
+
if (Array.isArray(encoding.category.sort)) {
|
|
111
|
+
globalSortedCategoryNames = (encoding.category.sort as string[]).filter(name => globalCategorySet.has(name))
|
|
112
|
+
.concat(globalSortedCategoryNames.filter(name => !(encoding.category.sort as string[]).includes(name)))
|
|
113
|
+
} else if (encoding.category.sort === 'original') {
|
|
114
|
+
// original 排序:依照原始資料中 category 名稱出現的順序
|
|
115
|
+
globalSortedCategoryNames = globalCategoryOrder
|
|
116
|
+
} else if (encoding.category.sort === 'alphabetical') {
|
|
117
|
+
// alphabetical 排序:依照字母順序
|
|
118
|
+
globalSortedCategoryNames.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 依據排序後的系列名稱來建立最終的資料結構
|
|
122
|
+
const gridData: ModelDatumGrid[][] = []
|
|
123
|
+
sortedSeriesNames.forEach((seriesName, seriesIndex) => {
|
|
124
|
+
const seriesItems = seriesMap.get(seriesName)!
|
|
125
|
+
|
|
126
|
+
// 依據 category 欄位將 series 內的資料分組
|
|
127
|
+
const categoryMap = new Map<string, RawDataColumn[]>()
|
|
128
|
+
seriesItems.forEach((d) => {
|
|
129
|
+
const categoryKey = (d as any)[encoding.category.from] || 'default'
|
|
130
|
+
if (!categoryMap.has(categoryKey)) {
|
|
131
|
+
categoryMap.set(categoryKey, [])
|
|
132
|
+
}
|
|
133
|
+
categoryMap.get(categoryKey)!.push(d)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// 處理每個 category(使用全局排序後的 category 名稱,確保所有 series 的 category 順序一致)
|
|
137
|
+
const seriesData: ModelDatumGrid[] = []
|
|
138
|
+
globalSortedCategoryNames.forEach((categoryName, categoryIndex) => {
|
|
139
|
+
const categoryItems = categoryMap.get(categoryName)
|
|
140
|
+
|
|
141
|
+
if (!categoryItems) {
|
|
142
|
+
// 該 series 缺少此 category,以 value 為 null 的預設資料補上
|
|
143
|
+
seriesData.push({
|
|
144
|
+
id: `${datasetName}-${seriesName}-${categoryName}-null`,
|
|
145
|
+
index: categoryIndex,
|
|
146
|
+
modelType: 'grid',
|
|
147
|
+
name: categoryName,
|
|
148
|
+
data: undefined,
|
|
149
|
+
value: null,
|
|
150
|
+
color: getColorByFrom(encoding.color.from, {
|
|
151
|
+
index: categoryIndex,
|
|
152
|
+
seriesIndex,
|
|
153
|
+
categoryIndex,
|
|
154
|
+
datasetIndex
|
|
155
|
+
}, theme),
|
|
156
|
+
series: seriesName,
|
|
157
|
+
seriesIndex,
|
|
158
|
+
category: categoryName,
|
|
159
|
+
categoryIndex,
|
|
160
|
+
})
|
|
161
|
+
} else if (encoding.value.aggregate === 'none') {
|
|
162
|
+
// 不聚合,保持原始資料結構
|
|
163
|
+
let modelData: ModelDatumGrid[] = categoryItems.map((d, index) => {
|
|
164
|
+
const value = (d as any)[encoding.value.from]
|
|
165
|
+
return {
|
|
166
|
+
id: d.id || `${datasetName}-${seriesName}-${categoryName}-${index}`,
|
|
167
|
+
index: categoryIndex, // Grid 模式下 index 對應 categoryIndex
|
|
168
|
+
modelType: 'grid',
|
|
169
|
+
name: d.name || '',
|
|
170
|
+
data: d.data,
|
|
171
|
+
value: typeof value === 'number' ? value : null,
|
|
172
|
+
color: getColorByFrom(encoding.color.from, {
|
|
173
|
+
index: categoryIndex,
|
|
174
|
+
seriesIndex,
|
|
175
|
+
categoryIndex,
|
|
176
|
+
datasetIndex
|
|
177
|
+
}, theme),
|
|
178
|
+
series: seriesName,
|
|
179
|
+
seriesIndex,
|
|
180
|
+
category: categoryName,
|
|
181
|
+
categoryIndex,
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// 根據 value.sort 進行排序
|
|
186
|
+
if (encoding.value.sort === 'asc') {
|
|
187
|
+
modelData.sort((a, b) => {
|
|
188
|
+
if (a.value === null && b.value === null) return 0
|
|
189
|
+
if (a.value === null) return 1
|
|
190
|
+
if (b.value === null) return -1
|
|
191
|
+
return a.value - b.value
|
|
192
|
+
})
|
|
193
|
+
} else if (encoding.value.sort === 'desc') {
|
|
194
|
+
modelData.sort((a, b) => {
|
|
195
|
+
if (a.value === null && b.value === null) return 0
|
|
196
|
+
if (a.value === null) return 1
|
|
197
|
+
if (b.value === null) return -1
|
|
198
|
+
return b.value - a.value
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
// 'original' 不需要額外排序,保持原始順序
|
|
202
|
+
|
|
203
|
+
seriesData.push(...modelData)
|
|
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 modelData: ModelDatumGrid = {
|
|
219
|
+
id: firstItem.id || `${datasetName}-${seriesName}-${categoryName}-aggregated`,
|
|
220
|
+
index: categoryIndex, // Grid 模式下 index 對應 categoryIndex
|
|
221
|
+
modelType: 'grid',
|
|
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
|
+
seriesData.push(modelData)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
gridData.push(seriesData)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
result.push(gridData)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
}
|