@orbcharts/core 3.0.7 → 4.0.0-alpha.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.
Files changed (196) hide show
  1. package/dist/orbcharts-core.es.js +2826 -6582
  2. package/dist/orbcharts-core.umd.js +6 -6
  3. package/dist/src/OrbCharts.d.ts +18 -0
  4. package/dist/src/chart/createChart.d.ts +3 -0
  5. package/dist/src/chart/createGraphData.d.ts +3 -0
  6. package/dist/src/chart/createGridData.d.ts +3 -0
  7. package/dist/src/chart/createMultivariateData.d.ts +3 -0
  8. package/dist/src/chart/createSeriesData.d.ts +3 -0
  9. package/dist/src/chart/createTreeData.d.ts +3 -0
  10. package/dist/src/chart/defaults.d.ts +5 -0
  11. package/dist/src/defineCanvasLayer.d.ts +16 -0
  12. package/dist/src/defineCanvasPlugin.d.ts +22 -0
  13. package/dist/src/defineSVGLayer.d.ts +16 -0
  14. package/dist/src/defineSVGPlugin.d.ts +22 -0
  15. package/dist/src/index.d.ts +6 -14
  16. package/dist/src/layer/createLayer.d.ts +3 -0
  17. package/dist/src/plugin/createPlugin.d.ts +3 -0
  18. package/dist/src/test/createGraphData.test.d.ts +1 -0
  19. package/dist/src/test/createTreeData.test.d.ts +1 -0
  20. package/dist/src/test/simple-graph-test.d.ts +74 -0
  21. package/dist/src/test/simple-tree-test.d.ts +13 -0
  22. package/dist/src/types/Chart.d.ts +39 -0
  23. package/dist/src/types/ChartContext.d.ts +27 -0
  24. package/dist/src/types/Common.d.ts +3 -0
  25. package/dist/src/types/Encoding.d.ts +33 -0
  26. package/dist/src/types/Event.d.ts +12 -0
  27. package/dist/src/types/Layers.d.ts +55 -0
  28. package/dist/src/types/ModelData.d.ts +70 -0
  29. package/dist/src/types/Plugin.d.ts +39 -0
  30. package/dist/src/types/RawData.d.ts +18 -0
  31. package/dist/src/types/RenderData.d.ts +4 -0
  32. package/dist/src/types/Theme.d.ts +17 -0
  33. package/dist/src/types/Validator.d.ts +20 -0
  34. package/dist/src/types/index.d.ts +12 -0
  35. package/dist/src/utils/aggregateUtils.d.ts +37 -0
  36. package/dist/src/utils/colorUtils.d.ts +22 -0
  37. package/dist/src/utils/commonUtils.d.ts +3 -5
  38. package/dist/src/utils/dom-lifecycle.d.ts +37 -0
  39. package/dist/src/utils/dom.d.ts +6 -0
  40. package/dist/src/utils/index.d.ts +5 -1
  41. package/dist/src/utils/observables.d.ts +1 -25
  42. package/dist/src/utils/orbchartsUtils.d.ts +2 -53
  43. package/dist/src/utils/validator.d.ts +2 -2
  44. package/dist/test/aggregateTest.d.ts +1 -0
  45. package/package.json +24 -13
  46. package/src/OrbCharts.ts +35 -0
  47. package/src/chart/createChart.ts +997 -0
  48. package/src/chart/createGraphData.ts +391 -0
  49. package/src/chart/createGridData.ts +247 -0
  50. package/src/chart/createMultivariateData.ts +181 -0
  51. package/src/chart/createSeriesData.ts +297 -0
  52. package/src/chart/createTreeData.ts +344 -0
  53. package/src/chart/defaults.ts +120 -0
  54. package/src/defineCanvasLayer.ts +24 -0
  55. package/src/defineCanvasPlugin.ts +39 -0
  56. package/src/defineSVGLayer.ts +24 -0
  57. package/src/defineSVGPlugin.ts +39 -0
  58. package/src/index.ts +6 -18
  59. package/src/layer/createLayer.ts +138 -0
  60. package/src/plugin/createPlugin.ts +470 -0
  61. package/src/test/createGraphData.test.ts +103 -0
  62. package/src/test/createTreeData.test.ts +97 -0
  63. package/src/test/simple-graph-test.js +51 -0
  64. package/src/test/simple-tree-test.js +58 -0
  65. package/src/types/Chart.ts +62 -0
  66. package/src/types/ChartContext.ts +42 -0
  67. package/src/types/Common.ts +5 -0
  68. package/src/types/Encoding.ts +43 -0
  69. package/src/types/Event.ts +26 -0
  70. package/src/types/Layers.ts +93 -0
  71. package/src/types/ModelData.ts +95 -0
  72. package/src/types/Plugin.ts +98 -0
  73. package/src/types/RawData.ts +67 -0
  74. package/src/types/RenderData.ts +16 -0
  75. package/src/types/Theme.ts +21 -0
  76. package/src/types/Validator.ts +36 -0
  77. package/src/types/index.ts +12 -0
  78. package/src/utils/aggregateUtils.ts +99 -0
  79. package/src/utils/colorUtils.ts +63 -0
  80. package/src/utils/commonUtils.ts +12 -11
  81. package/src/utils/dom-lifecycle.ts +164 -0
  82. package/src/utils/dom.ts +55 -0
  83. package/src/utils/index.ts +6 -2
  84. package/src/utils/observables.ts +1 -292
  85. package/src/utils/orbchartsUtils.ts +6 -393
  86. package/src/utils/validator.ts +15 -14
  87. package/dist/lib/core-types.d.ts +0 -1
  88. package/dist/src/AbstractChart.d.ts +0 -19
  89. package/dist/src/GridChart.d.ts +0 -6
  90. package/dist/src/MultiGridChart.d.ts +0 -6
  91. package/dist/src/MultiValueChart.d.ts +0 -6
  92. package/dist/src/RelationshipChart.d.ts +0 -6
  93. package/dist/src/SeriesChart.d.ts +0 -6
  94. package/dist/src/TreeChart.d.ts +0 -6
  95. package/dist/src/base/createBaseChart.d.ts +0 -3
  96. package/dist/src/base/createBasePlugin.d.ts +0 -3
  97. package/dist/src/base/validators/chartOptionsValidator.d.ts +0 -3
  98. package/dist/src/base/validators/chartParamsValidator.d.ts +0 -3
  99. package/dist/src/base/validators/elementValidator.d.ts +0 -3
  100. package/dist/src/base/validators/pluginsValidator.d.ts +0 -3
  101. package/dist/src/defaults.d.ts +0 -25
  102. package/dist/src/defineGridPlugin.d.ts +0 -1
  103. package/dist/src/defineMultiGridPlugin.d.ts +0 -1
  104. package/dist/src/defineMultiValuePlugin.d.ts +0 -1
  105. package/dist/src/defineNoneDataPlugin.d.ts +0 -1
  106. package/dist/src/defineRelationshipPlugin.d.ts +0 -1
  107. package/dist/src/defineSeriesPlugin.d.ts +0 -1
  108. package/dist/src/defineTreePlugin.d.ts +0 -1
  109. package/dist/src/grid/computedDataFn.d.ts +0 -4
  110. package/dist/src/grid/contextObserverCallback.d.ts +0 -3
  111. package/dist/src/grid/dataFormatterValidator.d.ts +0 -3
  112. package/dist/src/grid/dataValidator.d.ts +0 -3
  113. package/dist/src/grid/gridObservables.d.ts +0 -64
  114. package/dist/src/multiGrid/computedDataFn.d.ts +0 -3
  115. package/dist/src/multiGrid/contextObserverCallback.d.ts +0 -3
  116. package/dist/src/multiGrid/dataFormatterValidator.d.ts +0 -3
  117. package/dist/src/multiGrid/dataValidator.d.ts +0 -3
  118. package/dist/src/multiGrid/multiGridObservables.d.ts +0 -16
  119. package/dist/src/multiValue/computedDataFn.d.ts +0 -3
  120. package/dist/src/multiValue/contextObserverCallback.d.ts +0 -3
  121. package/dist/src/multiValue/dataFormatterValidator.d.ts +0 -3
  122. package/dist/src/multiValue/dataValidator.d.ts +0 -3
  123. package/dist/src/multiValue/multiValueObservables.d.ts +0 -130
  124. package/dist/src/relationship/computedDataFn.d.ts +0 -3
  125. package/dist/src/relationship/contextObserverCallback.d.ts +0 -3
  126. package/dist/src/relationship/dataFormatterValidator.d.ts +0 -3
  127. package/dist/src/relationship/dataValidator.d.ts +0 -3
  128. package/dist/src/relationship/relationshipObservables.d.ts +0 -13
  129. package/dist/src/series/computedDataFn.d.ts +0 -3
  130. package/dist/src/series/contextObserverCallback.d.ts +0 -3
  131. package/dist/src/series/dataFormatterValidator.d.ts +0 -3
  132. package/dist/src/series/dataValidator.d.ts +0 -3
  133. package/dist/src/series/seriesObservables.d.ts +0 -37
  134. package/dist/src/tree/computedDataFn.d.ts +0 -3
  135. package/dist/src/tree/contextObserverCallback.d.ts +0 -3
  136. package/dist/src/tree/dataFormatterValidator.d.ts +0 -3
  137. package/dist/src/tree/dataValidator.d.ts +0 -3
  138. package/dist/src/tree/treeObservables.d.ts +0 -10
  139. package/dist/src/utils/d3Scale.d.ts +0 -28
  140. package/lib/core-types.ts +0 -7
  141. package/src/AbstractChart.ts +0 -57
  142. package/src/GridChart.ts +0 -25
  143. package/src/MultiGridChart.ts +0 -25
  144. package/src/MultiValueChart.ts +0 -25
  145. package/src/RelationshipChart.ts +0 -25
  146. package/src/SeriesChart.ts +0 -25
  147. package/src/TreeChart.ts +0 -25
  148. package/src/base/createBaseChart.ts +0 -524
  149. package/src/base/createBasePlugin.ts +0 -154
  150. package/src/base/validators/chartOptionsValidator.ts +0 -24
  151. package/src/base/validators/chartParamsValidator.ts +0 -134
  152. package/src/base/validators/elementValidator.ts +0 -14
  153. package/src/base/validators/pluginsValidator.ts +0 -15
  154. package/src/defaults.ts +0 -284
  155. package/src/defineGridPlugin.ts +0 -3
  156. package/src/defineMultiGridPlugin.ts +0 -3
  157. package/src/defineMultiValuePlugin.ts +0 -3
  158. package/src/defineNoneDataPlugin.ts +0 -4
  159. package/src/defineRelationshipPlugin.ts +0 -3
  160. package/src/defineSeriesPlugin.ts +0 -3
  161. package/src/defineTreePlugin.ts +0 -3
  162. package/src/grid/computedDataFn.ts +0 -129
  163. package/src/grid/contextObserverCallback.ts +0 -209
  164. package/src/grid/dataFormatterValidator.ts +0 -126
  165. package/src/grid/dataValidator.ts +0 -13
  166. package/src/grid/gridObservables.ts +0 -699
  167. package/src/multiGrid/computedDataFn.ts +0 -123
  168. package/src/multiGrid/contextObserverCallback.ts +0 -109
  169. package/src/multiGrid/dataFormatterValidator.ts +0 -121
  170. package/src/multiGrid/dataValidator.ts +0 -13
  171. package/src/multiGrid/multiGridObservables.ts +0 -367
  172. package/src/multiValue/computedDataFn.ts +0 -113
  173. package/src/multiValue/contextObserverCallback.ts +0 -328
  174. package/src/multiValue/dataFormatterValidator.ts +0 -95
  175. package/src/multiValue/dataValidator.ts +0 -13
  176. package/src/multiValue/multiValueObservables.ts +0 -865
  177. package/src/relationship/computedDataFn.ts +0 -159
  178. package/src/relationship/contextObserverCallback.ts +0 -80
  179. package/src/relationship/dataFormatterValidator.ts +0 -14
  180. package/src/relationship/dataValidator.ts +0 -14
  181. package/src/relationship/relationshipObservables.ts +0 -85
  182. package/src/series/computedDataFn.ts +0 -88
  183. package/src/series/contextObserverCallback.ts +0 -132
  184. package/src/series/dataFormatterValidator.ts +0 -47
  185. package/src/series/dataValidator.ts +0 -13
  186. package/src/series/seriesObservables.ts +0 -210
  187. package/src/tree/computedDataFn.ts +0 -129
  188. package/src/tree/contextObserverCallback.ts +0 -58
  189. package/src/tree/dataFormatterValidator.ts +0 -14
  190. package/src/tree/dataValidator.ts +0 -14
  191. package/src/tree/treeObservables.ts +0 -106
  192. package/src/utils/d3Scale.ts +0 -198
  193. package/tsconfig.base.json +0 -14
  194. package/tsconfig.json +0 -3
  195. package/vite-env.d.ts +0 -7
  196. package/vite.config.js +0 -23
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }