@fluentui/react-charts 9.3.12 → 9.3.14
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/CHANGELOG.md +25 -2
- package/dist/index.d.ts +859 -0
- package/lib/VegaDeclarativeChart.js +1 -0
- package/lib/VegaDeclarativeChart.js.map +1 -0
- package/lib/components/LineChart/LineChart.js +47 -7
- package/lib/components/LineChart/LineChart.js.map +1 -1
- package/lib/components/ScatterChart/ScatterChart.js +12 -12
- package/lib/components/ScatterChart/ScatterChart.js.map +1 -1
- package/lib/components/VegaDeclarativeChart/VegaDeclarativeChart.js +405 -0
- package/lib/components/VegaDeclarativeChart/VegaDeclarativeChart.js.map +1 -0
- package/lib/components/VegaDeclarativeChart/VegaDeclarativeChartHooks.js +20 -0
- package/lib/components/VegaDeclarativeChart/VegaDeclarativeChartHooks.js.map +1 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteColorAdapter.js +415 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteColorAdapter.js.map +1 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteExpressionEvaluator.js +537 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteExpressionEvaluator.js.map +1 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteSchemaAdapter.js +3279 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteSchemaAdapter.js.map +1 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteTypes.js +28 -0
- package/lib/components/VegaDeclarativeChart/VegaLiteTypes.js.map +1 -0
- package/lib/components/VegaDeclarativeChart/index.js +1 -0
- package/lib/components/VegaDeclarativeChart/index.js.map +1 -0
- package/lib/components/VerticalStackedBarChart/VerticalStackedBarChart.js +5 -2
- package/lib/components/VerticalStackedBarChart/VerticalStackedBarChart.js.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/utilities/utilities.js +41 -0
- package/lib/utilities/utilities.js.map +1 -1
- package/lib-commonjs/VegaDeclarativeChart.js +6 -0
- package/lib-commonjs/VegaDeclarativeChart.js.map +1 -0
- package/lib-commonjs/components/LineChart/LineChart.js +46 -6
- package/lib-commonjs/components/LineChart/LineChart.js.map +1 -1
- package/lib-commonjs/components/ScatterChart/ScatterChart.js +11 -11
- package/lib-commonjs/components/ScatterChart/ScatterChart.js.map +1 -1
- package/lib-commonjs/components/VegaDeclarativeChart/VegaDeclarativeChart.js +274 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaDeclarativeChart.js.map +1 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaDeclarativeChartHooks.js +35 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaDeclarativeChartHooks.js.map +1 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteColorAdapter.js +412 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteColorAdapter.js.map +1 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteExpressionEvaluator.js +533 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteExpressionEvaluator.js.map +1 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteSchemaAdapter.js +3214 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteSchemaAdapter.js.map +1 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteTypes.js +31 -0
- package/lib-commonjs/components/VegaDeclarativeChart/VegaLiteTypes.js.map +1 -0
- package/lib-commonjs/components/VegaDeclarativeChart/index.js +6 -0
- package/lib-commonjs/components/VegaDeclarativeChart/index.js.map +1 -0
- package/lib-commonjs/components/VerticalStackedBarChart/VerticalStackedBarChart.js +4 -1
- package/lib-commonjs/components/VerticalStackedBarChart/VerticalStackedBarChart.js.map +1 -1
- package/lib-commonjs/index.js +1 -0
- package/lib-commonjs/index.js.map +1 -1
- package/lib-commonjs/utilities/utilities.js +33 -0
- package/lib-commonjs/utilities/utilities.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,3214 @@
|
|
|
1
|
+
// Using custom VegaLiteTypes for internal adapter logic
|
|
2
|
+
// For public API, VegaDeclarativeChart accepts vega-lite's TopLevelSpec
|
|
3
|
+
"use strict";
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
function _export(target, all) {
|
|
8
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
get: all[name]
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
_export(exports, {
|
|
14
|
+
autoCorrectEncodingTypes: function() {
|
|
15
|
+
return autoCorrectEncodingTypes;
|
|
16
|
+
},
|
|
17
|
+
getChartType: function() {
|
|
18
|
+
return getChartType;
|
|
19
|
+
},
|
|
20
|
+
getMarkType: function() {
|
|
21
|
+
return getMarkType;
|
|
22
|
+
},
|
|
23
|
+
getVegaLiteLegendsProps: function() {
|
|
24
|
+
return getVegaLiteLegendsProps;
|
|
25
|
+
},
|
|
26
|
+
getVegaLiteTitles: function() {
|
|
27
|
+
return getVegaLiteTitles;
|
|
28
|
+
},
|
|
29
|
+
transformVegaLiteToAreaChartProps: function() {
|
|
30
|
+
return transformVegaLiteToAreaChartProps;
|
|
31
|
+
},
|
|
32
|
+
transformVegaLiteToDonutChartProps: function() {
|
|
33
|
+
return transformVegaLiteToDonutChartProps;
|
|
34
|
+
},
|
|
35
|
+
transformVegaLiteToGroupedVerticalBarChartProps: function() {
|
|
36
|
+
return transformVegaLiteToGroupedVerticalBarChartProps;
|
|
37
|
+
},
|
|
38
|
+
transformVegaLiteToHeatMapChartProps: function() {
|
|
39
|
+
return transformVegaLiteToHeatMapChartProps;
|
|
40
|
+
},
|
|
41
|
+
transformVegaLiteToHistogramProps: function() {
|
|
42
|
+
return transformVegaLiteToHistogramProps;
|
|
43
|
+
},
|
|
44
|
+
transformVegaLiteToHorizontalBarChartProps: function() {
|
|
45
|
+
return transformVegaLiteToHorizontalBarChartProps;
|
|
46
|
+
},
|
|
47
|
+
transformVegaLiteToLineChartProps: function() {
|
|
48
|
+
return transformVegaLiteToLineChartProps;
|
|
49
|
+
},
|
|
50
|
+
transformVegaLiteToPolarChartProps: function() {
|
|
51
|
+
return transformVegaLiteToPolarChartProps;
|
|
52
|
+
},
|
|
53
|
+
transformVegaLiteToScatterChartProps: function() {
|
|
54
|
+
return transformVegaLiteToScatterChartProps;
|
|
55
|
+
},
|
|
56
|
+
transformVegaLiteToVerticalBarChartProps: function() {
|
|
57
|
+
return transformVegaLiteToVerticalBarChartProps;
|
|
58
|
+
},
|
|
59
|
+
transformVegaLiteToVerticalStackedBarChartProps: function() {
|
|
60
|
+
return transformVegaLiteToVerticalStackedBarChartProps;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const _VegaLiteColorAdapter = require("./VegaLiteColorAdapter");
|
|
64
|
+
const _VegaLiteExpressionEvaluator = require("./VegaLiteExpressionEvaluator");
|
|
65
|
+
const _d3array = require("d3-array");
|
|
66
|
+
const _d3format = require("d3-format");
|
|
67
|
+
const _chartutilities = require("@fluentui/chart-utilities");
|
|
68
|
+
/**
|
|
69
|
+
* Vega-Lite to Fluent Charts adapter for line/point charts.
|
|
70
|
+
*
|
|
71
|
+
* Transforms Vega-Lite JSON specifications into Fluent LineChart props.
|
|
72
|
+
* Supports basic line charts with temporal/quantitative axes and color-encoded series.
|
|
73
|
+
*
|
|
74
|
+
* TODO: Future enhancements
|
|
75
|
+
* - Multi-view layouts (facet, concat, repeat)
|
|
76
|
+
* - Selection interactions
|
|
77
|
+
* - Remote data loading (url)
|
|
78
|
+
* - Transform pipeline (filter, aggregate, calculate)
|
|
79
|
+
* - Conditional encodings
|
|
80
|
+
* - Additional mark types (area, bar, etc.)
|
|
81
|
+
* - Tooltip customization
|
|
82
|
+
*/ /**
|
|
83
|
+
* Default configuration values for VegaLite charts
|
|
84
|
+
*/ const DEFAULT_CHART_HEIGHT = 350;
|
|
85
|
+
const DEFAULT_MAX_BAR_WIDTH = 50;
|
|
86
|
+
const DEFAULT_TRUNCATE_CHARS = 20;
|
|
87
|
+
/**
|
|
88
|
+
* Determines if a spec is a layered specification
|
|
89
|
+
*/ function isLayerSpec(spec) {
|
|
90
|
+
return Array.isArray(spec.layer) && spec.layer.length > 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Determines if a spec is a single unit specification
|
|
94
|
+
*/ function isUnitSpec(spec) {
|
|
95
|
+
return spec.mark !== undefined && spec.encoding !== undefined;
|
|
96
|
+
}
|
|
97
|
+
function getMarkType(mark) {
|
|
98
|
+
if (!mark) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return typeof mark === 'string' ? mark : mark.type;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolves the color for a legend label using the priority chain:
|
|
105
|
+
* 1. Explicit color value from encoding
|
|
106
|
+
* 2. Mark-level color
|
|
107
|
+
* 3. Cached color from the shared color map
|
|
108
|
+
* 4. New color via local index for deterministic per-chart assignment
|
|
109
|
+
*/ function resolveColor(legend, index, colorValue, markColor, colorMap, colorScheme, colorRange, isDarkTheme) {
|
|
110
|
+
var _colorMap_current, _colorMap_current1;
|
|
111
|
+
if (colorValue) {
|
|
112
|
+
return colorValue;
|
|
113
|
+
}
|
|
114
|
+
if (markColor) {
|
|
115
|
+
return markColor;
|
|
116
|
+
}
|
|
117
|
+
// Check colorMap cache first for cross-chart consistency
|
|
118
|
+
if ((_colorMap_current = colorMap.current) === null || _colorMap_current === void 0 ? void 0 : _colorMap_current.has(legend)) {
|
|
119
|
+
return colorMap.current.get(legend);
|
|
120
|
+
}
|
|
121
|
+
// Use local index (not colorMap.size) for deterministic per-chart color assignment
|
|
122
|
+
const color = (0, _VegaLiteColorAdapter.getVegaColor)(index, colorScheme, colorRange, isDarkTheme);
|
|
123
|
+
(_colorMap_current1 = colorMap.current) === null || _colorMap_current1 === void 0 ? void 0 : _colorMap_current1.set(legend, color);
|
|
124
|
+
return color;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Extracts inline data values from a Vega-Lite data specification
|
|
128
|
+
* TODO: Add support for URL-based data loading
|
|
129
|
+
* TODO: Add support for named dataset resolution
|
|
130
|
+
* TODO: Add support for data format parsing (csv, tsv)
|
|
131
|
+
*/ function extractDataValues(data) {
|
|
132
|
+
if (!data) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
if (data.values && Array.isArray(data.values)) {
|
|
136
|
+
return data.values;
|
|
137
|
+
}
|
|
138
|
+
// TODO: Handle data.url - load remote data
|
|
139
|
+
if (data.url) {
|
|
140
|
+
// Remote data URLs are not yet supported
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
// TODO: Handle data.name - resolve named datasets
|
|
144
|
+
if (data.name) {
|
|
145
|
+
// Named datasets are not yet supported
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Applies a fold transform to convert wide-format data to long-format
|
|
152
|
+
* The fold transform unpivots specified fields into key-value pairs
|
|
153
|
+
*
|
|
154
|
+
* @param data - Array of data records in wide format
|
|
155
|
+
* @param foldFields - Array of field names to fold
|
|
156
|
+
* @param asFields - [keyName, valueName] for the new columns (defaults to ['key', 'value'])
|
|
157
|
+
* @returns Array of data records in long format
|
|
158
|
+
*/ function applyFoldTransform(data, foldFields, asFields = [
|
|
159
|
+
'key',
|
|
160
|
+
'value'
|
|
161
|
+
]) {
|
|
162
|
+
const [keyField, valueField] = asFields;
|
|
163
|
+
const result = [];
|
|
164
|
+
for (const row of data){
|
|
165
|
+
// Create a base row without the fields being folded
|
|
166
|
+
const baseRow = {};
|
|
167
|
+
for (const [key, value] of Object.entries(row)){
|
|
168
|
+
if (!foldFields.includes(key)) {
|
|
169
|
+
baseRow[key] = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Create a new row for each folded field
|
|
173
|
+
for (const field of foldFields){
|
|
174
|
+
if (field in row) {
|
|
175
|
+
result.push({
|
|
176
|
+
...baseRow,
|
|
177
|
+
[keyField]: field,
|
|
178
|
+
[valueField]: row[field]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Applies transforms from a Vega-Lite spec to data
|
|
187
|
+
* Currently supports: fold transform
|
|
188
|
+
*
|
|
189
|
+
* @param data - Array of data records
|
|
190
|
+
* @param transforms - Array of Vega-Lite transform specifications
|
|
191
|
+
* @returns Transformed data array
|
|
192
|
+
*/ function applyTransforms(data, transforms) {
|
|
193
|
+
if (!transforms || transforms.length === 0) {
|
|
194
|
+
return data;
|
|
195
|
+
}
|
|
196
|
+
let result = data;
|
|
197
|
+
for (const transform of transforms){
|
|
198
|
+
// Handle fold transform
|
|
199
|
+
if ('fold' in transform && Array.isArray(transform.fold)) {
|
|
200
|
+
const foldFields = transform.fold;
|
|
201
|
+
const asFields = transform.as || [
|
|
202
|
+
'key',
|
|
203
|
+
'value'
|
|
204
|
+
];
|
|
205
|
+
result = applyFoldTransform(result, foldFields, asFields);
|
|
206
|
+
}
|
|
207
|
+
// Handle filter transform
|
|
208
|
+
if ('filter' in transform) {
|
|
209
|
+
const filterExpr = transform.filter;
|
|
210
|
+
if (typeof filterExpr === 'string') {
|
|
211
|
+
result = result.filter((row)=>{
|
|
212
|
+
try {
|
|
213
|
+
return (0, _VegaLiteExpressionEvaluator.safeEvaluateExpression)(filterExpr, row);
|
|
214
|
+
} catch {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Handle calculate transform
|
|
221
|
+
if ('calculate' in transform && 'as' in transform) {
|
|
222
|
+
const expr = transform.calculate;
|
|
223
|
+
const asField = transform.as;
|
|
224
|
+
result = result.map((row)=>{
|
|
225
|
+
try {
|
|
226
|
+
const value = (0, _VegaLiteExpressionEvaluator.safeEvaluateExpression)(expr, row);
|
|
227
|
+
return {
|
|
228
|
+
...row,
|
|
229
|
+
[asField]: value
|
|
230
|
+
};
|
|
231
|
+
} catch {
|
|
232
|
+
return row;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// Handle aggregate transform
|
|
237
|
+
if ('aggregate' in transform && Array.isArray(transform.aggregate)) {
|
|
238
|
+
const aggSpecs = transform.aggregate;
|
|
239
|
+
const groupby = transform.groupby || [];
|
|
240
|
+
const groups = new Map();
|
|
241
|
+
result.forEach((row)=>{
|
|
242
|
+
const key = groupby.map((g)=>String(row[g])).join('|');
|
|
243
|
+
if (!groups.has(key)) {
|
|
244
|
+
groups.set(key, []);
|
|
245
|
+
}
|
|
246
|
+
groups.get(key).push(row);
|
|
247
|
+
});
|
|
248
|
+
result = Array.from(groups.entries()).map(([key, rows])=>{
|
|
249
|
+
const baseRow = {};
|
|
250
|
+
groupby.forEach((g, i)=>{
|
|
251
|
+
baseRow[g] = rows[0][g];
|
|
252
|
+
});
|
|
253
|
+
aggSpecs.forEach((spec)=>{
|
|
254
|
+
const values = spec.field ? rows.map((r)=>Number(r[spec.field])).filter((v)=>!isNaN(v)) : [];
|
|
255
|
+
switch(spec.op){
|
|
256
|
+
case 'count':
|
|
257
|
+
baseRow[spec.as] = rows.length;
|
|
258
|
+
break;
|
|
259
|
+
case 'sum':
|
|
260
|
+
baseRow[spec.as] = (0, _d3array.sum)(values);
|
|
261
|
+
break;
|
|
262
|
+
case 'mean':
|
|
263
|
+
case 'average':
|
|
264
|
+
var _d3Mean;
|
|
265
|
+
baseRow[spec.as] = (_d3Mean = (0, _d3array.mean)(values)) !== null && _d3Mean !== void 0 ? _d3Mean : 0;
|
|
266
|
+
break;
|
|
267
|
+
case 'min':
|
|
268
|
+
var _d3Min;
|
|
269
|
+
baseRow[spec.as] = (_d3Min = (0, _d3array.min)(values)) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
270
|
+
break;
|
|
271
|
+
case 'max':
|
|
272
|
+
var _d3Max;
|
|
273
|
+
baseRow[spec.as] = (_d3Max = (0, _d3array.max)(values)) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
baseRow[spec.as] = rows.length;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return baseRow;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// Handle window transform
|
|
283
|
+
if ('window' in transform && Array.isArray(transform.window)) {
|
|
284
|
+
const windowOps = transform.window;
|
|
285
|
+
const sortFields = transform.sort || [];
|
|
286
|
+
const groupby = transform.groupby || [];
|
|
287
|
+
// Group data
|
|
288
|
+
const groups = new Map();
|
|
289
|
+
result.forEach((row)=>{
|
|
290
|
+
const key = groupby.length > 0 ? groupby.map((g)=>String(row[g])).join('|') : '__all__';
|
|
291
|
+
if (!groups.has(key)) {
|
|
292
|
+
groups.set(key, []);
|
|
293
|
+
}
|
|
294
|
+
groups.get(key).push(row);
|
|
295
|
+
});
|
|
296
|
+
const newResult = [];
|
|
297
|
+
groups.forEach((rows)=>{
|
|
298
|
+
// Sort within group
|
|
299
|
+
if (sortFields.length > 0) {
|
|
300
|
+
rows.sort((a, b)=>{
|
|
301
|
+
for (const sf of sortFields){
|
|
302
|
+
const va = Number(a[sf.field]) || 0;
|
|
303
|
+
const vb = Number(b[sf.field]) || 0;
|
|
304
|
+
const cmp = sf.order === 'descending' ? vb - va : va - vb;
|
|
305
|
+
if (cmp !== 0) {
|
|
306
|
+
return cmp;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return 0;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
let runningSum = 0;
|
|
313
|
+
rows.forEach((row, idx)=>{
|
|
314
|
+
const newRow = {
|
|
315
|
+
...row
|
|
316
|
+
};
|
|
317
|
+
windowOps.forEach((op)=>{
|
|
318
|
+
switch(op.op){
|
|
319
|
+
case 'sum':
|
|
320
|
+
runningSum += Number(row[op.field]) || 0;
|
|
321
|
+
newRow[op.as] = runningSum;
|
|
322
|
+
break;
|
|
323
|
+
case 'rank':
|
|
324
|
+
newRow[op.as] = idx + 1;
|
|
325
|
+
break;
|
|
326
|
+
case 'row_number':
|
|
327
|
+
newRow[op.as] = idx + 1;
|
|
328
|
+
break;
|
|
329
|
+
case 'count':
|
|
330
|
+
newRow[op.as] = idx + 1;
|
|
331
|
+
break;
|
|
332
|
+
default:
|
|
333
|
+
newRow[op.as] = idx + 1;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
newResult.push(newRow);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
result = newResult;
|
|
340
|
+
}
|
|
341
|
+
// Handle joinaggregate transform
|
|
342
|
+
if ('joinaggregate' in transform && Array.isArray(transform.joinaggregate)) {
|
|
343
|
+
const aggSpecs = transform.joinaggregate;
|
|
344
|
+
const groupby = transform.groupby || [];
|
|
345
|
+
// Compute aggregates
|
|
346
|
+
const groups = new Map();
|
|
347
|
+
result.forEach((row)=>{
|
|
348
|
+
const key = groupby.length > 0 ? groupby.map((g)=>String(row[g])).join('|') : '__all__';
|
|
349
|
+
if (!groups.has(key)) {
|
|
350
|
+
groups.set(key, []);
|
|
351
|
+
}
|
|
352
|
+
groups.get(key).push(row);
|
|
353
|
+
});
|
|
354
|
+
const aggResults = new Map();
|
|
355
|
+
groups.forEach((rows, key)=>{
|
|
356
|
+
const aggs = {};
|
|
357
|
+
aggSpecs.forEach((spec)=>{
|
|
358
|
+
const values = spec.field ? rows.map((r)=>Number(r[spec.field])).filter((v)=>!isNaN(v)) : [];
|
|
359
|
+
switch(spec.op){
|
|
360
|
+
case 'mean':
|
|
361
|
+
case 'average':
|
|
362
|
+
var _d3Mean;
|
|
363
|
+
aggs[spec.as] = (_d3Mean = (0, _d3array.mean)(values)) !== null && _d3Mean !== void 0 ? _d3Mean : 0;
|
|
364
|
+
break;
|
|
365
|
+
case 'sum':
|
|
366
|
+
aggs[spec.as] = (0, _d3array.sum)(values);
|
|
367
|
+
break;
|
|
368
|
+
case 'count':
|
|
369
|
+
aggs[spec.as] = rows.length;
|
|
370
|
+
break;
|
|
371
|
+
case 'min':
|
|
372
|
+
var _d3Min;
|
|
373
|
+
aggs[spec.as] = (_d3Min = (0, _d3array.min)(values)) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
374
|
+
break;
|
|
375
|
+
case 'max':
|
|
376
|
+
var _d3Max;
|
|
377
|
+
aggs[spec.as] = (_d3Max = (0, _d3array.max)(values)) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
378
|
+
break;
|
|
379
|
+
default:
|
|
380
|
+
aggs[spec.as] = rows.length;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
aggResults.set(key, aggs);
|
|
384
|
+
});
|
|
385
|
+
// Join back: add aggregate values to each row
|
|
386
|
+
result = result.map((row)=>{
|
|
387
|
+
const key = groupby.length > 0 ? groupby.map((g)=>String(row[g])).join('|') : '__all__';
|
|
388
|
+
return {
|
|
389
|
+
...row,
|
|
390
|
+
...aggResults.get(key) || {}
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// Handle regression transform (simple linear regression)
|
|
395
|
+
if ('regression' in transform && 'on' in transform) {
|
|
396
|
+
const yField = transform.regression;
|
|
397
|
+
const xField = transform.on;
|
|
398
|
+
const points = result.map((r)=>({
|
|
399
|
+
x: Number(r[xField]),
|
|
400
|
+
y: Number(r[yField])
|
|
401
|
+
})).filter((p)=>!isNaN(p.x) && !isNaN(p.y));
|
|
402
|
+
if (points.length >= 2) {
|
|
403
|
+
const n = points.length;
|
|
404
|
+
const sumX = (0, _d3array.sum)(points.map((p)=>p.x));
|
|
405
|
+
const sumY = (0, _d3array.sum)(points.map((p)=>p.y));
|
|
406
|
+
const sumXY = (0, _d3array.sum)(points.map((p)=>p.x * p.y));
|
|
407
|
+
const sumX2 = (0, _d3array.sum)(points.map((p)=>p.x * p.x));
|
|
408
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
409
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
410
|
+
var _d3Min;
|
|
411
|
+
const xMin = (_d3Min = (0, _d3array.min)(points.map((p)=>p.x))) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
412
|
+
var _d3Max;
|
|
413
|
+
const xMax = (_d3Max = (0, _d3array.max)(points.map((p)=>p.x))) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
414
|
+
result = [
|
|
415
|
+
{
|
|
416
|
+
[xField]: xMin,
|
|
417
|
+
[yField]: slope * xMin + intercept
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
[xField]: xMax,
|
|
421
|
+
[yField]: slope * xMax + intercept
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Handle loess transform (simplified: moving average approximation)
|
|
427
|
+
if ('loess' in transform && 'on' in transform) {
|
|
428
|
+
const yField = transform.loess;
|
|
429
|
+
const xField = transform.on;
|
|
430
|
+
const sorted = [
|
|
431
|
+
...result
|
|
432
|
+
].filter((r)=>!isNaN(Number(r[xField])) && !isNaN(Number(r[yField]))).sort((a, b)=>Number(a[xField]) - Number(b[xField]));
|
|
433
|
+
const windowSize = Math.max(3, Math.floor(sorted.length / 4));
|
|
434
|
+
result = sorted.map((row, i)=>{
|
|
435
|
+
const start = Math.max(0, i - Math.floor(windowSize / 2));
|
|
436
|
+
const end = Math.min(sorted.length, start + windowSize);
|
|
437
|
+
const windowSlice = sorted.slice(start, end);
|
|
438
|
+
var _d3Mean;
|
|
439
|
+
const avgY = (_d3Mean = (0, _d3array.mean)(windowSlice.map((r)=>Number(r[yField])))) !== null && _d3Mean !== void 0 ? _d3Mean : Number(row[yField]);
|
|
440
|
+
return {
|
|
441
|
+
[xField]: row[xField],
|
|
442
|
+
[yField]: avgY
|
|
443
|
+
};
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// Handle density transform (simplified: histogram-based density estimation)
|
|
447
|
+
if ('density' in transform) {
|
|
448
|
+
const field = transform.density;
|
|
449
|
+
const groupby = transform.groupby || [];
|
|
450
|
+
const groups = new Map();
|
|
451
|
+
result.forEach((row)=>{
|
|
452
|
+
const key = groupby.length > 0 ? groupby.map((g)=>String(row[g])).join('|') : '__all__';
|
|
453
|
+
if (!groups.has(key)) {
|
|
454
|
+
groups.set(key, []);
|
|
455
|
+
}
|
|
456
|
+
groups.get(key).push(Number(row[field]));
|
|
457
|
+
});
|
|
458
|
+
const densityResult = [];
|
|
459
|
+
groups.forEach((values, key)=>{
|
|
460
|
+
var _d3Min;
|
|
461
|
+
const min = (_d3Min = (0, _d3array.min)(values)) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
462
|
+
var _d3Max;
|
|
463
|
+
const max = (_d3Max = (0, _d3array.max)(values)) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
464
|
+
const range = max - min || 1;
|
|
465
|
+
const bins = 20;
|
|
466
|
+
const bandwidth = range / bins;
|
|
467
|
+
const groupFields = {};
|
|
468
|
+
if (groupby.length > 0) {
|
|
469
|
+
const sampleRow = result.find((r)=>groupby.map((g)=>String(r[g])).join('|') === key);
|
|
470
|
+
groupby.forEach((g)=>{
|
|
471
|
+
groupFields[g] = sampleRow === null || sampleRow === void 0 ? void 0 : sampleRow[g];
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
for(let i = 0; i <= bins; i++){
|
|
475
|
+
const x = min + i / bins * range;
|
|
476
|
+
const count = values.filter((v)=>Math.abs(v - x) < bandwidth).length;
|
|
477
|
+
const density = count / (values.length * bandwidth);
|
|
478
|
+
densityResult.push({
|
|
479
|
+
value: x,
|
|
480
|
+
density,
|
|
481
|
+
...groupFields
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
result = densityResult;
|
|
486
|
+
}
|
|
487
|
+
// Handle quantile transform
|
|
488
|
+
if ('quantile' in transform) {
|
|
489
|
+
const field = transform.quantile;
|
|
490
|
+
const probs = transform.probs || [
|
|
491
|
+
0.25,
|
|
492
|
+
0.5,
|
|
493
|
+
0.75
|
|
494
|
+
];
|
|
495
|
+
const values = result.map((r)=>Number(r[field])).filter((v)=>!isNaN(v)).sort((a, b)=>a - b);
|
|
496
|
+
if (values.length > 0) {
|
|
497
|
+
result = probs.map((p)=>{
|
|
498
|
+
const idx = Math.min(Math.floor(p * values.length), values.length - 1);
|
|
499
|
+
return {
|
|
500
|
+
prob: String(p),
|
|
501
|
+
value: values[idx]
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Handle impute transform (fill missing values)
|
|
507
|
+
if ('impute' in transform && 'key' in transform) {
|
|
508
|
+
const field = transform.impute;
|
|
509
|
+
const keyField = transform.key;
|
|
510
|
+
const method = transform.method || 'value';
|
|
511
|
+
var _transform_value;
|
|
512
|
+
const fillValue = (_transform_value = transform.value) !== null && _transform_value !== void 0 ? _transform_value : 0;
|
|
513
|
+
const existingKeys = new Set(result.map((r)=>r[keyField]));
|
|
514
|
+
const allKeyValues = result.map((r)=>Number(r[keyField])).filter((v)=>!isNaN(v));
|
|
515
|
+
if (allKeyValues.length > 0) {
|
|
516
|
+
var _d3Min1;
|
|
517
|
+
const minKey = (_d3Min1 = (0, _d3array.min)(allKeyValues)) !== null && _d3Min1 !== void 0 ? _d3Min1 : 0;
|
|
518
|
+
var _d3Max1;
|
|
519
|
+
const maxKey = (_d3Max1 = (0, _d3array.max)(allKeyValues)) !== null && _d3Max1 !== void 0 ? _d3Max1 : 0;
|
|
520
|
+
for(let k = minKey; k <= maxKey; k++){
|
|
521
|
+
if (!existingKeys.has(k)) {
|
|
522
|
+
const imputed = {
|
|
523
|
+
[keyField]: k
|
|
524
|
+
};
|
|
525
|
+
imputed[field] = method === 'value' ? fillValue : 0;
|
|
526
|
+
result.push(imputed);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
result.sort((a, b)=>Number(a[keyField]) - Number(b[keyField]));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Handle lookup transform (join with secondary dataset)
|
|
533
|
+
if ('lookup' in transform && 'from' in transform) {
|
|
534
|
+
var _fromSpec_data;
|
|
535
|
+
const lookupField = transform.lookup;
|
|
536
|
+
const fromSpec = transform.from;
|
|
537
|
+
if (((_fromSpec_data = fromSpec.data) === null || _fromSpec_data === void 0 ? void 0 : _fromSpec_data.values) && fromSpec.key && fromSpec.fields) {
|
|
538
|
+
const lookupMap = new Map();
|
|
539
|
+
fromSpec.data.values.forEach((row)=>{
|
|
540
|
+
lookupMap.set(String(row[fromSpec.key]), row);
|
|
541
|
+
});
|
|
542
|
+
result = result.map((row)=>{
|
|
543
|
+
const lookupRow = lookupMap.get(String(row[lookupField]));
|
|
544
|
+
if (lookupRow) {
|
|
545
|
+
const extra = {};
|
|
546
|
+
fromSpec.fields.forEach((f)=>{
|
|
547
|
+
extra[f] = lookupRow[f];
|
|
548
|
+
});
|
|
549
|
+
return {
|
|
550
|
+
...row,
|
|
551
|
+
...extra
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return row;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Normalizes a Vega-Lite spec into an array of unit specs with resolved data and encoding
|
|
563
|
+
* Handles both single-view and layered specifications
|
|
564
|
+
*/ function normalizeSpec(spec) {
|
|
565
|
+
if (isLayerSpec(spec)) {
|
|
566
|
+
// Layered spec: merge shared data and encoding with each layer
|
|
567
|
+
const sharedData = spec.data;
|
|
568
|
+
const sharedEncoding = spec.encoding;
|
|
569
|
+
return spec.layer.map((layer)=>({
|
|
570
|
+
...layer,
|
|
571
|
+
data: layer.data || sharedData,
|
|
572
|
+
encoding: {
|
|
573
|
+
...sharedEncoding,
|
|
574
|
+
...layer.encoding
|
|
575
|
+
}
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
if (isUnitSpec(spec)) {
|
|
579
|
+
// Single unit spec
|
|
580
|
+
return [
|
|
581
|
+
{
|
|
582
|
+
mark: spec.mark,
|
|
583
|
+
encoding: spec.encoding,
|
|
584
|
+
data: spec.data
|
|
585
|
+
}
|
|
586
|
+
];
|
|
587
|
+
}
|
|
588
|
+
// Unsupported spec structure
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Parses a value to a Date if it's temporal, otherwise returns as number or string
|
|
593
|
+
*/ function parseValue(value, isTemporalType) {
|
|
594
|
+
if (value === null || value === undefined) {
|
|
595
|
+
return '';
|
|
596
|
+
}
|
|
597
|
+
if (isTemporalType) {
|
|
598
|
+
// Try parsing as date
|
|
599
|
+
const dateValue = new Date(value);
|
|
600
|
+
if (!isNaN(dateValue.getTime())) {
|
|
601
|
+
return dateValue;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (typeof value === 'number') {
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
return String(value);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Maps Vega-Lite interpolate to Fluent curve options
|
|
611
|
+
* Note: Only maps to curve types supported by LineChartLineOptions
|
|
612
|
+
*/ function mapInterpolateToCurve(interpolate) {
|
|
613
|
+
if (!interpolate) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
switch(interpolate){
|
|
617
|
+
case 'linear':
|
|
618
|
+
case 'linear-closed':
|
|
619
|
+
return 'linear';
|
|
620
|
+
case 'step':
|
|
621
|
+
return 'step';
|
|
622
|
+
case 'step-before':
|
|
623
|
+
return 'stepBefore';
|
|
624
|
+
case 'step-after':
|
|
625
|
+
return 'stepAfter';
|
|
626
|
+
case 'natural':
|
|
627
|
+
return 'natural';
|
|
628
|
+
case 'monotone':
|
|
629
|
+
return 'linear';
|
|
630
|
+
// Note: basis, cardinal, catmull-rom are not supported by LineChartLineOptions
|
|
631
|
+
default:
|
|
632
|
+
return 'linear';
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Extracts mark properties from VegaLiteMarkDef
|
|
637
|
+
*/ function getMarkProperties(mark) {
|
|
638
|
+
if (typeof mark === 'string') {
|
|
639
|
+
return {};
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
color: mark.color,
|
|
643
|
+
interpolate: mark.interpolate,
|
|
644
|
+
strokeWidth: mark.strokeWidth,
|
|
645
|
+
strokeDash: mark.strokeDash,
|
|
646
|
+
point: mark.point
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Extracts annotations from Vega-Lite layers with text or rule marks
|
|
651
|
+
* Text marks become text annotations, rule marks become reference lines
|
|
652
|
+
*/ function extractAnnotations(spec) {
|
|
653
|
+
const annotations = [];
|
|
654
|
+
if (!spec.layer || !Array.isArray(spec.layer)) {
|
|
655
|
+
return annotations;
|
|
656
|
+
}
|
|
657
|
+
spec.layer.forEach((layer, index)=>{
|
|
658
|
+
const mark = getMarkType(layer.mark);
|
|
659
|
+
const encoding = layer.encoding || {};
|
|
660
|
+
// Text marks become annotations
|
|
661
|
+
if (mark === 'text' && encoding.x && encoding.y) {
|
|
662
|
+
var _encoding_text, _encoding_text1, _encoding_text2;
|
|
663
|
+
const textValue = ((_encoding_text = encoding.text) === null || _encoding_text === void 0 ? void 0 : _encoding_text.datum) || ((_encoding_text1 = encoding.text) === null || _encoding_text1 === void 0 ? void 0 : _encoding_text1.value) || ((_encoding_text2 = encoding.text) === null || _encoding_text2 === void 0 ? void 0 : _encoding_text2.field) || '';
|
|
664
|
+
const xValue = encoding.x.datum || encoding.x.value || encoding.x.field;
|
|
665
|
+
const yValue = encoding.y.datum || encoding.y.value || encoding.y.field;
|
|
666
|
+
if (textValue && (xValue !== undefined || encoding.x.datum !== undefined) && (yValue !== undefined || encoding.y.datum !== undefined)) {
|
|
667
|
+
annotations.push({
|
|
668
|
+
id: `text-annotation-${index}`,
|
|
669
|
+
text: String(textValue),
|
|
670
|
+
coordinates: {
|
|
671
|
+
type: 'data',
|
|
672
|
+
x: encoding.x.datum || xValue || 0,
|
|
673
|
+
y: encoding.y.datum || yValue || 0
|
|
674
|
+
},
|
|
675
|
+
style: {
|
|
676
|
+
textColor: typeof layer.mark === 'object' ? layer.mark.color : undefined
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// Rule marks can become reference lines (horizontal or vertical)
|
|
682
|
+
if (mark === 'rule') {
|
|
683
|
+
const markColor = typeof layer.mark === 'object' ? layer.mark.color : '#000';
|
|
684
|
+
const markStrokeWidth = typeof layer.mark === 'object' ? layer.mark.strokeWidth || 1 : 1;
|
|
685
|
+
const markStrokeDash = typeof layer.mark === 'object' ? layer.mark.strokeDash : undefined;
|
|
686
|
+
// Horizontal rule (y value constant)
|
|
687
|
+
if (encoding.y && (encoding.y.value !== undefined || encoding.y.datum !== undefined)) {
|
|
688
|
+
var _spec_layer, _companionText_encoding_text, _companionText_encoding, _companionText_encoding_text1, _companionText_encoding1;
|
|
689
|
+
var _encoding_y_value;
|
|
690
|
+
const yValue = (_encoding_y_value = encoding.y.value) !== null && _encoding_y_value !== void 0 ? _encoding_y_value : encoding.y.datum;
|
|
691
|
+
// Look for a companion text annotation at the same y-value
|
|
692
|
+
const companionText = (_spec_layer = spec.layer) === null || _spec_layer === void 0 ? void 0 : _spec_layer.find((l, i)=>{
|
|
693
|
+
var _l_encoding;
|
|
694
|
+
if (i === index) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
const m = getMarkType(l.mark);
|
|
698
|
+
var _l_encoding_y_datum;
|
|
699
|
+
return m === 'text' && ((_l_encoding = l.encoding) === null || _l_encoding === void 0 ? void 0 : _l_encoding.y) && ((_l_encoding_y_datum = l.encoding.y.datum) !== null && _l_encoding_y_datum !== void 0 ? _l_encoding_y_datum : l.encoding.y.value) === yValue;
|
|
700
|
+
});
|
|
701
|
+
const ruleText = companionText ? String(((_companionText_encoding = companionText.encoding) === null || _companionText_encoding === void 0 ? void 0 : (_companionText_encoding_text = _companionText_encoding.text) === null || _companionText_encoding_text === void 0 ? void 0 : _companionText_encoding_text.datum) || ((_companionText_encoding1 = companionText.encoding) === null || _companionText_encoding1 === void 0 ? void 0 : (_companionText_encoding_text1 = _companionText_encoding1.text) === null || _companionText_encoding_text1 === void 0 ? void 0 : _companionText_encoding_text1.value) || yValue) : String(yValue);
|
|
702
|
+
annotations.push({
|
|
703
|
+
id: `rule-h-${index}`,
|
|
704
|
+
text: ruleText,
|
|
705
|
+
coordinates: {
|
|
706
|
+
type: 'data',
|
|
707
|
+
x: 0,
|
|
708
|
+
y: yValue
|
|
709
|
+
},
|
|
710
|
+
style: {
|
|
711
|
+
textColor: markColor,
|
|
712
|
+
borderColor: markColor,
|
|
713
|
+
borderWidth: markStrokeWidth,
|
|
714
|
+
...markStrokeDash && Array.isArray(markStrokeDash) ? {
|
|
715
|
+
borderRadius: 0
|
|
716
|
+
} // Indicate dashed style
|
|
717
|
+
: {}
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
} else if (encoding.x && (encoding.x.value !== undefined || encoding.x.datum !== undefined)) {
|
|
721
|
+
var _encoding_x_value;
|
|
722
|
+
const xValue = (_encoding_x_value = encoding.x.value) !== null && _encoding_x_value !== void 0 ? _encoding_x_value : encoding.x.datum;
|
|
723
|
+
annotations.push({
|
|
724
|
+
id: `rule-v-${index}`,
|
|
725
|
+
text: String(xValue),
|
|
726
|
+
coordinates: {
|
|
727
|
+
type: 'data',
|
|
728
|
+
x: xValue,
|
|
729
|
+
y: 0
|
|
730
|
+
},
|
|
731
|
+
style: {
|
|
732
|
+
textColor: markColor,
|
|
733
|
+
borderColor: markColor,
|
|
734
|
+
borderWidth: markStrokeWidth
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
return annotations;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Extracts color fill bars (background regions) from rect marks with x/x2 or y/y2 encodings
|
|
744
|
+
*/ function extractColorFillBars(spec, colorMap, isDarkTheme) {
|
|
745
|
+
const colorFillBars = [];
|
|
746
|
+
if (!spec.layer || !Array.isArray(spec.layer)) {
|
|
747
|
+
return colorFillBars;
|
|
748
|
+
}
|
|
749
|
+
// Detect if x-axis is temporal by checking the primary data layer (non-rect layer)
|
|
750
|
+
const isXTemporal = spec.layer.some((layer)=>{
|
|
751
|
+
var _layer_encoding_x, _layer_encoding;
|
|
752
|
+
const layerMark = getMarkType(layer.mark);
|
|
753
|
+
// Skip rect layers, look at line/point/area layers
|
|
754
|
+
if (layerMark === 'rect') {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
return ((_layer_encoding = layer.encoding) === null || _layer_encoding === void 0 ? void 0 : (_layer_encoding_x = _layer_encoding.x) === null || _layer_encoding_x === void 0 ? void 0 : _layer_encoding_x.type) === 'temporal';
|
|
758
|
+
});
|
|
759
|
+
spec.layer.forEach((layer, index)=>{
|
|
760
|
+
const mark = getMarkType(layer.mark);
|
|
761
|
+
const encoding = layer.encoding || {};
|
|
762
|
+
// Rect marks with x and x2 become color fill bars (vertical regions)
|
|
763
|
+
if (mark === 'rect' && encoding.x && encoding.x2) {
|
|
764
|
+
const legend = `region-${index}`;
|
|
765
|
+
const color = typeof layer.mark === 'object' && layer.mark.color ? layer.mark.color : (0, _VegaLiteColorAdapter.getVegaColorFromMap)(legend, colorMap, undefined, undefined, isDarkTheme);
|
|
766
|
+
// Extract start and end x values
|
|
767
|
+
const rawStartX = encoding.x.datum || encoding.x.value;
|
|
768
|
+
const rawEndX = encoding.x2.datum || encoding.x2.value;
|
|
769
|
+
if (rawStartX !== undefined && rawEndX !== undefined) {
|
|
770
|
+
// Convert to Date if x-axis is temporal and values are date-like strings
|
|
771
|
+
let startX = rawStartX;
|
|
772
|
+
let endX = rawEndX;
|
|
773
|
+
if (isXTemporal) {
|
|
774
|
+
const parsedStart = new Date(rawStartX);
|
|
775
|
+
const parsedEnd = new Date(rawEndX);
|
|
776
|
+
if (!isNaN(parsedStart.getTime())) {
|
|
777
|
+
startX = parsedStart;
|
|
778
|
+
}
|
|
779
|
+
if (!isNaN(parsedEnd.getTime())) {
|
|
780
|
+
endX = parsedEnd;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
colorFillBars.push({
|
|
784
|
+
legend,
|
|
785
|
+
color,
|
|
786
|
+
data: [
|
|
787
|
+
{
|
|
788
|
+
startX,
|
|
789
|
+
endX
|
|
790
|
+
}
|
|
791
|
+
],
|
|
792
|
+
applyPattern: false
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
return colorFillBars;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Extracts tick configuration from axis properties
|
|
801
|
+
*/ function extractTickConfig(spec) {
|
|
802
|
+
var _encoding_x, _encoding_y;
|
|
803
|
+
const config = {};
|
|
804
|
+
const encoding = spec.encoding || {};
|
|
805
|
+
if ((_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.axis) {
|
|
806
|
+
if (encoding.x.axis.values) {
|
|
807
|
+
config.tickValues = encoding.x.axis.values;
|
|
808
|
+
}
|
|
809
|
+
if (encoding.x.axis.tickCount) {
|
|
810
|
+
config.xAxisTickCount = encoding.x.axis.tickCount;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if ((_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.axis) {
|
|
814
|
+
if (encoding.y.axis.tickCount) {
|
|
815
|
+
config.yAxisTickCount = encoding.y.axis.tickCount;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return config;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Validates that data array is not empty and contains valid values for the specified field
|
|
822
|
+
* @param data - Array of data objects
|
|
823
|
+
* @param field - Field name to validate
|
|
824
|
+
* @param chartType - Chart type for error message context
|
|
825
|
+
* @throws Error if data is empty or field has no valid values
|
|
826
|
+
*/ function validateDataArray(data, field, chartType) {
|
|
827
|
+
if (!data || data.length === 0) {
|
|
828
|
+
throw new Error(`VegaLiteSchemaAdapter: Empty data array for ${chartType}`);
|
|
829
|
+
}
|
|
830
|
+
const hasValidValues = data.some((row)=>row[field] !== undefined && row[field] !== null);
|
|
831
|
+
if (!hasValidValues) {
|
|
832
|
+
throw new Error(`VegaLiteSchemaAdapter: No valid values found for field '${field}' in ${chartType}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Validates that nested arrays are not present in the data field (unsupported)
|
|
837
|
+
* @param data - Array of data objects
|
|
838
|
+
* @param field - Field name to validate
|
|
839
|
+
* @throws Error if nested arrays are detected
|
|
840
|
+
*/ function validateNoNestedArrays(data, field) {
|
|
841
|
+
const hasNestedArrays = data.some((row)=>Array.isArray(row[field]));
|
|
842
|
+
if (hasNestedArrays) {
|
|
843
|
+
throw new Error(`VegaLiteSchemaAdapter: Nested arrays not supported for field '${field}'. ` + `Use flat data structures only.`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Validates data type compatibility with encoding type
|
|
848
|
+
* @param data - Array of data objects
|
|
849
|
+
* @param field - Field name to validate
|
|
850
|
+
* @param expectedType - Expected Vega-Lite type (quantitative, temporal, nominal, ordinal)
|
|
851
|
+
* @throws Error if data type doesn't match encoding type
|
|
852
|
+
*/ /**
|
|
853
|
+
* Validates and potentially auto-corrects encoding types based on actual data
|
|
854
|
+
* Returns the corrected type if auto-correction was applied
|
|
855
|
+
*
|
|
856
|
+
* @param data - Array of data values
|
|
857
|
+
* @param field - Field name to validate
|
|
858
|
+
* @param expectedType - Expected Vega-Lite type from schema
|
|
859
|
+
* @param encoding - Encoding object to potentially modify
|
|
860
|
+
* @param channelName - Name of encoding channel (x, y, etc.) for auto-correction
|
|
861
|
+
* @returns Corrected type if auto-correction was applied, otherwise undefined
|
|
862
|
+
*/ function validateEncodingType(data, field, expectedType, encoding, channelName) {
|
|
863
|
+
if (!expectedType || expectedType === 'nominal' || expectedType === 'ordinal' || expectedType === 'geojson') {
|
|
864
|
+
return; // Nominal, ordinal, and geojson accept any type
|
|
865
|
+
}
|
|
866
|
+
// Find first non-null value to check type
|
|
867
|
+
const sampleValue = data.map((row)=>row[field]).find((v)=>v !== null && v !== undefined);
|
|
868
|
+
if (!sampleValue) {
|
|
869
|
+
return; // No values to validate
|
|
870
|
+
}
|
|
871
|
+
if (expectedType === 'quantitative') {
|
|
872
|
+
if (typeof sampleValue !== 'number' && !isFinite(Number(sampleValue))) {
|
|
873
|
+
// Type mismatch: quantitative declared but data is not numeric
|
|
874
|
+
const actualType = typeof sampleValue;
|
|
875
|
+
if (actualType === 'string') {
|
|
876
|
+
// Auto-correct: treat as nominal for categorical string data
|
|
877
|
+
// This matches Plotly behavior - render as categorical chart
|
|
878
|
+
// Modify encoding to use nominal type
|
|
879
|
+
if (encoding && channelName && encoding[channelName]) {
|
|
880
|
+
encoding[channelName].type = 'nominal';
|
|
881
|
+
}
|
|
882
|
+
return 'nominal';
|
|
883
|
+
}
|
|
884
|
+
// For non-string types, still throw error (truly invalid)
|
|
885
|
+
throw new Error(`VegaLiteSchemaAdapter: Field '${field}' marked as quantitative but contains non-numeric values (${actualType}).`);
|
|
886
|
+
}
|
|
887
|
+
} else if (expectedType === 'temporal') {
|
|
888
|
+
const isValidDate = sampleValue instanceof Date || typeof sampleValue === 'string' && !isNaN(Date.parse(sampleValue));
|
|
889
|
+
if (!isValidDate) {
|
|
890
|
+
let suggestion = '';
|
|
891
|
+
if (typeof sampleValue === 'number') {
|
|
892
|
+
suggestion = ' The data contains numbers. Change the type to "quantitative" instead.';
|
|
893
|
+
} else if (typeof sampleValue === 'string') {
|
|
894
|
+
suggestion = ` The data contains strings that are not valid dates (e.g., "${sampleValue}"). Ensure dates are in ISO format (YYYY-MM-DD) or valid date strings.`;
|
|
895
|
+
}
|
|
896
|
+
throw new Error(`VegaLiteSchemaAdapter: Field '${field}' marked as temporal but contains invalid date values.${suggestion}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Validates X and Y encodings for charts requiring both axes
|
|
903
|
+
* Performs comprehensive validation including data array, nested arrays, and encoding types
|
|
904
|
+
* Can auto-correct type mismatches (e.g., quantitative with string data → nominal)
|
|
905
|
+
*
|
|
906
|
+
* @param data - Array of data objects
|
|
907
|
+
* @param xField - X field name
|
|
908
|
+
* @param yField - Y field name
|
|
909
|
+
* @param xType - Expected X encoding type
|
|
910
|
+
* @param yType - Expected Y encoding type
|
|
911
|
+
* @param chartType - Chart type for error message context
|
|
912
|
+
* @param encoding - Encoding object (optional, for auto-correction)
|
|
913
|
+
* @throws Error if validation fails
|
|
914
|
+
*/ function validateXYEncodings(data, xField, yField, xType, yType, chartType, encoding) {
|
|
915
|
+
validateDataArray(data, xField, chartType);
|
|
916
|
+
validateDataArray(data, yField, chartType);
|
|
917
|
+
validateNoNestedArrays(data, xField);
|
|
918
|
+
validateNoNestedArrays(data, yField);
|
|
919
|
+
// Validate types with auto-correction support
|
|
920
|
+
validateEncodingType(data, xField, xType, encoding, 'x');
|
|
921
|
+
validateEncodingType(data, yField, yType, encoding, 'y');
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Extracts Y-axis scale type from encoding
|
|
925
|
+
* Returns 'log' if logarithmic scale is specified, undefined otherwise
|
|
926
|
+
*/ function extractYAxisType(encoding) {
|
|
927
|
+
var _encoding_y;
|
|
928
|
+
const yScale = encoding === null || encoding === void 0 ? void 0 : (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.scale;
|
|
929
|
+
return (yScale === null || yScale === void 0 ? void 0 : yScale.type) === 'log' ? 'log' : undefined;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Extracts y-axis min/max considering both scale.domain and scale.zero.
|
|
933
|
+
* When scale.zero is false and no explicit domain, yMinValue is computed from data.
|
|
934
|
+
*/ function extractYMinMax(encoding, dataValues) {
|
|
935
|
+
var _encoding_y;
|
|
936
|
+
const yScale = encoding === null || encoding === void 0 ? void 0 : (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.scale;
|
|
937
|
+
const domain = yScale === null || yScale === void 0 ? void 0 : yScale.domain;
|
|
938
|
+
// Explicit domain takes priority
|
|
939
|
+
if (Array.isArray(domain)) {
|
|
940
|
+
return {
|
|
941
|
+
yMinValue: domain[0],
|
|
942
|
+
yMaxValue: domain[1]
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
// When zero is explicitly false, compute min from data so y-axis doesn't start at 0
|
|
946
|
+
if ((yScale === null || yScale === void 0 ? void 0 : yScale.zero) === false) {
|
|
947
|
+
var _encoding_y1;
|
|
948
|
+
const yField = encoding === null || encoding === void 0 ? void 0 : (_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.field;
|
|
949
|
+
if (yField) {
|
|
950
|
+
const yValues = dataValues.map((row)=>Number(row[yField])).filter((v)=>!isNaN(v));
|
|
951
|
+
if (yValues.length > 0) {
|
|
952
|
+
var _d3Min;
|
|
953
|
+
const dataMin = (_d3Min = (0, _d3array.min)(yValues)) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
954
|
+
var _d3Max;
|
|
955
|
+
const dataMax = (_d3Max = (0, _d3array.max)(yValues)) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
956
|
+
const padding = (dataMax - dataMin) * 0.05;
|
|
957
|
+
return {
|
|
958
|
+
yMinValue: dataMin - padding
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return {};
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Creates a value formatter from a d3-format specifier string.
|
|
967
|
+
* Returns undefined if no format is specified or if the format is invalid.
|
|
968
|
+
*/ function createValueFormatter(formatSpec) {
|
|
969
|
+
if (!formatSpec) {
|
|
970
|
+
return undefined;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const formatter = (0, _d3format.format)(formatSpec);
|
|
974
|
+
return formatter;
|
|
975
|
+
} catch {
|
|
976
|
+
return undefined;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Converts Vega-Lite sort specification to Fluent Charts AxisCategoryOrder
|
|
981
|
+
* Supports: 'ascending', 'descending', null, array, or object with op/order
|
|
982
|
+
* @param sort - Vega-Lite sort specification
|
|
983
|
+
* @returns AxisCategoryOrder compatible value
|
|
984
|
+
*/ function convertVegaSortToAxisCategoryOrder(sort) {
|
|
985
|
+
if (!sort) {
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
// Handle string sorts: 'ascending' | 'descending'
|
|
989
|
+
if (typeof sort === 'string') {
|
|
990
|
+
if (sort === 'ascending') {
|
|
991
|
+
return 'category ascending';
|
|
992
|
+
}
|
|
993
|
+
if (sort === 'descending') {
|
|
994
|
+
return 'category descending';
|
|
995
|
+
}
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
// Handle array sort (explicit ordering)
|
|
999
|
+
if (Array.isArray(sort)) {
|
|
1000
|
+
return sort;
|
|
1001
|
+
}
|
|
1002
|
+
// Handle object sort with op and order
|
|
1003
|
+
if (typeof sort === 'object' && sort.op && sort.order) {
|
|
1004
|
+
const op = sort.op === 'average' ? 'mean' : sort.op; // Map 'average' to 'mean'
|
|
1005
|
+
const order = sort.order === 'ascending' ? 'ascending' : 'descending';
|
|
1006
|
+
return `${op} ${order}`;
|
|
1007
|
+
}
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Extracts axis category ordering from Vega-Lite encoding
|
|
1012
|
+
* Returns props for xAxisCategoryOrder and yAxisCategoryOrder
|
|
1013
|
+
*/ function extractAxisCategoryOrderProps(encoding) {
|
|
1014
|
+
var _encoding_x, _encoding_y;
|
|
1015
|
+
const result = {};
|
|
1016
|
+
if (encoding === null || encoding === void 0 ? void 0 : (_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.sort) {
|
|
1017
|
+
const xOrder = convertVegaSortToAxisCategoryOrder(encoding.x.sort);
|
|
1018
|
+
if (xOrder) {
|
|
1019
|
+
result.xAxisCategoryOrder = xOrder;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (encoding === null || encoding === void 0 ? void 0 : (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.sort) {
|
|
1023
|
+
const yOrder = convertVegaSortToAxisCategoryOrder(encoding.y.sort);
|
|
1024
|
+
if (yOrder) {
|
|
1025
|
+
result.yAxisCategoryOrder = yOrder;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Initializes the transformation context by normalizing spec and extracting common data
|
|
1032
|
+
* This reduces boilerplate across all transformer functions
|
|
1033
|
+
*
|
|
1034
|
+
* @param spec - Vega-Lite specification
|
|
1035
|
+
* @returns Normalized context with unit specs, data values, encoding, and mark properties
|
|
1036
|
+
*/ function initializeTransformContext(spec) {
|
|
1037
|
+
var _colorEnc_condition, _encoding_x, _encoding_x1;
|
|
1038
|
+
const unitSpecs = normalizeSpec(spec);
|
|
1039
|
+
if (unitSpecs.length === 0) {
|
|
1040
|
+
throw new Error('VegaLiteSchemaAdapter: No valid unit specs found in specification');
|
|
1041
|
+
}
|
|
1042
|
+
const primarySpec = unitSpecs[0];
|
|
1043
|
+
const rawDataValues = extractDataValues(primarySpec.data);
|
|
1044
|
+
// Apply any transforms from both top-level spec and primary unit spec
|
|
1045
|
+
let dataValues = applyTransforms(rawDataValues, spec.transform);
|
|
1046
|
+
dataValues = applyTransforms(dataValues, primarySpec.transform);
|
|
1047
|
+
const encoding = primarySpec.encoding || {};
|
|
1048
|
+
// Handle conditional color encoding — evaluate test expressions and materialize color values
|
|
1049
|
+
const colorEnc = encoding.color;
|
|
1050
|
+
if ((colorEnc === null || colorEnc === void 0 ? void 0 : colorEnc.condition) && typeof ((_colorEnc_condition = colorEnc.condition) === null || _colorEnc_condition === void 0 ? void 0 : _colorEnc_condition.test) === 'string') {
|
|
1051
|
+
const condition = colorEnc.condition;
|
|
1052
|
+
const elseValue = colorEnc.value || '#999';
|
|
1053
|
+
const colorField = '__conditional_color__';
|
|
1054
|
+
dataValues.forEach((row)=>{
|
|
1055
|
+
try {
|
|
1056
|
+
const testResult = (0, _VegaLiteExpressionEvaluator.safeEvaluateExpression)(condition.test, row);
|
|
1057
|
+
row[colorField] = testResult ? condition.value : elseValue;
|
|
1058
|
+
} catch {
|
|
1059
|
+
row[colorField] = elseValue;
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
// Replace conditional color with a field-based color encoding using the materialized values
|
|
1063
|
+
encoding.color = {
|
|
1064
|
+
field: colorField,
|
|
1065
|
+
type: 'nominal',
|
|
1066
|
+
scale: {
|
|
1067
|
+
domain: [
|
|
1068
|
+
condition.value,
|
|
1069
|
+
elseValue
|
|
1070
|
+
],
|
|
1071
|
+
range: [
|
|
1072
|
+
condition.value,
|
|
1073
|
+
elseValue
|
|
1074
|
+
]
|
|
1075
|
+
},
|
|
1076
|
+
legend: null
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
// Handle timeUnit on x/y encodings — aggregate data by time unit
|
|
1080
|
+
if (((_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.timeUnit) && ((_encoding_x1 = encoding.x) === null || _encoding_x1 === void 0 ? void 0 : _encoding_x1.field)) {
|
|
1081
|
+
var _encoding_y, _encoding_y1, _encoding_y2;
|
|
1082
|
+
const field = encoding.x.field;
|
|
1083
|
+
const unit = encoding.x.timeUnit;
|
|
1084
|
+
const yField = (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.field;
|
|
1085
|
+
const yAgg = ((_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.aggregate) || (yField ? 'mean' : 'count');
|
|
1086
|
+
const groups = new Map();
|
|
1087
|
+
dataValues.forEach((row)=>{
|
|
1088
|
+
const dateVal = new Date(row[field]);
|
|
1089
|
+
if (isNaN(dateVal.getTime())) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
let key;
|
|
1093
|
+
switch(unit){
|
|
1094
|
+
case 'year':
|
|
1095
|
+
key = String(dateVal.getFullYear());
|
|
1096
|
+
break;
|
|
1097
|
+
case 'quarter':
|
|
1098
|
+
key = `Q${Math.floor(dateVal.getMonth() / 3) + 1}`;
|
|
1099
|
+
break;
|
|
1100
|
+
case 'month':
|
|
1101
|
+
key = dateVal.toLocaleString('en', {
|
|
1102
|
+
month: 'short'
|
|
1103
|
+
});
|
|
1104
|
+
break;
|
|
1105
|
+
case 'day':
|
|
1106
|
+
key = String(dateVal.getDate());
|
|
1107
|
+
break;
|
|
1108
|
+
case 'hours':
|
|
1109
|
+
key = String(dateVal.getHours());
|
|
1110
|
+
break;
|
|
1111
|
+
default:
|
|
1112
|
+
key = String(dateVal);
|
|
1113
|
+
}
|
|
1114
|
+
if (!groups.has(key)) {
|
|
1115
|
+
groups.set(key, []);
|
|
1116
|
+
}
|
|
1117
|
+
groups.get(key).push(row);
|
|
1118
|
+
});
|
|
1119
|
+
dataValues = Array.from(groups.entries()).map(([key, rows])=>{
|
|
1120
|
+
const result = {
|
|
1121
|
+
[field]: key
|
|
1122
|
+
};
|
|
1123
|
+
if (yField && yAgg !== 'count') {
|
|
1124
|
+
const vals = rows.map((r)=>Number(r[yField])).filter((v)=>!isNaN(v));
|
|
1125
|
+
var _d3Mean;
|
|
1126
|
+
result[yField] = yAgg === 'sum' ? (0, _d3array.sum)(vals) : (_d3Mean = (0, _d3array.mean)(vals)) !== null && _d3Mean !== void 0 ? _d3Mean : 0;
|
|
1127
|
+
} else {
|
|
1128
|
+
result[yField || '__count'] = rows.length;
|
|
1129
|
+
}
|
|
1130
|
+
return result;
|
|
1131
|
+
});
|
|
1132
|
+
// Switch x from temporal to ordinal since we've aggregated
|
|
1133
|
+
encoding.x.type = 'ordinal';
|
|
1134
|
+
delete encoding.x.timeUnit;
|
|
1135
|
+
if ((_encoding_y2 = encoding.y) === null || _encoding_y2 === void 0 ? void 0 : _encoding_y2.aggregate) {
|
|
1136
|
+
delete encoding.y.aggregate;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const markProps = getMarkProperties(primarySpec.mark);
|
|
1140
|
+
return {
|
|
1141
|
+
unitSpecs,
|
|
1142
|
+
primarySpec,
|
|
1143
|
+
dataValues,
|
|
1144
|
+
encoding,
|
|
1145
|
+
markProps
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Extracts common encoding fields and aggregates from Vega-Lite encoding
|
|
1150
|
+
*
|
|
1151
|
+
* @param encoding - Vega-Lite encoding specification
|
|
1152
|
+
* @returns Object containing extracted field names and aggregates
|
|
1153
|
+
*/ function extractEncodingFields(encoding) {
|
|
1154
|
+
var _encoding_x, _encoding_y, _encoding_x2, _encoding_color, _encoding_size, _encoding_theta, _encoding_radius, _encoding_x1, _encoding_y1;
|
|
1155
|
+
return {
|
|
1156
|
+
xField: (_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.field,
|
|
1157
|
+
yField: (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.field,
|
|
1158
|
+
x2Field: (_encoding_x2 = encoding.x2) === null || _encoding_x2 === void 0 ? void 0 : _encoding_x2.field,
|
|
1159
|
+
colorField: (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.field,
|
|
1160
|
+
sizeField: (_encoding_size = encoding.size) === null || _encoding_size === void 0 ? void 0 : _encoding_size.field,
|
|
1161
|
+
thetaField: (_encoding_theta = encoding.theta) === null || _encoding_theta === void 0 ? void 0 : _encoding_theta.field,
|
|
1162
|
+
radiusField: (_encoding_radius = encoding.radius) === null || _encoding_radius === void 0 ? void 0 : _encoding_radius.field,
|
|
1163
|
+
xAggregate: (_encoding_x1 = encoding.x) === null || _encoding_x1 === void 0 ? void 0 : _encoding_x1.aggregate,
|
|
1164
|
+
yAggregate: (_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.aggregate
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Computes aggregate values for bar charts
|
|
1169
|
+
* Supports count, sum, mean, min, max aggregations
|
|
1170
|
+
*
|
|
1171
|
+
* @param data - Array of data values
|
|
1172
|
+
* @param groupField - Field to group by (x-axis field)
|
|
1173
|
+
* @param valueField - Field to aggregate (y-axis field, optional for count)
|
|
1174
|
+
* @param aggregate - Aggregate function (count, sum, mean, min, max)
|
|
1175
|
+
* @returns Array of {category, value} objects
|
|
1176
|
+
*/ function computeAggregateData(data, groupField, valueField, aggregate) {
|
|
1177
|
+
// Group data by category
|
|
1178
|
+
const groups = new Map();
|
|
1179
|
+
data.forEach((row)=>{
|
|
1180
|
+
const category = String(row[groupField]);
|
|
1181
|
+
if (aggregate === 'count') {
|
|
1182
|
+
// For count, just track the count
|
|
1183
|
+
if (!groups.has(category)) {
|
|
1184
|
+
groups.set(category, []);
|
|
1185
|
+
}
|
|
1186
|
+
groups.get(category).push(1);
|
|
1187
|
+
} else if (valueField && row[valueField] !== undefined && row[valueField] !== null) {
|
|
1188
|
+
// For other aggregates, collect values
|
|
1189
|
+
const value = Number(row[valueField]);
|
|
1190
|
+
if (!isNaN(value)) {
|
|
1191
|
+
if (!groups.has(category)) {
|
|
1192
|
+
groups.set(category, []);
|
|
1193
|
+
}
|
|
1194
|
+
groups.get(category).push(value);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
// Compute aggregate for each group
|
|
1199
|
+
const result = [];
|
|
1200
|
+
groups.forEach((values, category)=>{
|
|
1201
|
+
let aggregatedValue;
|
|
1202
|
+
switch(aggregate){
|
|
1203
|
+
case 'count':
|
|
1204
|
+
aggregatedValue = values.length;
|
|
1205
|
+
break;
|
|
1206
|
+
case 'sum':
|
|
1207
|
+
aggregatedValue = values.reduce((a, b)=>a + b, 0);
|
|
1208
|
+
break;
|
|
1209
|
+
case 'mean':
|
|
1210
|
+
case 'average':
|
|
1211
|
+
aggregatedValue = values.reduce((a, b)=>a + b, 0) / values.length;
|
|
1212
|
+
break;
|
|
1213
|
+
case 'min':
|
|
1214
|
+
var _d3Min;
|
|
1215
|
+
aggregatedValue = (_d3Min = (0, _d3array.min)(values)) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
1216
|
+
break;
|
|
1217
|
+
case 'max':
|
|
1218
|
+
var _d3Max;
|
|
1219
|
+
aggregatedValue = (_d3Max = (0, _d3array.max)(values)) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
1220
|
+
break;
|
|
1221
|
+
default:
|
|
1222
|
+
aggregatedValue = values.length; // Default to count
|
|
1223
|
+
}
|
|
1224
|
+
result.push({
|
|
1225
|
+
category,
|
|
1226
|
+
value: aggregatedValue
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
return result;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Counts rows per x-category, optionally grouped by a secondary (color) field.
|
|
1233
|
+
* Returns a Map<xKey, Map<legend, count>>.
|
|
1234
|
+
*/ function countByCategory(dataValues, xField, colorField, defaultLegend) {
|
|
1235
|
+
const countMap = new Map();
|
|
1236
|
+
dataValues.forEach((row)=>{
|
|
1237
|
+
const xValue = row[xField];
|
|
1238
|
+
if (xValue === undefined) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const xKey = String(xValue);
|
|
1242
|
+
const legend = colorField && row[colorField] !== undefined ? String(row[colorField]) : defaultLegend;
|
|
1243
|
+
if (!countMap.has(xKey)) {
|
|
1244
|
+
countMap.set(xKey, new Map());
|
|
1245
|
+
}
|
|
1246
|
+
const legendMap = countMap.get(xKey);
|
|
1247
|
+
legendMap.set(legend, (legendMap.get(legend) || 0) + 1);
|
|
1248
|
+
});
|
|
1249
|
+
return countMap;
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Extracts color configuration from Vega-Lite encoding
|
|
1253
|
+
*
|
|
1254
|
+
* @param encoding - Vega-Lite encoding specification
|
|
1255
|
+
* @returns Color scheme and range configuration
|
|
1256
|
+
*/ function extractColorConfig(encoding) {
|
|
1257
|
+
var _encoding_color_scale, _encoding_color, _encoding_color_scale1, _encoding_color1;
|
|
1258
|
+
return {
|
|
1259
|
+
colorScheme: (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : (_encoding_color_scale = _encoding_color.scale) === null || _encoding_color_scale === void 0 ? void 0 : _encoding_color_scale.scheme,
|
|
1260
|
+
colorRange: (_encoding_color1 = encoding.color) === null || _encoding_color1 === void 0 ? void 0 : (_encoding_color_scale1 = _encoding_color1.scale) === null || _encoding_color_scale1 === void 0 ? void 0 : _encoding_color_scale1.range
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Groups data rows into series based on color encoding field
|
|
1265
|
+
* Returns a map of series name to data points and ordinal mapping for categorical x-axis
|
|
1266
|
+
*/ function groupDataBySeries(dataValues, xField, yField, colorField, isXTemporal, isYTemporal, xType, sizeField, yType) {
|
|
1267
|
+
const seriesMap = new Map();
|
|
1268
|
+
if (!xField || !yField) {
|
|
1269
|
+
return {
|
|
1270
|
+
seriesMap
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
// Check if x-axis is ordinal/nominal (categorical)
|
|
1274
|
+
const isXOrdinal = xType === 'ordinal' || xType === 'nominal';
|
|
1275
|
+
const ordinalMapping = isXOrdinal ? new Map() : undefined;
|
|
1276
|
+
const ordinalLabels = [];
|
|
1277
|
+
// Check if y-axis is ordinal/nominal (categorical)
|
|
1278
|
+
const isYOrdinal = yType === 'ordinal' || yType === 'nominal';
|
|
1279
|
+
const yOrdinalMapping = isYOrdinal ? new Map() : undefined;
|
|
1280
|
+
const yOrdinalLabels = [];
|
|
1281
|
+
dataValues.forEach((row)=>{
|
|
1282
|
+
const xValue = parseValue(row[xField], isXTemporal);
|
|
1283
|
+
const yValue = parseValue(row[yField], isYTemporal);
|
|
1284
|
+
// Skip invalid values using chart-utilities validation
|
|
1285
|
+
if ((0, _chartutilities.isInvalidValue)(xValue) || (0, _chartutilities.isInvalidValue)(yValue)) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
// Skip if x or y is empty string (from null/undefined) or y is not a valid number/string
|
|
1289
|
+
if (xValue === '' || yValue === '' || typeof yValue !== 'number' && typeof yValue !== 'string') {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const seriesName = colorField && row[colorField] !== undefined ? String(row[colorField]) : 'default';
|
|
1293
|
+
if (!seriesMap.has(seriesName)) {
|
|
1294
|
+
seriesMap.set(seriesName, []);
|
|
1295
|
+
}
|
|
1296
|
+
// Handle x-value based on type
|
|
1297
|
+
let numericX;
|
|
1298
|
+
if (isXOrdinal && typeof xValue === 'string') {
|
|
1299
|
+
// For ordinal data, map each unique string to a sequential index
|
|
1300
|
+
if (!ordinalMapping.has(xValue)) {
|
|
1301
|
+
ordinalMapping.set(xValue, ordinalMapping.size);
|
|
1302
|
+
ordinalLabels.push(xValue);
|
|
1303
|
+
}
|
|
1304
|
+
numericX = ordinalMapping.get(xValue);
|
|
1305
|
+
} else if (typeof xValue === 'string') {
|
|
1306
|
+
// For non-ordinal strings, try to parse as float (fallback to 0)
|
|
1307
|
+
const parsed = parseFloat(xValue);
|
|
1308
|
+
if (isNaN(parsed)) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
numericX = parsed;
|
|
1312
|
+
} else {
|
|
1313
|
+
numericX = xValue;
|
|
1314
|
+
}
|
|
1315
|
+
const markerSize = sizeField && row[sizeField] !== undefined ? Number(row[sizeField]) : undefined;
|
|
1316
|
+
// Handle y-value: numeric or ordinal mapping
|
|
1317
|
+
let numericY;
|
|
1318
|
+
if (isYOrdinal && typeof yValue === 'string') {
|
|
1319
|
+
if (!yOrdinalMapping.has(yValue)) {
|
|
1320
|
+
yOrdinalMapping.set(yValue, yOrdinalMapping.size);
|
|
1321
|
+
yOrdinalLabels.push(yValue);
|
|
1322
|
+
}
|
|
1323
|
+
numericY = yOrdinalMapping.get(yValue);
|
|
1324
|
+
} else {
|
|
1325
|
+
numericY = typeof yValue === 'number' ? yValue : 0;
|
|
1326
|
+
}
|
|
1327
|
+
seriesMap.get(seriesName).push({
|
|
1328
|
+
x: numericX,
|
|
1329
|
+
y: numericY,
|
|
1330
|
+
...markerSize !== undefined && !isNaN(markerSize) && {
|
|
1331
|
+
markerSize
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
return {
|
|
1336
|
+
seriesMap,
|
|
1337
|
+
ordinalMapping,
|
|
1338
|
+
ordinalLabels: ordinalLabels.length > 0 ? ordinalLabels : undefined,
|
|
1339
|
+
yOrdinalLabels: yOrdinalLabels.length > 0 ? yOrdinalLabels : undefined
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Finds the primary data layer from unit specs for line/area charts
|
|
1344
|
+
* Skips rect layers (used for color fill bars) and finds the actual line/point/area layer
|
|
1345
|
+
*
|
|
1346
|
+
* @param unitSpecs - Array of normalized unit specs
|
|
1347
|
+
* @returns The primary spec containing the actual chart data, or undefined if not found
|
|
1348
|
+
*/ function findPrimaryLineSpec(unitSpecs) {
|
|
1349
|
+
// First, try to find a line, point, or area layer
|
|
1350
|
+
const lineSpec = unitSpecs.find((spec)=>{
|
|
1351
|
+
const markType = getMarkType(spec.mark);
|
|
1352
|
+
return markType === 'line' || markType === 'point' || markType === 'area';
|
|
1353
|
+
});
|
|
1354
|
+
if (lineSpec) {
|
|
1355
|
+
return lineSpec;
|
|
1356
|
+
}
|
|
1357
|
+
// If no line/point/area layer, find first layer with actual field encodings (not just datum)
|
|
1358
|
+
const dataSpec = unitSpecs.find((spec)=>{
|
|
1359
|
+
var _encoding_x, _encoding_y;
|
|
1360
|
+
const encoding = spec.encoding || {};
|
|
1361
|
+
return ((_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.field) || ((_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.field);
|
|
1362
|
+
});
|
|
1363
|
+
return dataSpec || unitSpecs[0];
|
|
1364
|
+
}
|
|
1365
|
+
function autoCorrectEncodingTypes(spec) {
|
|
1366
|
+
var _encoding_x, _encoding_y;
|
|
1367
|
+
const unitSpec = spec.layer ? spec.layer[0] : spec;
|
|
1368
|
+
if (!unitSpec) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const encoding = unitSpec.encoding;
|
|
1372
|
+
var _unitSpec_data;
|
|
1373
|
+
const data = extractDataValues((_unitSpec_data = unitSpec.data) !== null && _unitSpec_data !== void 0 ? _unitSpec_data : spec.data);
|
|
1374
|
+
if (!encoding || data.length === 0) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
// Check x encoding
|
|
1378
|
+
if ((_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.field) {
|
|
1379
|
+
const sample = data.map((row)=>row[encoding.x.field]).find((v)=>v !== null && v !== undefined);
|
|
1380
|
+
if (sample !== undefined) {
|
|
1381
|
+
if (encoding.x.type === 'quantitative') {
|
|
1382
|
+
if (typeof sample === 'string' && !isFinite(Number(sample))) {
|
|
1383
|
+
encoding.x.type = 'nominal';
|
|
1384
|
+
} else if (typeof sample === 'object') {
|
|
1385
|
+
encoding.x.type = 'nominal';
|
|
1386
|
+
}
|
|
1387
|
+
} else if (encoding.x.type === 'temporal') {
|
|
1388
|
+
const isValidDate = sample instanceof Date || typeof sample === 'string' && !isNaN(Date.parse(sample));
|
|
1389
|
+
if (!isValidDate) {
|
|
1390
|
+
encoding.x.type = typeof sample === 'number' ? 'quantitative' : 'nominal';
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
// Check y encoding
|
|
1396
|
+
if ((_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.field) {
|
|
1397
|
+
const sample = data.map((row)=>row[encoding.y.field]).find((v)=>v !== null && v !== undefined);
|
|
1398
|
+
if (sample !== undefined) {
|
|
1399
|
+
if (encoding.y.type === 'quantitative') {
|
|
1400
|
+
if (typeof sample === 'string' && !isFinite(Number(sample))) {
|
|
1401
|
+
encoding.y.type = 'nominal';
|
|
1402
|
+
} else if (typeof sample === 'object' && sample !== null && !Array.isArray(sample) && !(sample instanceof Date)) {
|
|
1403
|
+
// Try to extract a numeric value from the object
|
|
1404
|
+
const numericKeys = Object.keys(sample).filter((k)=>typeof sample[k] === 'number');
|
|
1405
|
+
if (numericKeys.length === 1) {
|
|
1406
|
+
// Object has exactly one numeric property - use it as the value
|
|
1407
|
+
const numericKey = numericKeys[0];
|
|
1408
|
+
const yField = encoding.y.field;
|
|
1409
|
+
data.forEach((row)=>{
|
|
1410
|
+
const obj = row[yField];
|
|
1411
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
1412
|
+
row[yField] = obj[numericKey];
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
// Keep type as quantitative since we extracted numeric values
|
|
1416
|
+
} else {
|
|
1417
|
+
encoding.y.type = 'nominal';
|
|
1418
|
+
}
|
|
1419
|
+
} else if (typeof sample === 'object') {
|
|
1420
|
+
encoding.y.type = 'nominal';
|
|
1421
|
+
}
|
|
1422
|
+
} else if (encoding.y.type === 'temporal') {
|
|
1423
|
+
const isValidDate = sample instanceof Date || typeof sample === 'string' && !isNaN(Date.parse(sample));
|
|
1424
|
+
if (!isValidDate) {
|
|
1425
|
+
encoding.y.type = typeof sample === 'number' ? 'quantitative' : 'nominal';
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function getChartType(spec) {
|
|
1432
|
+
var _spec_layer_, _spec_layer_1, _encoding_color, _encoding_x, _encoding_y, _encoding_color1;
|
|
1433
|
+
// Auto-correct encoding types based on actual data BEFORE chart type detection
|
|
1434
|
+
autoCorrectEncodingTypes(spec);
|
|
1435
|
+
// Handle layered specs - check if it's a bar+line combo for stacked bar with lines
|
|
1436
|
+
if (spec.layer && spec.layer.length > 1) {
|
|
1437
|
+
const marks = spec.layer.map((layer)=>getMarkType(layer.mark));
|
|
1438
|
+
const hasBar = marks.includes('bar');
|
|
1439
|
+
const hasLine = marks.includes('line') || marks.includes('point');
|
|
1440
|
+
// Bar + line combo should use stacked bar chart (which supports line overlays)
|
|
1441
|
+
if (hasBar && hasLine) {
|
|
1442
|
+
var _barLayer_encoding_color, _barLayer_encoding;
|
|
1443
|
+
const barLayer = spec.layer.find((layer)=>getMarkType(layer.mark) === 'bar');
|
|
1444
|
+
if (barLayer === null || barLayer === void 0 ? void 0 : (_barLayer_encoding = barLayer.encoding) === null || _barLayer_encoding === void 0 ? void 0 : (_barLayer_encoding_color = _barLayer_encoding.color) === null || _barLayer_encoding_color === void 0 ? void 0 : _barLayer_encoding_color.field) {
|
|
1445
|
+
return {
|
|
1446
|
+
type: 'stacked-bar',
|
|
1447
|
+
mark: 'bar'
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
type: 'stacked-bar',
|
|
1452
|
+
mark: 'bar'
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
// Handle layered specs - use first layer's mark for other cases
|
|
1457
|
+
const mark = spec.layer ? (_spec_layer_ = spec.layer[0]) === null || _spec_layer_ === void 0 ? void 0 : _spec_layer_.mark : spec.mark;
|
|
1458
|
+
const markType = getMarkType(mark);
|
|
1459
|
+
const encoding = spec.layer ? (_spec_layer_1 = spec.layer[0]) === null || _spec_layer_1 === void 0 ? void 0 : _spec_layer_1.encoding : spec.encoding;
|
|
1460
|
+
const hasColorEncoding = !!(encoding === null || encoding === void 0 ? void 0 : (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.field);
|
|
1461
|
+
// Polar charts with arc marks: theta AND radius encodings
|
|
1462
|
+
if (markType === 'arc' && (encoding === null || encoding === void 0 ? void 0 : encoding.theta) && (encoding === null || encoding === void 0 ? void 0 : encoding.radius)) {
|
|
1463
|
+
return {
|
|
1464
|
+
type: 'polar',
|
|
1465
|
+
mark: markType
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
// Arc marks for pie/donut charts (theta only, no radius)
|
|
1469
|
+
if (markType === 'arc' && (encoding === null || encoding === void 0 ? void 0 : encoding.theta)) {
|
|
1470
|
+
return {
|
|
1471
|
+
type: 'donut',
|
|
1472
|
+
mark: markType
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
// Polar charts: non-arc marks with theta and radius encodings
|
|
1476
|
+
if ((encoding === null || encoding === void 0 ? void 0 : encoding.theta) && (encoding === null || encoding === void 0 ? void 0 : encoding.radius)) {
|
|
1477
|
+
return {
|
|
1478
|
+
type: 'polar',
|
|
1479
|
+
mark: markType
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
// Rect marks for heatmaps (quantitative or nominal color)
|
|
1483
|
+
if (markType === 'rect' && (encoding === null || encoding === void 0 ? void 0 : (_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.field) && (encoding === null || encoding === void 0 ? void 0 : (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.field) && (encoding === null || encoding === void 0 ? void 0 : (_encoding_color1 = encoding.color) === null || _encoding_color1 === void 0 ? void 0 : _encoding_color1.field)) {
|
|
1484
|
+
return {
|
|
1485
|
+
type: 'heatmap',
|
|
1486
|
+
mark: markType
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
// Bar charts
|
|
1490
|
+
if (markType === 'bar') {
|
|
1491
|
+
var _encoding_x1, _encoding_x2, _encoding_x3, _encoding_y1, _encoding_y2, _encoding_x4, _spec_data;
|
|
1492
|
+
if (encoding === null || encoding === void 0 ? void 0 : (_encoding_x1 = encoding.x) === null || _encoding_x1 === void 0 ? void 0 : _encoding_x1.bin) {
|
|
1493
|
+
return {
|
|
1494
|
+
type: 'histogram',
|
|
1495
|
+
mark: markType
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
const isXNominal = (encoding === null || encoding === void 0 ? void 0 : (_encoding_x2 = encoding.x) === null || _encoding_x2 === void 0 ? void 0 : _encoding_x2.type) === 'nominal' || (encoding === null || encoding === void 0 ? void 0 : (_encoding_x3 = encoding.x) === null || _encoding_x3 === void 0 ? void 0 : _encoding_x3.type) === 'ordinal';
|
|
1499
|
+
const isYNominal = (encoding === null || encoding === void 0 ? void 0 : (_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.type) === 'nominal' || (encoding === null || encoding === void 0 ? void 0 : (_encoding_y2 = encoding.y) === null || _encoding_y2 === void 0 ? void 0 : _encoding_y2.type) === 'ordinal';
|
|
1500
|
+
if (isYNominal && !isXNominal) {
|
|
1501
|
+
return {
|
|
1502
|
+
type: 'horizontal-bar',
|
|
1503
|
+
mark: markType
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
if (hasColorEncoding) {
|
|
1507
|
+
const hasXOffset = !!(encoding === null || encoding === void 0 ? void 0 : encoding.xOffset);
|
|
1508
|
+
if (hasXOffset) {
|
|
1509
|
+
return {
|
|
1510
|
+
type: 'grouped-bar',
|
|
1511
|
+
mark: markType
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
type: 'stacked-bar',
|
|
1516
|
+
mark: markType
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
const xField = encoding === null || encoding === void 0 ? void 0 : (_encoding_x4 = encoding.x) === null || _encoding_x4 === void 0 ? void 0 : _encoding_x4.field;
|
|
1520
|
+
const dataValues = (_spec_data = spec.data) === null || _spec_data === void 0 ? void 0 : _spec_data.values;
|
|
1521
|
+
if (xField && Array.isArray(dataValues) && dataValues.length > 0) {
|
|
1522
|
+
const xValues = dataValues.map((row)=>row[xField]);
|
|
1523
|
+
const uniqueXValues = new Set(xValues);
|
|
1524
|
+
if (uniqueXValues.size < xValues.length) {
|
|
1525
|
+
return {
|
|
1526
|
+
type: 'stacked-bar',
|
|
1527
|
+
mark: markType
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return {
|
|
1532
|
+
type: 'bar',
|
|
1533
|
+
mark: markType
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (markType === 'area') {
|
|
1537
|
+
return {
|
|
1538
|
+
type: 'area',
|
|
1539
|
+
mark: markType
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
if (markType === 'point' || markType === 'circle' || markType === 'square' || markType === 'tick') {
|
|
1543
|
+
return {
|
|
1544
|
+
type: 'scatter',
|
|
1545
|
+
mark: markType
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
// Trail marks rendered as line charts (size encoding as markerSize)
|
|
1549
|
+
if (markType === 'trail') {
|
|
1550
|
+
return {
|
|
1551
|
+
type: 'line',
|
|
1552
|
+
mark: 'line'
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
// Error bar/band marks rendered as line charts
|
|
1556
|
+
if (markType === 'errorbar' || markType === 'errorband') {
|
|
1557
|
+
return {
|
|
1558
|
+
type: 'line',
|
|
1559
|
+
mark: 'line'
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
type: 'line',
|
|
1564
|
+
mark: markType || 'line'
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
function transformVegaLiteToLineChartProps(spec, colorMap, isDarkTheme) {
|
|
1568
|
+
var _encoding_x, _encoding_y, _encoding_x1, _encoding_y1, _encoding_x2, _encoding_y2, _spec_title, _encoding_x_axis, _encoding_x3, _encoding_y_axis, _encoding_y3, _encoding_x_axis1, _encoding_x4, _encoding_y_axis1, _encoding_y4, _encoding_x_axis2, _encoding_x5, _encoding_y_axis2, _encoding_y5, _encoding_color_legend, _encoding_color;
|
|
1569
|
+
// Initialize transformation context, but find the primary line/point layer for layered specs
|
|
1570
|
+
const unitSpecs = normalizeSpec(spec);
|
|
1571
|
+
if (unitSpecs.length === 0) {
|
|
1572
|
+
throw new Error('VegaLiteSchemaAdapter: No valid unit specs found in specification');
|
|
1573
|
+
}
|
|
1574
|
+
// For layered specs, find the actual line/point layer (not rect layers for color fill bars)
|
|
1575
|
+
const primarySpec = findPrimaryLineSpec(unitSpecs);
|
|
1576
|
+
if (!primarySpec) {
|
|
1577
|
+
throw new Error('VegaLiteSchemaAdapter: No valid line/point layer found in specification');
|
|
1578
|
+
}
|
|
1579
|
+
// Check if there's a point layer in addition to line layer (for line+point combo charts)
|
|
1580
|
+
const hasPointLayer = unitSpecs.some((unitSpec)=>getMarkType(unitSpec.mark) === 'point');
|
|
1581
|
+
const hasLineLayer = unitSpecs.some((unitSpec)=>getMarkType(unitSpec.mark) === 'line');
|
|
1582
|
+
const shouldShowPoints = hasPointLayer && hasLineLayer;
|
|
1583
|
+
const rawDataValues = extractDataValues(primarySpec.data);
|
|
1584
|
+
// Apply any transforms (fold, etc.) from the spec
|
|
1585
|
+
const dataValues = applyTransforms(rawDataValues, spec.transform);
|
|
1586
|
+
const encoding = primarySpec.encoding || {};
|
|
1587
|
+
const markProps = getMarkProperties(primarySpec.mark);
|
|
1588
|
+
// Extract field names
|
|
1589
|
+
const { xField, yField, colorField } = extractEncodingFields(encoding);
|
|
1590
|
+
// Check for size encoding from any layer (e.g., point layer with size in line+point combo)
|
|
1591
|
+
let sizeField;
|
|
1592
|
+
if (unitSpecs.length > 1) {
|
|
1593
|
+
for (const unitSpec of unitSpecs){
|
|
1594
|
+
var _unitEncoding_size;
|
|
1595
|
+
const unitEncoding = unitSpec.encoding || {};
|
|
1596
|
+
if ((_unitEncoding_size = unitEncoding.size) === null || _unitEncoding_size === void 0 ? void 0 : _unitEncoding_size.field) {
|
|
1597
|
+
sizeField = unitEncoding.size.field;
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
var _encoding_size;
|
|
1603
|
+
sizeField = (_encoding_size = encoding.size) === null || _encoding_size === void 0 ? void 0 : _encoding_size.field;
|
|
1604
|
+
}
|
|
1605
|
+
// Validate data and encodings
|
|
1606
|
+
if (!xField || !yField) {
|
|
1607
|
+
throw new Error('VegaLiteSchemaAdapter: Line chart requires both x and y encodings with field names');
|
|
1608
|
+
}
|
|
1609
|
+
validateXYEncodings(dataValues, xField, yField, (_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.type, (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.type, 'LineChart', encoding);
|
|
1610
|
+
const isXTemporal = ((_encoding_x1 = encoding.x) === null || _encoding_x1 === void 0 ? void 0 : _encoding_x1.type) === 'temporal';
|
|
1611
|
+
const isYTemporal = ((_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.type) === 'temporal';
|
|
1612
|
+
// Group data into series
|
|
1613
|
+
const { seriesMap, ordinalLabels, yOrdinalLabels } = groupDataBySeries(dataValues, xField, yField, colorField, isXTemporal, isYTemporal, (_encoding_x2 = encoding.x) === null || _encoding_x2 === void 0 ? void 0 : _encoding_x2.type, sizeField, (_encoding_y2 = encoding.y) === null || _encoding_y2 === void 0 ? void 0 : _encoding_y2.type);
|
|
1614
|
+
// Extract color configuration
|
|
1615
|
+
const { colorScheme, colorRange } = extractColorConfig(encoding);
|
|
1616
|
+
// Convert series map to LineChartPoints array
|
|
1617
|
+
const lineChartData = [];
|
|
1618
|
+
const colorIndex = new Map();
|
|
1619
|
+
let currentColorIndex = 0;
|
|
1620
|
+
seriesMap.forEach((dataPoints, seriesName)=>{
|
|
1621
|
+
if (!colorIndex.has(seriesName)) {
|
|
1622
|
+
colorIndex.set(seriesName, currentColorIndex++);
|
|
1623
|
+
}
|
|
1624
|
+
const color = resolveColor(seriesName, colorIndex.get(seriesName), undefined, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
1625
|
+
const curveOption = mapInterpolateToCurve(markProps.interpolate);
|
|
1626
|
+
// Build line options with curve, strokeDash, and strokeWidth
|
|
1627
|
+
const lineOptions = {};
|
|
1628
|
+
if (curveOption) {
|
|
1629
|
+
lineOptions.curve = curveOption;
|
|
1630
|
+
}
|
|
1631
|
+
if (markProps.strokeDash) {
|
|
1632
|
+
lineOptions.strokeDasharray = markProps.strokeDash.join(' ');
|
|
1633
|
+
}
|
|
1634
|
+
if (markProps.strokeWidth) {
|
|
1635
|
+
lineOptions.strokeWidth = markProps.strokeWidth;
|
|
1636
|
+
}
|
|
1637
|
+
lineChartData.push({
|
|
1638
|
+
legend: seriesName,
|
|
1639
|
+
data: dataPoints,
|
|
1640
|
+
color,
|
|
1641
|
+
hideNonActiveDots: !shouldShowPoints,
|
|
1642
|
+
...Object.keys(lineOptions).length > 0 && {
|
|
1643
|
+
lineOptions
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1647
|
+
// Extract chart title
|
|
1648
|
+
const chartTitle = typeof spec.title === 'string' ? spec.title : (_spec_title = spec.title) === null || _spec_title === void 0 ? void 0 : _spec_title.text;
|
|
1649
|
+
var _encoding_x_axis_title;
|
|
1650
|
+
// Extract axis titles and formats
|
|
1651
|
+
const xAxisTitle = (_encoding_x_axis_title = (_encoding_x3 = encoding.x) === null || _encoding_x3 === void 0 ? void 0 : (_encoding_x_axis = _encoding_x3.axis) === null || _encoding_x_axis === void 0 ? void 0 : _encoding_x_axis.title) !== null && _encoding_x_axis_title !== void 0 ? _encoding_x_axis_title : undefined;
|
|
1652
|
+
var _encoding_y_axis_title;
|
|
1653
|
+
const yAxisTitle = (_encoding_y_axis_title = (_encoding_y3 = encoding.y) === null || _encoding_y3 === void 0 ? void 0 : (_encoding_y_axis = _encoding_y3.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.title) !== null && _encoding_y_axis_title !== void 0 ? _encoding_y_axis_title : undefined;
|
|
1654
|
+
const tickFormat = (_encoding_x4 = encoding.x) === null || _encoding_x4 === void 0 ? void 0 : (_encoding_x_axis1 = _encoding_x4.axis) === null || _encoding_x_axis1 === void 0 ? void 0 : _encoding_x_axis1.format;
|
|
1655
|
+
const yAxisTickFormat = (_encoding_y4 = encoding.y) === null || _encoding_y4 === void 0 ? void 0 : (_encoding_y_axis1 = _encoding_y4.axis) === null || _encoding_y_axis1 === void 0 ? void 0 : _encoding_y_axis1.format;
|
|
1656
|
+
// Extract tick values and counts
|
|
1657
|
+
// Use ordinalLabels for ordinal x-axis, otherwise use explicit values from spec
|
|
1658
|
+
const tickValues = ordinalLabels || ((_encoding_x5 = encoding.x) === null || _encoding_x5 === void 0 ? void 0 : (_encoding_x_axis2 = _encoding_x5.axis) === null || _encoding_x_axis2 === void 0 ? void 0 : _encoding_x_axis2.values);
|
|
1659
|
+
const yAxisTickCount = (_encoding_y5 = encoding.y) === null || _encoding_y5 === void 0 ? void 0 : (_encoding_y_axis2 = _encoding_y5.axis) === null || _encoding_y_axis2 === void 0 ? void 0 : _encoding_y_axis2.tickCount;
|
|
1660
|
+
// Extract domain/range for min/max values
|
|
1661
|
+
const { yMinValue, yMaxValue } = extractYMinMax(encoding, dataValues);
|
|
1662
|
+
// Extract annotations and color fill bars from layers
|
|
1663
|
+
const annotations = extractAnnotations(spec);
|
|
1664
|
+
const colorFillBars = extractColorFillBars(spec, colorMap, isDarkTheme);
|
|
1665
|
+
// Convert rule marks in layered specs to reference line series
|
|
1666
|
+
// Each horizontal rule becomes a 2-point line at constant y spanning the data x-range
|
|
1667
|
+
if (spec.layer && Array.isArray(spec.layer) && lineChartData.length > 0) {
|
|
1668
|
+
const allXValues = lineChartData.flatMap((series)=>series.data.map((p)=>p.x));
|
|
1669
|
+
const xMin = allXValues.length > 0 ? allXValues.reduce((a, b)=>a < b ? a : b) : 0;
|
|
1670
|
+
const xMax = allXValues.length > 0 ? allXValues.reduce((a, b)=>a > b ? a : b) : 0;
|
|
1671
|
+
spec.layer.forEach((layer, layerIndex)=>{
|
|
1672
|
+
var _ruleEncoding_y, _ruleEncoding_y1, _textLayer_encoding_text, _textLayer_encoding, _textLayer_encoding_text1, _textLayer_encoding1;
|
|
1673
|
+
const layerMark = getMarkType(layer.mark);
|
|
1674
|
+
if (layerMark !== 'rule') {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const ruleEncoding = layer.encoding || {};
|
|
1678
|
+
var _ruleEncoding_y_datum;
|
|
1679
|
+
const yDatum = (_ruleEncoding_y_datum = (_ruleEncoding_y = ruleEncoding.y) === null || _ruleEncoding_y === void 0 ? void 0 : _ruleEncoding_y.datum) !== null && _ruleEncoding_y_datum !== void 0 ? _ruleEncoding_y_datum : (_ruleEncoding_y1 = ruleEncoding.y) === null || _ruleEncoding_y1 === void 0 ? void 0 : _ruleEncoding_y1.value;
|
|
1680
|
+
if (yDatum === undefined) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const ruleMarkProps = getMarkProperties(layer.mark);
|
|
1684
|
+
const ruleColor = ruleMarkProps.color || '#d62728';
|
|
1685
|
+
// Find companion text annotation for legend name
|
|
1686
|
+
const textLayer = spec.layer.find((l)=>{
|
|
1687
|
+
var _l_encoding;
|
|
1688
|
+
const m = getMarkType(l.mark);
|
|
1689
|
+
var _l_encoding_y_datum;
|
|
1690
|
+
return m === 'text' && ((_l_encoding = l.encoding) === null || _l_encoding === void 0 ? void 0 : _l_encoding.y) && ((_l_encoding_y_datum = l.encoding.y.datum) !== null && _l_encoding_y_datum !== void 0 ? _l_encoding_y_datum : l.encoding.y.value) === yDatum;
|
|
1691
|
+
});
|
|
1692
|
+
const ruleLegend = textLayer ? String(((_textLayer_encoding = textLayer.encoding) === null || _textLayer_encoding === void 0 ? void 0 : (_textLayer_encoding_text = _textLayer_encoding.text) === null || _textLayer_encoding_text === void 0 ? void 0 : _textLayer_encoding_text.datum) || ((_textLayer_encoding1 = textLayer.encoding) === null || _textLayer_encoding1 === void 0 ? void 0 : (_textLayer_encoding_text1 = _textLayer_encoding1.text) === null || _textLayer_encoding_text1 === void 0 ? void 0 : _textLayer_encoding_text1.value) || `y=${yDatum}`) : `y=${yDatum}`;
|
|
1693
|
+
const ruleLineOptions = {};
|
|
1694
|
+
if (ruleMarkProps.strokeDash) {
|
|
1695
|
+
ruleLineOptions.strokeDasharray = ruleMarkProps.strokeDash.join(' ');
|
|
1696
|
+
}
|
|
1697
|
+
if (ruleMarkProps.strokeWidth) {
|
|
1698
|
+
ruleLineOptions.strokeWidth = ruleMarkProps.strokeWidth;
|
|
1699
|
+
}
|
|
1700
|
+
lineChartData.push({
|
|
1701
|
+
legend: ruleLegend,
|
|
1702
|
+
data: [
|
|
1703
|
+
{
|
|
1704
|
+
x: xMin,
|
|
1705
|
+
y: yDatum
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
x: xMax,
|
|
1709
|
+
y: yDatum
|
|
1710
|
+
}
|
|
1711
|
+
],
|
|
1712
|
+
color: ruleColor,
|
|
1713
|
+
hideNonActiveDots: true,
|
|
1714
|
+
...Object.keys(ruleLineOptions).length > 0 && {
|
|
1715
|
+
lineOptions: ruleLineOptions
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
// Check for log scale on Y-axis
|
|
1721
|
+
const yAxisType = extractYAxisType(encoding);
|
|
1722
|
+
// Extract axis category ordering
|
|
1723
|
+
const categoryOrderProps = extractAxisCategoryOrderProps(encoding);
|
|
1724
|
+
// Build LineChartProps
|
|
1725
|
+
const chartProps = {
|
|
1726
|
+
lineChartData,
|
|
1727
|
+
...chartTitle && {
|
|
1728
|
+
chartTitle
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
var _encoding_color_legend_disable;
|
|
1732
|
+
return {
|
|
1733
|
+
data: chartProps,
|
|
1734
|
+
width: typeof spec.width === 'number' ? spec.width : undefined,
|
|
1735
|
+
height: typeof spec.height === 'number' ? spec.height : undefined,
|
|
1736
|
+
...xAxisTitle && {
|
|
1737
|
+
chartTitle: xAxisTitle
|
|
1738
|
+
},
|
|
1739
|
+
...yAxisTitle && {
|
|
1740
|
+
yAxisTitle
|
|
1741
|
+
},
|
|
1742
|
+
...tickFormat && {
|
|
1743
|
+
tickFormat
|
|
1744
|
+
},
|
|
1745
|
+
...yAxisTickFormat && {
|
|
1746
|
+
yAxisTickFormat
|
|
1747
|
+
},
|
|
1748
|
+
...tickValues && {
|
|
1749
|
+
tickValues
|
|
1750
|
+
},
|
|
1751
|
+
...yAxisTickCount && {
|
|
1752
|
+
yAxisTickCount
|
|
1753
|
+
},
|
|
1754
|
+
...yMinValue !== undefined && {
|
|
1755
|
+
yMinValue
|
|
1756
|
+
},
|
|
1757
|
+
...yMaxValue !== undefined && {
|
|
1758
|
+
yMaxValue
|
|
1759
|
+
},
|
|
1760
|
+
...annotations.length > 0 && {
|
|
1761
|
+
annotations
|
|
1762
|
+
},
|
|
1763
|
+
...colorFillBars.length > 0 && {
|
|
1764
|
+
colorFillBars
|
|
1765
|
+
},
|
|
1766
|
+
...yAxisType && {
|
|
1767
|
+
yScaleType: yAxisType
|
|
1768
|
+
},
|
|
1769
|
+
// For nominal y-axis, provide tick values and labels
|
|
1770
|
+
...yOrdinalLabels && yOrdinalLabels.length > 0 && {
|
|
1771
|
+
yAxisTickValues: Array.from({
|
|
1772
|
+
length: yOrdinalLabels.length
|
|
1773
|
+
}, (_, i)=>i),
|
|
1774
|
+
yAxisTickFormat: (val)=>{
|
|
1775
|
+
var _yOrdinalLabels_val;
|
|
1776
|
+
return (_yOrdinalLabels_val = yOrdinalLabels[val]) !== null && _yOrdinalLabels_val !== void 0 ? _yOrdinalLabels_val : String(val);
|
|
1777
|
+
},
|
|
1778
|
+
yMinValue: -0.5,
|
|
1779
|
+
yMaxValue: yOrdinalLabels.length - 0.5
|
|
1780
|
+
},
|
|
1781
|
+
...categoryOrderProps,
|
|
1782
|
+
hideLegend: (_encoding_color_legend_disable = (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : (_encoding_color_legend = _encoding_color.legend) === null || _encoding_color_legend === void 0 ? void 0 : _encoding_color_legend.disable) !== null && _encoding_color_legend_disable !== void 0 ? _encoding_color_legend_disable : false
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function getVegaLiteLegendsProps(spec, colorMap, isDarkTheme) {
|
|
1786
|
+
var _encoding_color;
|
|
1787
|
+
const unitSpecs = normalizeSpec(spec);
|
|
1788
|
+
const legends = [];
|
|
1789
|
+
if (unitSpecs.length === 0) {
|
|
1790
|
+
return {
|
|
1791
|
+
legends,
|
|
1792
|
+
centerLegends: true,
|
|
1793
|
+
enabledWrapLines: true,
|
|
1794
|
+
canSelectMultipleLegends: true
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
const primarySpec = unitSpecs[0];
|
|
1798
|
+
const dataValues = extractDataValues(primarySpec.data);
|
|
1799
|
+
const encoding = primarySpec.encoding || {};
|
|
1800
|
+
const colorField = (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.field;
|
|
1801
|
+
if (!colorField) {
|
|
1802
|
+
return {
|
|
1803
|
+
legends,
|
|
1804
|
+
centerLegends: true,
|
|
1805
|
+
enabledWrapLines: true,
|
|
1806
|
+
canSelectMultipleLegends: true
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
// Extract unique series names
|
|
1810
|
+
const seriesNames = new Set();
|
|
1811
|
+
dataValues.forEach((row)=>{
|
|
1812
|
+
if (row[colorField] !== undefined) {
|
|
1813
|
+
seriesNames.add(String(row[colorField]));
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
// Generate legends
|
|
1817
|
+
seriesNames.forEach((seriesName)=>{
|
|
1818
|
+
const color = (0, _VegaLiteColorAdapter.getVegaColorFromMap)(seriesName, colorMap, undefined, undefined, isDarkTheme);
|
|
1819
|
+
legends.push({
|
|
1820
|
+
title: seriesName,
|
|
1821
|
+
color
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
return {
|
|
1825
|
+
legends,
|
|
1826
|
+
centerLegends: true,
|
|
1827
|
+
enabledWrapLines: true,
|
|
1828
|
+
canSelectMultipleLegends: true
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function getVegaLiteTitles(spec) {
|
|
1832
|
+
var _spec_title, _encoding_x, _encoding_x_axis, _encoding_x1, _encoding_y, _encoding_y_axis, _encoding_y1;
|
|
1833
|
+
const unitSpecs = normalizeSpec(spec);
|
|
1834
|
+
if (unitSpecs.length === 0) {
|
|
1835
|
+
return {};
|
|
1836
|
+
}
|
|
1837
|
+
const primarySpec = unitSpecs[0];
|
|
1838
|
+
const encoding = primarySpec.encoding || {};
|
|
1839
|
+
// Extract chart title
|
|
1840
|
+
const chartTitle = typeof spec.title === 'string' ? spec.title : (_spec_title = spec.title) === null || _spec_title === void 0 ? void 0 : _spec_title.text;
|
|
1841
|
+
// Extract title styles if title is an object
|
|
1842
|
+
let titleStyles;
|
|
1843
|
+
if (typeof spec.title === 'object' && spec.title !== null) {
|
|
1844
|
+
const titleObj = spec.title;
|
|
1845
|
+
// Build titleFont object if any font properties are present
|
|
1846
|
+
const titleFont = {};
|
|
1847
|
+
if (titleObj.font) {
|
|
1848
|
+
titleFont.family = titleObj.font;
|
|
1849
|
+
}
|
|
1850
|
+
if (titleObj.fontSize) {
|
|
1851
|
+
titleFont.size = titleObj.fontSize;
|
|
1852
|
+
}
|
|
1853
|
+
if (titleObj.fontWeight) {
|
|
1854
|
+
// Convert string weights to numbers (Font interface expects number)
|
|
1855
|
+
const weight = titleObj.fontWeight;
|
|
1856
|
+
if (typeof weight === 'string') {
|
|
1857
|
+
const weightMap = {
|
|
1858
|
+
normal: 400,
|
|
1859
|
+
bold: 700,
|
|
1860
|
+
lighter: 300,
|
|
1861
|
+
bolder: 600
|
|
1862
|
+
};
|
|
1863
|
+
titleFont.weight = weightMap[weight.toLowerCase()] || 400;
|
|
1864
|
+
} else {
|
|
1865
|
+
titleFont.weight = weight;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (titleObj.color) {
|
|
1869
|
+
titleFont.color = titleObj.color;
|
|
1870
|
+
}
|
|
1871
|
+
// Map Vega-Lite anchor values to TitleStyles anchor values
|
|
1872
|
+
const anchorMap = {
|
|
1873
|
+
start: 'left',
|
|
1874
|
+
middle: 'center',
|
|
1875
|
+
end: 'right'
|
|
1876
|
+
};
|
|
1877
|
+
titleStyles = {
|
|
1878
|
+
...Object.keys(titleFont).length > 0 ? {
|
|
1879
|
+
titleFont
|
|
1880
|
+
} : {},
|
|
1881
|
+
...titleObj.anchor && anchorMap[titleObj.anchor] ? {
|
|
1882
|
+
titleXAnchor: anchorMap[titleObj.anchor]
|
|
1883
|
+
} : {},
|
|
1884
|
+
...titleObj.offset !== undefined || titleObj.subtitlePadding !== undefined ? {
|
|
1885
|
+
titlePad: {
|
|
1886
|
+
t: titleObj.offset,
|
|
1887
|
+
b: titleObj.subtitlePadding
|
|
1888
|
+
}
|
|
1889
|
+
} : {}
|
|
1890
|
+
};
|
|
1891
|
+
// Only include titleStyles if it has properties
|
|
1892
|
+
if (Object.keys(titleStyles).length === 0) {
|
|
1893
|
+
titleStyles = undefined;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
var _encoding_x_title, _ref, _encoding_y_title, _ref1;
|
|
1897
|
+
return {
|
|
1898
|
+
chartTitle,
|
|
1899
|
+
xAxisTitle: (_ref = (_encoding_x_title = (_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.title) !== null && _encoding_x_title !== void 0 ? _encoding_x_title : (_encoding_x1 = encoding.x) === null || _encoding_x1 === void 0 ? void 0 : (_encoding_x_axis = _encoding_x1.axis) === null || _encoding_x_axis === void 0 ? void 0 : _encoding_x_axis.title) !== null && _ref !== void 0 ? _ref : undefined,
|
|
1900
|
+
yAxisTitle: (_ref1 = (_encoding_y_title = (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.title) !== null && _encoding_y_title !== void 0 ? _encoding_y_title : (_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : (_encoding_y_axis = _encoding_y1.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.title) !== null && _ref1 !== void 0 ? _ref1 : undefined,
|
|
1901
|
+
...titleStyles ? {
|
|
1902
|
+
titleStyles
|
|
1903
|
+
} : {}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function transformVegaLiteToVerticalBarChartProps(spec, colorMap, isDarkTheme) {
|
|
1907
|
+
var _encoding_color, _encoding_y_axis, _encoding_y, _barData_, _encoding_color_legend, _encoding_color1;
|
|
1908
|
+
// Initialize transformation context
|
|
1909
|
+
const { dataValues, encoding, markProps } = initializeTransformContext(spec);
|
|
1910
|
+
// Extract field names and aggregates
|
|
1911
|
+
const { xField, yField, colorField, yAggregate } = extractEncodingFields(encoding);
|
|
1912
|
+
// Check if this is an aggregate bar chart
|
|
1913
|
+
// Aggregate can be: count (no field needed) or sum/mean/etc (with field)
|
|
1914
|
+
const isAggregate = !!yAggregate;
|
|
1915
|
+
if (!xField && !isAggregate) {
|
|
1916
|
+
throw new Error('VegaLiteSchemaAdapter: x encoding is required for bar charts');
|
|
1917
|
+
}
|
|
1918
|
+
// For aggregate charts, compute aggregated data
|
|
1919
|
+
let aggregatedData;
|
|
1920
|
+
if (isAggregate && xField) {
|
|
1921
|
+
aggregatedData = computeAggregateData(dataValues, xField, yField, yAggregate);
|
|
1922
|
+
}
|
|
1923
|
+
// Validate data and encodings (skip for aggregate charts)
|
|
1924
|
+
if (!isAggregate && xField && yField) {
|
|
1925
|
+
var _encoding_x, _encoding_y1;
|
|
1926
|
+
validateXYEncodings(dataValues, xField, yField, (_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.type, (_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.type, 'VerticalBarChart', encoding);
|
|
1927
|
+
}
|
|
1928
|
+
// Extract color configuration
|
|
1929
|
+
const { colorScheme, colorRange } = extractColorConfig(encoding);
|
|
1930
|
+
const colorValue = (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.value;
|
|
1931
|
+
const barData = [];
|
|
1932
|
+
const colorIndex = new Map();
|
|
1933
|
+
let currentColorIndex = 0;
|
|
1934
|
+
// When there's no color field, all bars share a single legend
|
|
1935
|
+
const useSingleLegendForAggregate = !colorField;
|
|
1936
|
+
if (aggregatedData) {
|
|
1937
|
+
// Use aggregated data
|
|
1938
|
+
aggregatedData.forEach(({ category, value })=>{
|
|
1939
|
+
const legend = useSingleLegendForAggregate ? 'Bar' : String(category);
|
|
1940
|
+
if (!colorIndex.has(legend)) {
|
|
1941
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
1942
|
+
}
|
|
1943
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
1944
|
+
barData.push({
|
|
1945
|
+
x: category,
|
|
1946
|
+
y: value,
|
|
1947
|
+
legend,
|
|
1948
|
+
color
|
|
1949
|
+
});
|
|
1950
|
+
});
|
|
1951
|
+
} else if (xField && yField) {
|
|
1952
|
+
var _dataValues_find;
|
|
1953
|
+
// Check if y values are numeric; if not, fall back to count aggregation
|
|
1954
|
+
const firstYValue = (_dataValues_find = dataValues.find((r)=>r[yField] !== undefined)) === null || _dataValues_find === void 0 ? void 0 : _dataValues_find[yField];
|
|
1955
|
+
const yIsNumeric = typeof firstYValue === 'number';
|
|
1956
|
+
if (!yIsNumeric) {
|
|
1957
|
+
// y values are non-numeric: compute count per x category
|
|
1958
|
+
const counts = countByCategory(dataValues, xField, undefined, '');
|
|
1959
|
+
counts.forEach((legendMap, xKey)=>{
|
|
1960
|
+
// No color grouping - each xKey gets one bar; use xKey as legend
|
|
1961
|
+
const totalCount = Array.from(legendMap.values()).reduce((a, b)=>a + b, 0);
|
|
1962
|
+
const legend = xKey;
|
|
1963
|
+
if (!colorIndex.has(legend)) {
|
|
1964
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
1965
|
+
}
|
|
1966
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
1967
|
+
barData.push({
|
|
1968
|
+
x: xKey,
|
|
1969
|
+
y: totalCount,
|
|
1970
|
+
legend,
|
|
1971
|
+
color
|
|
1972
|
+
});
|
|
1973
|
+
});
|
|
1974
|
+
} else {
|
|
1975
|
+
var _encoding_y_axis1, _encoding_y2;
|
|
1976
|
+
// When there's no color field encoding, use a single legend name for all bars
|
|
1977
|
+
// This ensures: uniform bar color, single legend entry, no tooltip duplication
|
|
1978
|
+
const useSingleLegend = !colorField;
|
|
1979
|
+
// Create value formatter for bar data labels
|
|
1980
|
+
const yFormatter = createValueFormatter((_encoding_y2 = encoding.y) === null || _encoding_y2 === void 0 ? void 0 : (_encoding_y_axis1 = _encoding_y2.axis) === null || _encoding_y_axis1 === void 0 ? void 0 : _encoding_y_axis1.format);
|
|
1981
|
+
// Use raw data (normal numeric y values)
|
|
1982
|
+
dataValues.forEach((row)=>{
|
|
1983
|
+
const xValue = row[xField];
|
|
1984
|
+
const yValue = row[yField];
|
|
1985
|
+
// Use chart-utilities validation
|
|
1986
|
+
if ((0, _chartutilities.isInvalidValue)(xValue) || (0, _chartutilities.isInvalidValue)(yValue) || typeof yValue !== 'number') {
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
const legend = colorField && row[colorField] !== undefined ? String(row[colorField]) : useSingleLegend ? 'Bar' : String(xValue);
|
|
1990
|
+
if (!colorIndex.has(legend)) {
|
|
1991
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
1992
|
+
}
|
|
1993
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
1994
|
+
// For bar charts, x-axis values are treated as categories (even if numeric)
|
|
1995
|
+
// Convert to string to ensure consistent categorical positioning
|
|
1996
|
+
const xCategory = typeof xValue === 'number' ? String(xValue) : xValue;
|
|
1997
|
+
barData.push({
|
|
1998
|
+
x: xCategory,
|
|
1999
|
+
y: yValue,
|
|
2000
|
+
legend,
|
|
2001
|
+
color,
|
|
2002
|
+
...yFormatter && {
|
|
2003
|
+
yAxisCalloutData: yFormatter(yValue),
|
|
2004
|
+
barLabel: yFormatter(yValue)
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const titles = getVegaLiteTitles(spec);
|
|
2011
|
+
// Extract axis category ordering
|
|
2012
|
+
const categoryOrderProps = extractAxisCategoryOrderProps(encoding);
|
|
2013
|
+
// Extract tick configuration
|
|
2014
|
+
const tickConfig = extractTickConfig(spec);
|
|
2015
|
+
// Extract y-axis formatting and scale props
|
|
2016
|
+
const yAxisTickFormat = (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : (_encoding_y_axis = _encoding_y.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.format;
|
|
2017
|
+
const { yMinValue, yMaxValue } = extractYMinMax(encoding, dataValues);
|
|
2018
|
+
const yAxisType = extractYAxisType(encoding);
|
|
2019
|
+
// Compute truncation based on number of unique x-axis categories
|
|
2020
|
+
const uniqueXCount = new Set(barData.map((d)=>String(d.x))).size;
|
|
2021
|
+
const barTruncateChars = uniqueXCount > 20 ? 6 : uniqueXCount > 10 ? 10 : DEFAULT_TRUNCATE_CHARS;
|
|
2022
|
+
var _encoding_color_legend_disable;
|
|
2023
|
+
const result = {
|
|
2024
|
+
data: barData,
|
|
2025
|
+
chartTitle: titles.chartTitle,
|
|
2026
|
+
xAxisTitle: titles.xAxisTitle,
|
|
2027
|
+
yAxisTitle: titles.yAxisTitle,
|
|
2028
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
2029
|
+
roundCorners: true,
|
|
2030
|
+
wrapXAxisLables: typeof ((_barData_ = barData[0]) === null || _barData_ === void 0 ? void 0 : _barData_.x) === 'string',
|
|
2031
|
+
hideTickOverlap: true,
|
|
2032
|
+
noOfCharsToTruncate: barTruncateChars,
|
|
2033
|
+
xAxis: {
|
|
2034
|
+
tickLayout: 'auto'
|
|
2035
|
+
},
|
|
2036
|
+
...yAxisTickFormat && {
|
|
2037
|
+
yAxisTickFormat
|
|
2038
|
+
},
|
|
2039
|
+
...yMinValue !== undefined && {
|
|
2040
|
+
yMinValue
|
|
2041
|
+
},
|
|
2042
|
+
...yMaxValue !== undefined && {
|
|
2043
|
+
yMaxValue
|
|
2044
|
+
},
|
|
2045
|
+
...yAxisType && {
|
|
2046
|
+
yScaleType: yAxisType
|
|
2047
|
+
},
|
|
2048
|
+
...categoryOrderProps,
|
|
2049
|
+
// Hide legend for single-series bar charts (no color encoding) to avoid showing "Bar" legend
|
|
2050
|
+
hideLegend: !colorField ? true : (_encoding_color_legend_disable = (_encoding_color1 = encoding.color) === null || _encoding_color1 === void 0 ? void 0 : (_encoding_color_legend = _encoding_color1.legend) === null || _encoding_color_legend === void 0 ? void 0 : _encoding_color_legend.disable) !== null && _encoding_color_legend_disable !== void 0 ? _encoding_color_legend_disable : false
|
|
2051
|
+
};
|
|
2052
|
+
if (tickConfig.tickValues) {
|
|
2053
|
+
result.tickValues = tickConfig.tickValues;
|
|
2054
|
+
}
|
|
2055
|
+
if (tickConfig.xAxisTickCount) {
|
|
2056
|
+
result.xAxisTickCount = tickConfig.xAxisTickCount;
|
|
2057
|
+
}
|
|
2058
|
+
return result;
|
|
2059
|
+
}
|
|
2060
|
+
function transformVegaLiteToVerticalStackedBarChartProps(spec, colorMap, isDarkTheme) {
|
|
2061
|
+
var _encoding_color, _encoding_y_axis, _encoding_y, _encoding_color_legend, _encoding_color1, _chartData_;
|
|
2062
|
+
// Initialize transformation context (skip warnings as we handle layered spec differently)
|
|
2063
|
+
const { unitSpecs } = initializeTransformContext(spec);
|
|
2064
|
+
// Separate bar, line, and rule specs from layered specifications
|
|
2065
|
+
const barSpecs = unitSpecs.filter((s)=>getMarkType(s.mark) === 'bar');
|
|
2066
|
+
const lineSpecs = unitSpecs.filter((s)=>{
|
|
2067
|
+
const mark = getMarkType(s.mark);
|
|
2068
|
+
return mark === 'line' || mark === 'point';
|
|
2069
|
+
});
|
|
2070
|
+
const ruleSpecs = unitSpecs.filter((s)=>getMarkType(s.mark) === 'rule');
|
|
2071
|
+
// Use bar specs if available, otherwise fall back to first unit spec
|
|
2072
|
+
const primarySpec = barSpecs.length > 0 ? barSpecs[0] : unitSpecs[0];
|
|
2073
|
+
const rawDataValues = extractDataValues(primarySpec.data);
|
|
2074
|
+
// Apply transforms from both top-level spec and primary spec
|
|
2075
|
+
let dataValues = applyTransforms(rawDataValues, spec.transform);
|
|
2076
|
+
dataValues = applyTransforms(dataValues, primarySpec.transform);
|
|
2077
|
+
const encoding = primarySpec.encoding || {};
|
|
2078
|
+
const markProps = getMarkProperties(primarySpec.mark);
|
|
2079
|
+
// Extract field names and aggregates
|
|
2080
|
+
const { xField, yField, colorField, yAggregate } = extractEncodingFields(encoding);
|
|
2081
|
+
const colorValue = (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.value; // Static color value
|
|
2082
|
+
// Support aggregate encodings (e.g., count, sum)
|
|
2083
|
+
const isAggregate = !!yAggregate;
|
|
2084
|
+
if (!xField) {
|
|
2085
|
+
throw new Error('VegaLiteSchemaAdapter: x encoding is required for stacked bar charts');
|
|
2086
|
+
}
|
|
2087
|
+
// For aggregate charts, compute aggregated data
|
|
2088
|
+
let aggregatedData;
|
|
2089
|
+
if (isAggregate) {
|
|
2090
|
+
aggregatedData = computeAggregateData(dataValues, xField, yField, yAggregate);
|
|
2091
|
+
} else if (!yField) {
|
|
2092
|
+
throw new Error('VegaLiteSchemaAdapter: y encoding is required for stacked bar charts');
|
|
2093
|
+
}
|
|
2094
|
+
// Extract color configuration
|
|
2095
|
+
const { colorScheme, colorRange } = extractColorConfig(encoding);
|
|
2096
|
+
// Group data by x value, then by color (stack)
|
|
2097
|
+
const mapXToDataPoints = {};
|
|
2098
|
+
const colorIndex = new Map();
|
|
2099
|
+
let currentColorIndex = 0;
|
|
2100
|
+
if (aggregatedData) {
|
|
2101
|
+
// Use aggregated data
|
|
2102
|
+
aggregatedData.forEach(({ category, value })=>{
|
|
2103
|
+
const xKey = String(category);
|
|
2104
|
+
const legend = 'Bar';
|
|
2105
|
+
if (!mapXToDataPoints[xKey]) {
|
|
2106
|
+
mapXToDataPoints[xKey] = {
|
|
2107
|
+
xAxisPoint: category,
|
|
2108
|
+
chartData: [],
|
|
2109
|
+
lineData: []
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
if (!colorIndex.has(legend)) {
|
|
2113
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2114
|
+
}
|
|
2115
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
2116
|
+
mapXToDataPoints[xKey].chartData.push({
|
|
2117
|
+
legend,
|
|
2118
|
+
data: value,
|
|
2119
|
+
color
|
|
2120
|
+
});
|
|
2121
|
+
});
|
|
2122
|
+
} else {
|
|
2123
|
+
var _dataValues_find;
|
|
2124
|
+
// Check if y values are actually numeric; if not, fall back to count aggregation
|
|
2125
|
+
const firstYValue = (_dataValues_find = dataValues.find((r)=>r[yField] !== undefined)) === null || _dataValues_find === void 0 ? void 0 : _dataValues_find[yField];
|
|
2126
|
+
const yIsNumeric = typeof firstYValue === 'number';
|
|
2127
|
+
if (!yIsNumeric && yField) {
|
|
2128
|
+
// y values are non-numeric (e.g., strings after auto-correction from quantitative to nominal)
|
|
2129
|
+
// Fall back to count aggregation: count rows per x category and color
|
|
2130
|
+
const counts = countByCategory(dataValues, xField, colorField, 'Bar');
|
|
2131
|
+
counts.forEach((legendMap, xKey)=>{
|
|
2132
|
+
mapXToDataPoints[xKey] = {
|
|
2133
|
+
xAxisPoint: xKey,
|
|
2134
|
+
chartData: [],
|
|
2135
|
+
lineData: []
|
|
2136
|
+
};
|
|
2137
|
+
legendMap.forEach((count, legend)=>{
|
|
2138
|
+
if (!colorIndex.has(legend)) {
|
|
2139
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2140
|
+
}
|
|
2141
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
2142
|
+
mapXToDataPoints[xKey].chartData.push({
|
|
2143
|
+
legend,
|
|
2144
|
+
data: count,
|
|
2145
|
+
color
|
|
2146
|
+
});
|
|
2147
|
+
});
|
|
2148
|
+
});
|
|
2149
|
+
} else {
|
|
2150
|
+
// Process bar data (normal numeric y values)
|
|
2151
|
+
dataValues.forEach((row)=>{
|
|
2152
|
+
var _encoding_y_axis, _encoding_y;
|
|
2153
|
+
const xValue = row[xField];
|
|
2154
|
+
const yValue = row[yField];
|
|
2155
|
+
const stackValue = colorField ? row[colorField] : 'Bar'; // Default legend if no color field
|
|
2156
|
+
if ((0, _chartutilities.isInvalidValue)(xValue) || (0, _chartutilities.isInvalidValue)(yValue) || typeof yValue !== 'number') {
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const xKey = String(xValue);
|
|
2160
|
+
const legend = stackValue !== undefined ? String(stackValue) : 'Bar';
|
|
2161
|
+
if (!mapXToDataPoints[xKey]) {
|
|
2162
|
+
// For bar charts, x-axis values are treated as categories (even if numeric)
|
|
2163
|
+
const xCategory = typeof xValue === 'number' ? String(xValue) : xValue;
|
|
2164
|
+
mapXToDataPoints[xKey] = {
|
|
2165
|
+
xAxisPoint: xCategory,
|
|
2166
|
+
chartData: [],
|
|
2167
|
+
lineData: []
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
if (!colorIndex.has(legend)) {
|
|
2171
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2172
|
+
}
|
|
2173
|
+
// Use static color if provided, otherwise use color scheme/scale
|
|
2174
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
2175
|
+
const stackYFormatter = createValueFormatter((_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : (_encoding_y_axis = _encoding_y.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.format);
|
|
2176
|
+
mapXToDataPoints[xKey].chartData.push({
|
|
2177
|
+
legend,
|
|
2178
|
+
data: yValue,
|
|
2179
|
+
color,
|
|
2180
|
+
...stackYFormatter && {
|
|
2181
|
+
yAxisCalloutData: stackYFormatter(yValue),
|
|
2182
|
+
barLabel: stackYFormatter(yValue)
|
|
2183
|
+
}
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
} // end else (non-aggregate)
|
|
2188
|
+
// Process line data from additional layers (if any)
|
|
2189
|
+
lineSpecs.forEach((lineSpec, lineIndex)=>{
|
|
2190
|
+
var _lineEncoding_x, _lineEncoding_y, _lineEncoding_color;
|
|
2191
|
+
let lineDataValues = extractDataValues(lineSpec.data);
|
|
2192
|
+
// Apply transforms from both top-level spec and line spec
|
|
2193
|
+
lineDataValues = applyTransforms(lineDataValues, spec.transform);
|
|
2194
|
+
lineDataValues = applyTransforms(lineDataValues, lineSpec.transform);
|
|
2195
|
+
const lineEncoding = lineSpec.encoding || {};
|
|
2196
|
+
const lineMarkProps = getMarkProperties(lineSpec.mark);
|
|
2197
|
+
const lineXField = (_lineEncoding_x = lineEncoding.x) === null || _lineEncoding_x === void 0 ? void 0 : _lineEncoding_x.field;
|
|
2198
|
+
const lineYField = (_lineEncoding_y = lineEncoding.y) === null || _lineEncoding_y === void 0 ? void 0 : _lineEncoding_y.field;
|
|
2199
|
+
const lineColorField = (_lineEncoding_color = lineEncoding.color) === null || _lineEncoding_color === void 0 ? void 0 : _lineEncoding_color.field;
|
|
2200
|
+
if (!lineXField || !lineYField) {
|
|
2201
|
+
return; // Skip if required fields are missing
|
|
2202
|
+
}
|
|
2203
|
+
const lineLegendBase = lineColorField ? 'Line' : `Line ${lineIndex + 1}`;
|
|
2204
|
+
lineDataValues.forEach((row)=>{
|
|
2205
|
+
var _spec_resolve_scale, _spec_resolve;
|
|
2206
|
+
const xValue = row[lineXField];
|
|
2207
|
+
const yValue = row[lineYField];
|
|
2208
|
+
if ((0, _chartutilities.isInvalidValue)(xValue) || (0, _chartutilities.isInvalidValue)(yValue)) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const xKey = String(xValue);
|
|
2212
|
+
const lineLegend = lineColorField && row[lineColorField] !== undefined ? String(row[lineColorField]) : lineLegendBase;
|
|
2213
|
+
// Ensure x-axis point exists
|
|
2214
|
+
if (!mapXToDataPoints[xKey]) {
|
|
2215
|
+
mapXToDataPoints[xKey] = {
|
|
2216
|
+
xAxisPoint: xValue,
|
|
2217
|
+
chartData: [],
|
|
2218
|
+
lineData: []
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
// Determine line color
|
|
2222
|
+
if (!colorIndex.has(lineLegend)) {
|
|
2223
|
+
colorIndex.set(lineLegend, currentColorIndex++);
|
|
2224
|
+
}
|
|
2225
|
+
let lineColor;
|
|
2226
|
+
if (lineMarkProps.color) {
|
|
2227
|
+
lineColor = lineMarkProps.color;
|
|
2228
|
+
} else {
|
|
2229
|
+
// Use lineLegend for consistent color assignment
|
|
2230
|
+
lineColor = resolveColor(lineLegend, colorIndex.get(lineLegend), undefined, undefined, colorMap, undefined, undefined, isDarkTheme);
|
|
2231
|
+
}
|
|
2232
|
+
// Determine if this line should use secondary Y-axis
|
|
2233
|
+
// Check if spec has independent Y scales AND line uses different Y field than bars
|
|
2234
|
+
const hasIndependentYScales = ((_spec_resolve = spec.resolve) === null || _spec_resolve === void 0 ? void 0 : (_spec_resolve_scale = _spec_resolve.scale) === null || _spec_resolve_scale === void 0 ? void 0 : _spec_resolve_scale.y) === 'independent';
|
|
2235
|
+
const useSecondaryYScale = hasIndependentYScales && lineYField !== yField;
|
|
2236
|
+
const lineData = {
|
|
2237
|
+
y: yValue,
|
|
2238
|
+
color: lineColor,
|
|
2239
|
+
legend: lineLegend,
|
|
2240
|
+
legendShape: 'triangle',
|
|
2241
|
+
data: typeof yValue === 'number' ? yValue : undefined,
|
|
2242
|
+
useSecondaryYScale
|
|
2243
|
+
};
|
|
2244
|
+
// Add line options if available
|
|
2245
|
+
if (lineMarkProps.strokeWidth || lineMarkProps.strokeDash) {
|
|
2246
|
+
lineData.lineOptions = {
|
|
2247
|
+
...lineMarkProps.strokeWidth && {
|
|
2248
|
+
strokeWidth: lineMarkProps.strokeWidth
|
|
2249
|
+
},
|
|
2250
|
+
...lineMarkProps.strokeDash && {
|
|
2251
|
+
strokeDasharray: lineMarkProps.strokeDash.join(' ')
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
mapXToDataPoints[xKey].lineData.push(lineData);
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
// Process rule specs as horizontal reference lines
|
|
2259
|
+
// Each rule with a constant y-value becomes a flat line across all x-axis points
|
|
2260
|
+
ruleSpecs.forEach((ruleSpec, ruleIndex)=>{
|
|
2261
|
+
var _ruleEncoding_y, _ruleEncoding_y1;
|
|
2262
|
+
const ruleEncoding = ruleSpec.encoding || {};
|
|
2263
|
+
const ruleMarkProps = getMarkProperties(ruleSpec.mark);
|
|
2264
|
+
var _ruleEncoding_y_datum;
|
|
2265
|
+
const yDatum = (_ruleEncoding_y_datum = (_ruleEncoding_y = ruleEncoding.y) === null || _ruleEncoding_y === void 0 ? void 0 : _ruleEncoding_y.datum) !== null && _ruleEncoding_y_datum !== void 0 ? _ruleEncoding_y_datum : (_ruleEncoding_y1 = ruleEncoding.y) === null || _ruleEncoding_y1 === void 0 ? void 0 : _ruleEncoding_y1.value;
|
|
2266
|
+
if (yDatum !== undefined) {
|
|
2267
|
+
var _textSpec_encoding_text, _textSpec_encoding, _textSpec_encoding_text1, _textSpec_encoding1;
|
|
2268
|
+
const ruleLegend = `Reference_${ruleIndex}`;
|
|
2269
|
+
const ruleColor = ruleMarkProps.color || '#d62728';
|
|
2270
|
+
if (!colorIndex.has(ruleLegend)) {
|
|
2271
|
+
colorIndex.set(ruleLegend, currentColorIndex++);
|
|
2272
|
+
}
|
|
2273
|
+
const lineOptions = {};
|
|
2274
|
+
if (ruleMarkProps.strokeDash) {
|
|
2275
|
+
lineOptions.strokeDasharray = ruleMarkProps.strokeDash.join(' ');
|
|
2276
|
+
}
|
|
2277
|
+
if (ruleMarkProps.strokeWidth) {
|
|
2278
|
+
lineOptions.strokeWidth = ruleMarkProps.strokeWidth;
|
|
2279
|
+
}
|
|
2280
|
+
// Look for companion text annotation at the same y-value
|
|
2281
|
+
const textSpec = unitSpecs.find((s, i)=>{
|
|
2282
|
+
var _s_encoding;
|
|
2283
|
+
var _s_encoding_y_datum;
|
|
2284
|
+
return getMarkType(s.mark) === 'text' && ((_s_encoding = s.encoding) === null || _s_encoding === void 0 ? void 0 : _s_encoding.y) && ((_s_encoding_y_datum = s.encoding.y.datum) !== null && _s_encoding_y_datum !== void 0 ? _s_encoding_y_datum : s.encoding.y.value) === yDatum;
|
|
2285
|
+
});
|
|
2286
|
+
const ruleText = textSpec ? String(((_textSpec_encoding = textSpec.encoding) === null || _textSpec_encoding === void 0 ? void 0 : (_textSpec_encoding_text = _textSpec_encoding.text) === null || _textSpec_encoding_text === void 0 ? void 0 : _textSpec_encoding_text.datum) || ((_textSpec_encoding1 = textSpec.encoding) === null || _textSpec_encoding1 === void 0 ? void 0 : (_textSpec_encoding_text1 = _textSpec_encoding1.text) === null || _textSpec_encoding_text1 === void 0 ? void 0 : _textSpec_encoding_text1.value) || yDatum) : String(yDatum);
|
|
2287
|
+
// Add the constant y-value line to every x-axis point
|
|
2288
|
+
Object.keys(mapXToDataPoints).forEach((xKey)=>{
|
|
2289
|
+
mapXToDataPoints[xKey].lineData.push({
|
|
2290
|
+
y: yDatum,
|
|
2291
|
+
legend: ruleText,
|
|
2292
|
+
color: ruleColor,
|
|
2293
|
+
...Object.keys(lineOptions).length > 0 && {
|
|
2294
|
+
lineOptions
|
|
2295
|
+
},
|
|
2296
|
+
useSecondaryYScale: false
|
|
2297
|
+
});
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
const chartData = Object.values(mapXToDataPoints);
|
|
2302
|
+
const titles = getVegaLiteTitles(spec);
|
|
2303
|
+
// Check if we have secondary Y-axis data
|
|
2304
|
+
const hasSecondaryYAxis = chartData.some((point)=>{
|
|
2305
|
+
var _point_lineData;
|
|
2306
|
+
return (_point_lineData = point.lineData) === null || _point_lineData === void 0 ? void 0 : _point_lineData.some((line)=>line.useSecondaryYScale);
|
|
2307
|
+
});
|
|
2308
|
+
// Extract secondary Y-axis properties from line layers
|
|
2309
|
+
let secondaryYAxisProps = {};
|
|
2310
|
+
if (hasSecondaryYAxis && lineSpecs.length > 0) {
|
|
2311
|
+
var _lineEncoding_y;
|
|
2312
|
+
const lineSpec = lineSpecs[0];
|
|
2313
|
+
const lineEncoding = lineSpec.encoding || {};
|
|
2314
|
+
const lineYAxis = (_lineEncoding_y = lineEncoding.y) === null || _lineEncoding_y === void 0 ? void 0 : _lineEncoding_y.axis;
|
|
2315
|
+
if (lineYAxis === null || lineYAxis === void 0 ? void 0 : lineYAxis.title) {
|
|
2316
|
+
secondaryYAxisProps.secondaryYAxistitle = lineYAxis.title;
|
|
2317
|
+
}
|
|
2318
|
+
// Compute secondary Y scale domain from line data values
|
|
2319
|
+
const allLineYValues = [];
|
|
2320
|
+
chartData.forEach((point)=>{
|
|
2321
|
+
var _point_lineData;
|
|
2322
|
+
(_point_lineData = point.lineData) === null || _point_lineData === void 0 ? void 0 : _point_lineData.forEach((line)=>{
|
|
2323
|
+
if (line.useSecondaryYScale && typeof line.y === 'number') {
|
|
2324
|
+
allLineYValues.push(line.y);
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
});
|
|
2328
|
+
if (allLineYValues.length > 0) {
|
|
2329
|
+
var _lineEncoding_y_scale, _lineEncoding_y1;
|
|
2330
|
+
// Use explicit domain from line encoding if available, otherwise compute from data
|
|
2331
|
+
const lineDomain = (_lineEncoding_y1 = lineEncoding.y) === null || _lineEncoding_y1 === void 0 ? void 0 : (_lineEncoding_y_scale = _lineEncoding_y1.scale) === null || _lineEncoding_y_scale === void 0 ? void 0 : _lineEncoding_y_scale.domain;
|
|
2332
|
+
var _d3Min;
|
|
2333
|
+
const secYMin = Array.isArray(lineDomain) ? lineDomain[0] : (_d3Min = (0, _d3array.min)(allLineYValues)) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
2334
|
+
var _d3Max;
|
|
2335
|
+
const secYMax = Array.isArray(lineDomain) ? lineDomain[1] : (_d3Max = (0, _d3array.max)(allLineYValues)) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
2336
|
+
secondaryYAxisProps.secondaryYScaleOptions = {
|
|
2337
|
+
yMinValue: secYMin,
|
|
2338
|
+
yMaxValue: secYMax
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
// Check for log scale on primary Y-axis
|
|
2343
|
+
const yAxisType = extractYAxisType(encoding);
|
|
2344
|
+
// Extract y-axis formatting and domain props
|
|
2345
|
+
const yAxisTickFormat = (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : (_encoding_y_axis = _encoding_y.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.format;
|
|
2346
|
+
const { yMinValue, yMaxValue } = extractYMinMax(encoding, dataValues);
|
|
2347
|
+
// Extract axis category ordering
|
|
2348
|
+
const categoryOrderProps = extractAxisCategoryOrderProps(encoding);
|
|
2349
|
+
var _spec_height, _encoding_color_legend_disable;
|
|
2350
|
+
return {
|
|
2351
|
+
data: chartData,
|
|
2352
|
+
chartTitle: titles.chartTitle,
|
|
2353
|
+
xAxisTitle: titles.xAxisTitle,
|
|
2354
|
+
yAxisTitle: titles.yAxisTitle,
|
|
2355
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
2356
|
+
width: spec.width,
|
|
2357
|
+
height: (_spec_height = spec.height) !== null && _spec_height !== void 0 ? _spec_height : DEFAULT_CHART_HEIGHT,
|
|
2358
|
+
hideLegend: (_encoding_color_legend_disable = (_encoding_color1 = encoding.color) === null || _encoding_color1 === void 0 ? void 0 : (_encoding_color_legend = _encoding_color1.legend) === null || _encoding_color_legend === void 0 ? void 0 : _encoding_color_legend.disable) !== null && _encoding_color_legend_disable !== void 0 ? _encoding_color_legend_disable : false,
|
|
2359
|
+
showYAxisLables: true,
|
|
2360
|
+
roundCorners: true,
|
|
2361
|
+
hideTickOverlap: true,
|
|
2362
|
+
barGapMax: 2,
|
|
2363
|
+
noOfCharsToTruncate: DEFAULT_TRUNCATE_CHARS,
|
|
2364
|
+
showYAxisLablesTooltip: true,
|
|
2365
|
+
wrapXAxisLables: typeof ((_chartData_ = chartData[0]) === null || _chartData_ === void 0 ? void 0 : _chartData_.xAxisPoint) === 'string',
|
|
2366
|
+
xAxis: {
|
|
2367
|
+
tickLayout: 'auto'
|
|
2368
|
+
},
|
|
2369
|
+
...yAxisTickFormat && {
|
|
2370
|
+
yAxisTickFormat
|
|
2371
|
+
},
|
|
2372
|
+
...yMinValue !== undefined && {
|
|
2373
|
+
yMinValue
|
|
2374
|
+
},
|
|
2375
|
+
...yMaxValue !== undefined && {
|
|
2376
|
+
yMaxValue
|
|
2377
|
+
},
|
|
2378
|
+
...yAxisType && {
|
|
2379
|
+
yScaleType: yAxisType
|
|
2380
|
+
},
|
|
2381
|
+
...secondaryYAxisProps,
|
|
2382
|
+
...categoryOrderProps
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
function transformVegaLiteToGroupedVerticalBarChartProps(spec, colorMap, isDarkTheme) {
|
|
2386
|
+
var _encoding_y_axis, _encoding_y;
|
|
2387
|
+
// Initialize transformation context
|
|
2388
|
+
const { dataValues, encoding } = initializeTransformContext(spec);
|
|
2389
|
+
// Extract field names
|
|
2390
|
+
const { xField, yField, colorField } = extractEncodingFields(encoding);
|
|
2391
|
+
if (!xField || !yField || !colorField) {
|
|
2392
|
+
throw new Error('VegaLiteSchemaAdapter: x, y, and color encodings are required for grouped bar charts');
|
|
2393
|
+
}
|
|
2394
|
+
// Extract color configuration
|
|
2395
|
+
const { colorScheme, colorRange } = extractColorConfig(encoding);
|
|
2396
|
+
// Group data by x value (name), then by color (series)
|
|
2397
|
+
const groupedData = {};
|
|
2398
|
+
const colorIndex = new Map();
|
|
2399
|
+
let currentColorIndex = 0;
|
|
2400
|
+
dataValues.forEach((row)=>{
|
|
2401
|
+
const xValue = row[xField];
|
|
2402
|
+
const yValue = row[yField];
|
|
2403
|
+
const groupValue = row[colorField];
|
|
2404
|
+
if ((0, _chartutilities.isInvalidValue)(xValue) || (0, _chartutilities.isInvalidValue)(yValue) || typeof yValue !== 'number' || (0, _chartutilities.isInvalidValue)(groupValue)) {
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const xKey = String(xValue);
|
|
2408
|
+
const legend = String(groupValue);
|
|
2409
|
+
if (!groupedData[xKey]) {
|
|
2410
|
+
groupedData[xKey] = {};
|
|
2411
|
+
}
|
|
2412
|
+
groupedData[xKey][legend] = yValue;
|
|
2413
|
+
if (!colorIndex.has(legend)) {
|
|
2414
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
// Convert to GroupedVerticalBarChartData format
|
|
2418
|
+
const chartData = Object.keys(groupedData).map((name)=>{
|
|
2419
|
+
const series = Object.keys(groupedData[name]).map((legend)=>({
|
|
2420
|
+
key: legend,
|
|
2421
|
+
data: groupedData[name][legend],
|
|
2422
|
+
legend,
|
|
2423
|
+
color: resolveColor(legend, colorIndex.get(legend), undefined, undefined, colorMap, colorScheme, colorRange, isDarkTheme)
|
|
2424
|
+
}));
|
|
2425
|
+
return {
|
|
2426
|
+
name,
|
|
2427
|
+
series
|
|
2428
|
+
};
|
|
2429
|
+
});
|
|
2430
|
+
const titles = getVegaLiteTitles(spec);
|
|
2431
|
+
// Extract y-axis formatting and scale props
|
|
2432
|
+
const yAxisTickFormat = (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : (_encoding_y_axis = _encoding_y.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.format;
|
|
2433
|
+
const { yMinValue, yMaxValue } = extractYMinMax(encoding, dataValues);
|
|
2434
|
+
const yAxisType = extractYAxisType(encoding);
|
|
2435
|
+
return {
|
|
2436
|
+
data: chartData,
|
|
2437
|
+
chartTitle: titles.chartTitle,
|
|
2438
|
+
xAxisTitle: titles.xAxisTitle,
|
|
2439
|
+
yAxisTitle: titles.yAxisTitle,
|
|
2440
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
2441
|
+
...yAxisTickFormat && {
|
|
2442
|
+
yAxisTickFormat
|
|
2443
|
+
},
|
|
2444
|
+
...yMinValue !== undefined && {
|
|
2445
|
+
yMinValue
|
|
2446
|
+
},
|
|
2447
|
+
...yMaxValue !== undefined && {
|
|
2448
|
+
yMaxValue
|
|
2449
|
+
},
|
|
2450
|
+
...yAxisType && {
|
|
2451
|
+
yScaleType: yAxisType
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
function transformVegaLiteToHorizontalBarChartProps(spec, colorMap, isDarkTheme) {
|
|
2456
|
+
var _encoding_color, _encoding_color_legend, _encoding_color1;
|
|
2457
|
+
// Initialize transformation context
|
|
2458
|
+
const { dataValues, encoding, markProps } = initializeTransformContext(spec);
|
|
2459
|
+
// Extract field names and aggregates
|
|
2460
|
+
const { xField, yField, colorField, xAggregate, x2Field } = extractEncodingFields(encoding);
|
|
2461
|
+
// Check if this is an aggregate bar chart
|
|
2462
|
+
// Aggregate can be: count (no field needed) or sum/mean/etc (with field)
|
|
2463
|
+
const isAggregate = !!xAggregate;
|
|
2464
|
+
if (!yField && !isAggregate) {
|
|
2465
|
+
throw new Error('VegaLiteSchemaAdapter: y encoding is required for horizontal bar charts');
|
|
2466
|
+
}
|
|
2467
|
+
// For aggregate charts, compute aggregated data
|
|
2468
|
+
let aggregatedData;
|
|
2469
|
+
if (isAggregate && yField) {
|
|
2470
|
+
aggregatedData = computeAggregateData(dataValues, yField, xField, xAggregate);
|
|
2471
|
+
}
|
|
2472
|
+
const colorValue = (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.value;
|
|
2473
|
+
const barData = [];
|
|
2474
|
+
const colorIndex = new Map();
|
|
2475
|
+
let currentColorIndex = 0;
|
|
2476
|
+
if (aggregatedData) {
|
|
2477
|
+
// Use aggregated data
|
|
2478
|
+
aggregatedData.forEach(({ category, value })=>{
|
|
2479
|
+
const legend = String(category);
|
|
2480
|
+
if (!colorIndex.has(legend)) {
|
|
2481
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2482
|
+
}
|
|
2483
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, undefined, undefined, isDarkTheme);
|
|
2484
|
+
barData.push({
|
|
2485
|
+
x: value,
|
|
2486
|
+
y: category,
|
|
2487
|
+
legend,
|
|
2488
|
+
color
|
|
2489
|
+
});
|
|
2490
|
+
});
|
|
2491
|
+
} else if (x2Field && xField && yField) {
|
|
2492
|
+
var _encoding_x;
|
|
2493
|
+
// Gantt chart: bar mark with x/x2 temporal range encoding
|
|
2494
|
+
const isXTemporal = ((_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.type) === 'temporal';
|
|
2495
|
+
dataValues.forEach((row)=>{
|
|
2496
|
+
const startVal = row[xField];
|
|
2497
|
+
const endVal = row[x2Field];
|
|
2498
|
+
const yValue = row[yField];
|
|
2499
|
+
if (startVal === undefined || endVal === undefined || yValue === undefined) {
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
let xNumeric;
|
|
2503
|
+
if (isXTemporal) {
|
|
2504
|
+
const startDate = new Date(startVal);
|
|
2505
|
+
const endDate = new Date(endVal);
|
|
2506
|
+
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
xNumeric = Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
2510
|
+
} else {
|
|
2511
|
+
xNumeric = Number(endVal) - Number(startVal);
|
|
2512
|
+
if (isNaN(xNumeric)) {
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const legend = colorField && row[colorField] !== undefined ? String(row[colorField]) : String(yValue);
|
|
2517
|
+
if (!colorIndex.has(legend)) {
|
|
2518
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2519
|
+
}
|
|
2520
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, undefined, undefined, isDarkTheme);
|
|
2521
|
+
barData.push({
|
|
2522
|
+
x: xNumeric,
|
|
2523
|
+
y: yValue,
|
|
2524
|
+
legend,
|
|
2525
|
+
color
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
2528
|
+
} else if (xField && yField) {
|
|
2529
|
+
// Use raw data
|
|
2530
|
+
dataValues.forEach((row)=>{
|
|
2531
|
+
const xValue = row[xField];
|
|
2532
|
+
const yValue = row[yField];
|
|
2533
|
+
if ((0, _chartutilities.isInvalidValue)(xValue) || (0, _chartutilities.isInvalidValue)(yValue) || typeof xValue !== 'number') {
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
// When no color field, use single legend to avoid tooltip duplication with y-axis labels
|
|
2537
|
+
const legend = colorField && row[colorField] !== undefined ? String(row[colorField]) : !colorField ? 'Bar' : String(yValue);
|
|
2538
|
+
if (!colorIndex.has(legend)) {
|
|
2539
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2540
|
+
}
|
|
2541
|
+
const color = resolveColor(legend, colorIndex.get(legend), colorValue, markProps.color, colorMap, undefined, undefined, isDarkTheme);
|
|
2542
|
+
barData.push({
|
|
2543
|
+
x: xValue,
|
|
2544
|
+
y: yValue,
|
|
2545
|
+
legend,
|
|
2546
|
+
color
|
|
2547
|
+
});
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
const titles = getVegaLiteTitles(spec);
|
|
2551
|
+
const annotations = extractAnnotations(spec);
|
|
2552
|
+
const tickConfig = extractTickConfig(spec);
|
|
2553
|
+
var _encoding_color_legend_disable;
|
|
2554
|
+
const result = {
|
|
2555
|
+
data: barData,
|
|
2556
|
+
chartTitle: titles.chartTitle,
|
|
2557
|
+
xAxisTitle: titles.xAxisTitle,
|
|
2558
|
+
yAxisTitle: titles.yAxisTitle,
|
|
2559
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
2560
|
+
// Hide legend for single-series horizontal bars (no color encoding)
|
|
2561
|
+
hideLegend: !colorField ? true : (_encoding_color_legend_disable = (_encoding_color1 = encoding.color) === null || _encoding_color1 === void 0 ? void 0 : (_encoding_color_legend = _encoding_color1.legend) === null || _encoding_color_legend === void 0 ? void 0 : _encoding_color_legend.disable) !== null && _encoding_color_legend_disable !== void 0 ? _encoding_color_legend_disable : false
|
|
2562
|
+
};
|
|
2563
|
+
if (annotations.length > 0) {
|
|
2564
|
+
result.annotations = annotations;
|
|
2565
|
+
}
|
|
2566
|
+
if (tickConfig.tickValues) {
|
|
2567
|
+
result.tickValues = tickConfig.tickValues;
|
|
2568
|
+
}
|
|
2569
|
+
if (tickConfig.xAxisTickCount) {
|
|
2570
|
+
result.xAxisTickCount = tickConfig.xAxisTickCount;
|
|
2571
|
+
}
|
|
2572
|
+
if (tickConfig.yAxisTickCount) {
|
|
2573
|
+
result.yAxisTickCount = tickConfig.yAxisTickCount;
|
|
2574
|
+
}
|
|
2575
|
+
return result;
|
|
2576
|
+
}
|
|
2577
|
+
function transformVegaLiteToAreaChartProps(spec, colorMap, isDarkTheme) {
|
|
2578
|
+
var _encoding_color, _encoding_y;
|
|
2579
|
+
// Area charts use the same structure as line charts in Fluent Charts
|
|
2580
|
+
// The only difference is the component renders with filled areas
|
|
2581
|
+
const lineChartProps = transformVegaLiteToLineChartProps(spec, colorMap, isDarkTheme);
|
|
2582
|
+
// Determine stacking mode based on Vega-Lite spec
|
|
2583
|
+
const unitSpecs = normalizeSpec(spec);
|
|
2584
|
+
// Use findPrimaryLineSpec to skip auxiliary layers (like rect for color fill bars)
|
|
2585
|
+
const primarySpec = findPrimaryLineSpec(unitSpecs);
|
|
2586
|
+
const encoding = (primarySpec === null || primarySpec === void 0 ? void 0 : primarySpec.encoding) || {};
|
|
2587
|
+
// Check if stacking is enabled
|
|
2588
|
+
// In Vega-Lite, area charts stack by default when color encoding is present
|
|
2589
|
+
// stack can be explicitly set to null to disable stacking
|
|
2590
|
+
const hasColorEncoding = !!((_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.field);
|
|
2591
|
+
const stackConfig = (_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.stack;
|
|
2592
|
+
const isStacked = stackConfig !== null && (stackConfig === 'zero' || hasColorEncoding);
|
|
2593
|
+
// Set mode: 'tozeroy' for single series, 'tonexty' for stacked
|
|
2594
|
+
const mode = isStacked ? 'tonexty' : 'tozeroy';
|
|
2595
|
+
return {
|
|
2596
|
+
...lineChartProps,
|
|
2597
|
+
mode
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
function transformVegaLiteToScatterChartProps(spec, colorMap, isDarkTheme) {
|
|
2601
|
+
var _encoding_x, _encoding_y, _encoding_y1, _encoding_y2, _encoding_y_axis, _encoding_y3, _encoding_color_legend, _encoding_color;
|
|
2602
|
+
// Initialize transformation context
|
|
2603
|
+
const { dataValues, encoding, markProps } = initializeTransformContext(spec);
|
|
2604
|
+
// Extract field names
|
|
2605
|
+
const { xField, yField, colorField, sizeField } = extractEncodingFields(encoding);
|
|
2606
|
+
if (!xField || !yField) {
|
|
2607
|
+
throw new Error('VegaLiteSchemaAdapter: Both x and y encodings are required for scatter charts');
|
|
2608
|
+
}
|
|
2609
|
+
const isXTemporal = ((_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.type) === 'temporal';
|
|
2610
|
+
const isYTemporal = ((_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.type) === 'temporal';
|
|
2611
|
+
// Check if y-values are strings (nominal/ordinal) and build ordinal mapping
|
|
2612
|
+
const yIsNominal = ((_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.type) === 'nominal' || ((_encoding_y2 = encoding.y) === null || _encoding_y2 === void 0 ? void 0 : _encoding_y2.type) === 'ordinal';
|
|
2613
|
+
const yOrdinalMap = new Map();
|
|
2614
|
+
const yOrdinalLabels = [];
|
|
2615
|
+
if (yIsNominal) {
|
|
2616
|
+
// Collect unique y-values in order
|
|
2617
|
+
dataValues.forEach((row)=>{
|
|
2618
|
+
const yVal = row[yField];
|
|
2619
|
+
if (yVal !== undefined) {
|
|
2620
|
+
const key = String(yVal);
|
|
2621
|
+
if (!yOrdinalMap.has(key)) {
|
|
2622
|
+
yOrdinalMap.set(key, yOrdinalMap.size);
|
|
2623
|
+
yOrdinalLabels.push(key);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
// Group data by series (color encoding)
|
|
2629
|
+
const groupedData = {};
|
|
2630
|
+
dataValues.forEach((row)=>{
|
|
2631
|
+
const seriesName = colorField && row[colorField] !== undefined ? String(row[colorField]) : 'default';
|
|
2632
|
+
if (!groupedData[seriesName]) {
|
|
2633
|
+
groupedData[seriesName] = [];
|
|
2634
|
+
}
|
|
2635
|
+
groupedData[seriesName].push(row);
|
|
2636
|
+
});
|
|
2637
|
+
const seriesNames = Object.keys(groupedData);
|
|
2638
|
+
const colorIndex = new Map();
|
|
2639
|
+
let currentColorIndex = 0;
|
|
2640
|
+
const chartData = seriesNames.map((seriesName, index)=>{
|
|
2641
|
+
var _encoding_color_scale, _encoding_color;
|
|
2642
|
+
if (!colorIndex.has(seriesName)) {
|
|
2643
|
+
colorIndex.set(seriesName, currentColorIndex++);
|
|
2644
|
+
}
|
|
2645
|
+
const seriesData = groupedData[seriesName];
|
|
2646
|
+
const points = seriesData.map((row)=>{
|
|
2647
|
+
const xValue = parseValue(row[xField], isXTemporal);
|
|
2648
|
+
const yValue = parseValue(row[yField], isYTemporal);
|
|
2649
|
+
const markerSize = sizeField && row[sizeField] !== undefined ? Number(row[sizeField]) : undefined;
|
|
2650
|
+
// Map nominal y-values to numeric indices
|
|
2651
|
+
let numericY;
|
|
2652
|
+
if (yIsNominal && typeof yValue === 'string') {
|
|
2653
|
+
var _yOrdinalMap_get;
|
|
2654
|
+
numericY = (_yOrdinalMap_get = yOrdinalMap.get(yValue)) !== null && _yOrdinalMap_get !== void 0 ? _yOrdinalMap_get : 0;
|
|
2655
|
+
} else {
|
|
2656
|
+
numericY = typeof yValue === 'number' ? yValue : 0;
|
|
2657
|
+
}
|
|
2658
|
+
return {
|
|
2659
|
+
x: typeof xValue === 'number' || xValue instanceof Date ? xValue : String(xValue),
|
|
2660
|
+
y: numericY,
|
|
2661
|
+
...markerSize !== undefined && {
|
|
2662
|
+
markerSize
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
});
|
|
2666
|
+
// Get color for this series
|
|
2667
|
+
const colorValue = colorField && ((_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : (_encoding_color_scale = _encoding_color.scale) === null || _encoding_color_scale === void 0 ? void 0 : _encoding_color_scale.range) && Array.isArray(encoding.color.scale.range) ? encoding.color.scale.range[index] : markProps.color;
|
|
2668
|
+
const color = typeof colorValue === 'string' ? colorValue : resolveColor(seriesName, colorIndex.get(seriesName), undefined, undefined, colorMap, undefined, undefined, isDarkTheme);
|
|
2669
|
+
return {
|
|
2670
|
+
legend: seriesName,
|
|
2671
|
+
data: points,
|
|
2672
|
+
color,
|
|
2673
|
+
legendShape: 'circle'
|
|
2674
|
+
};
|
|
2675
|
+
});
|
|
2676
|
+
const titles = getVegaLiteTitles(spec);
|
|
2677
|
+
const annotations = extractAnnotations(spec);
|
|
2678
|
+
const tickConfig = extractTickConfig(spec);
|
|
2679
|
+
// Check for log scale on Y-axis
|
|
2680
|
+
const yAxisType = extractYAxisType(encoding);
|
|
2681
|
+
// Extract y-axis formatting and domain props
|
|
2682
|
+
const yAxisTickFormat = (_encoding_y3 = encoding.y) === null || _encoding_y3 === void 0 ? void 0 : (_encoding_y_axis = _encoding_y3.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.format;
|
|
2683
|
+
const { yMinValue, yMaxValue } = extractYMinMax(encoding, dataValues);
|
|
2684
|
+
// Extract axis category ordering
|
|
2685
|
+
const categoryOrderProps = extractAxisCategoryOrderProps(encoding);
|
|
2686
|
+
var _encoding_color_legend_disable;
|
|
2687
|
+
const result = {
|
|
2688
|
+
data: {
|
|
2689
|
+
chartTitle: titles.chartTitle,
|
|
2690
|
+
scatterChartData: chartData
|
|
2691
|
+
},
|
|
2692
|
+
xAxisTitle: titles.xAxisTitle,
|
|
2693
|
+
yAxisTitle: titles.yAxisTitle,
|
|
2694
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
2695
|
+
...yAxisTickFormat && {
|
|
2696
|
+
yAxisTickFormat
|
|
2697
|
+
},
|
|
2698
|
+
...yMinValue !== undefined && {
|
|
2699
|
+
yMinValue
|
|
2700
|
+
},
|
|
2701
|
+
...yMaxValue !== undefined && {
|
|
2702
|
+
yMaxValue
|
|
2703
|
+
},
|
|
2704
|
+
...yAxisType && {
|
|
2705
|
+
yScaleType: yAxisType
|
|
2706
|
+
},
|
|
2707
|
+
// For nominal y-axis, provide tick values and labels
|
|
2708
|
+
...yIsNominal && yOrdinalLabels.length > 0 && {
|
|
2709
|
+
yAxisTickValues: Array.from({
|
|
2710
|
+
length: yOrdinalLabels.length
|
|
2711
|
+
}, (_, i)=>i),
|
|
2712
|
+
yAxisTickFormat: (val)=>{
|
|
2713
|
+
var _yOrdinalLabels_val;
|
|
2714
|
+
return (_yOrdinalLabels_val = yOrdinalLabels[val]) !== null && _yOrdinalLabels_val !== void 0 ? _yOrdinalLabels_val : String(val);
|
|
2715
|
+
},
|
|
2716
|
+
yMinValue: -0.5,
|
|
2717
|
+
yMaxValue: yOrdinalLabels.length - 0.5
|
|
2718
|
+
},
|
|
2719
|
+
...categoryOrderProps,
|
|
2720
|
+
hideLegend: (_encoding_color_legend_disable = (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : (_encoding_color_legend = _encoding_color.legend) === null || _encoding_color_legend === void 0 ? void 0 : _encoding_color_legend.disable) !== null && _encoding_color_legend_disable !== void 0 ? _encoding_color_legend_disable : false
|
|
2721
|
+
};
|
|
2722
|
+
if (annotations.length > 0) {
|
|
2723
|
+
result.annotations = annotations;
|
|
2724
|
+
}
|
|
2725
|
+
if (tickConfig.tickValues) {
|
|
2726
|
+
result.tickValues = tickConfig.tickValues;
|
|
2727
|
+
}
|
|
2728
|
+
if (tickConfig.xAxisTickCount) {
|
|
2729
|
+
result.xAxisTickCount = tickConfig.xAxisTickCount;
|
|
2730
|
+
}
|
|
2731
|
+
if (tickConfig.yAxisTickCount) {
|
|
2732
|
+
result.yAxisTickCount = tickConfig.yAxisTickCount;
|
|
2733
|
+
}
|
|
2734
|
+
return result;
|
|
2735
|
+
}
|
|
2736
|
+
function transformVegaLiteToDonutChartProps(spec, colorMap, isDarkTheme) {
|
|
2737
|
+
// Initialize transformation context
|
|
2738
|
+
const { dataValues, encoding, primarySpec } = initializeTransformContext(spec);
|
|
2739
|
+
// Extract field names
|
|
2740
|
+
const { thetaField, colorField } = extractEncodingFields(encoding);
|
|
2741
|
+
if (!thetaField) {
|
|
2742
|
+
throw new Error('VegaLiteSchemaAdapter: Theta encoding is required for donut charts');
|
|
2743
|
+
}
|
|
2744
|
+
// Extract color configuration
|
|
2745
|
+
const { colorScheme, colorRange } = extractColorConfig(encoding);
|
|
2746
|
+
// Extract innerRadius from mark properties if available
|
|
2747
|
+
const mark = primarySpec.mark;
|
|
2748
|
+
const innerRadius = typeof mark === 'object' && (mark === null || mark === void 0 ? void 0 : mark.innerRadius) !== undefined ? mark.innerRadius : 0;
|
|
2749
|
+
const chartData = [];
|
|
2750
|
+
const colorIndex = new Map();
|
|
2751
|
+
let currentColorIndex = 0;
|
|
2752
|
+
dataValues.forEach((row)=>{
|
|
2753
|
+
const value = row[thetaField];
|
|
2754
|
+
const legend = colorField && row[colorField] !== undefined ? String(row[colorField]) : String(value);
|
|
2755
|
+
if (value === undefined || typeof value !== 'number') {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
if (!colorIndex.has(legend)) {
|
|
2759
|
+
colorIndex.set(legend, currentColorIndex++);
|
|
2760
|
+
}
|
|
2761
|
+
chartData.push({
|
|
2762
|
+
legend,
|
|
2763
|
+
data: value,
|
|
2764
|
+
color: resolveColor(legend, colorIndex.get(legend), undefined, undefined, colorMap, colorScheme, colorRange, isDarkTheme)
|
|
2765
|
+
});
|
|
2766
|
+
});
|
|
2767
|
+
const titles = getVegaLiteTitles(spec);
|
|
2768
|
+
return {
|
|
2769
|
+
data: {
|
|
2770
|
+
chartTitle: titles.chartTitle,
|
|
2771
|
+
chartData
|
|
2772
|
+
},
|
|
2773
|
+
innerRadius,
|
|
2774
|
+
width: typeof spec.width === 'number' ? spec.width : undefined,
|
|
2775
|
+
height: typeof spec.height === 'number' ? spec.height : undefined,
|
|
2776
|
+
...titles.titleStyles ? titles.titleStyles : {}
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
function transformVegaLiteToHeatMapChartProps(spec, colorMap, isDarkTheme) {
|
|
2780
|
+
var _encoding_color, _encoding_color1, _encoding_x, _encoding_x1, _encoding_color_scale, _encoding_color2, _encoding_color_scale1, _encoding_color3;
|
|
2781
|
+
// Initialize transformation context
|
|
2782
|
+
const { dataValues, encoding } = initializeTransformContext(spec);
|
|
2783
|
+
// Extract field names
|
|
2784
|
+
const { xField, yField, colorField } = extractEncodingFields(encoding);
|
|
2785
|
+
if (!xField || !yField || !colorField) {
|
|
2786
|
+
throw new Error('VegaLiteSchemaAdapter: x, y, and color encodings are required for heatmap charts');
|
|
2787
|
+
}
|
|
2788
|
+
const heatmapDataPoints = [];
|
|
2789
|
+
let minValue = Number.POSITIVE_INFINITY;
|
|
2790
|
+
let maxValue = Number.NEGATIVE_INFINITY;
|
|
2791
|
+
// Check if color values are nominal (strings) rather than quantitative (numbers)
|
|
2792
|
+
const isNominalColor = ((_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.type) === 'nominal' || ((_encoding_color1 = encoding.color) === null || _encoding_color1 === void 0 ? void 0 : _encoding_color1.type) === 'ordinal' || dataValues.some((row)=>row[colorField] !== undefined && typeof row[colorField] !== 'number');
|
|
2793
|
+
const nominalColorMap = new Map();
|
|
2794
|
+
dataValues.forEach((row)=>{
|
|
2795
|
+
const xValue = row[xField];
|
|
2796
|
+
const yValue = row[yField];
|
|
2797
|
+
const colorValue = row[colorField];
|
|
2798
|
+
if ((0, _chartutilities.isInvalidValue)(xValue) || (0, _chartutilities.isInvalidValue)(yValue) || (0, _chartutilities.isInvalidValue)(colorValue)) {
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
let value;
|
|
2802
|
+
if (isNominalColor) {
|
|
2803
|
+
// Map nominal color values to sequential numeric indices
|
|
2804
|
+
const key = String(colorValue);
|
|
2805
|
+
if (!nominalColorMap.has(key)) {
|
|
2806
|
+
nominalColorMap.set(key, nominalColorMap.size);
|
|
2807
|
+
}
|
|
2808
|
+
value = nominalColorMap.get(key);
|
|
2809
|
+
} else {
|
|
2810
|
+
value = typeof colorValue === 'number' ? colorValue : 0;
|
|
2811
|
+
}
|
|
2812
|
+
minValue = Math.min(minValue, value);
|
|
2813
|
+
maxValue = Math.max(maxValue, value);
|
|
2814
|
+
heatmapDataPoints.push({
|
|
2815
|
+
x: xValue,
|
|
2816
|
+
y: yValue,
|
|
2817
|
+
value,
|
|
2818
|
+
rectText: isNominalColor ? String(colorValue) : value
|
|
2819
|
+
});
|
|
2820
|
+
});
|
|
2821
|
+
// Validate that we have complete grid data
|
|
2822
|
+
if (heatmapDataPoints.length === 0) {
|
|
2823
|
+
throw new Error('VegaLiteSchemaAdapter: Heatmap requires data points with x, y, and color values');
|
|
2824
|
+
}
|
|
2825
|
+
// Extract unique x and y values and create complete grid
|
|
2826
|
+
const uniqueXValues = new Set(heatmapDataPoints.map((p)=>String(p.x)));
|
|
2827
|
+
const uniqueYValues = new Set(heatmapDataPoints.map((p)=>String(p.y)));
|
|
2828
|
+
// Build a map of existing data points for quick lookup
|
|
2829
|
+
const dataPointMap = new Map();
|
|
2830
|
+
const rectTextMap = new Map();
|
|
2831
|
+
heatmapDataPoints.forEach((point)=>{
|
|
2832
|
+
const key = `${String(point.x)}|${String(point.y)}`;
|
|
2833
|
+
dataPointMap.set(key, point.value);
|
|
2834
|
+
var _point_rectText;
|
|
2835
|
+
rectTextMap.set(key, (_point_rectText = point.rectText) !== null && _point_rectText !== void 0 ? _point_rectText : point.value);
|
|
2836
|
+
});
|
|
2837
|
+
// Generate complete grid - fill missing cells with 0
|
|
2838
|
+
const completeGridDataPoints = [];
|
|
2839
|
+
let xValuesArray = Array.from(uniqueXValues);
|
|
2840
|
+
const yValuesArray = Array.from(uniqueYValues);
|
|
2841
|
+
// Sort x-values chronologically if they appear to be dates
|
|
2842
|
+
const isXTemporal = ((_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.type) === 'temporal' || ((_encoding_x1 = encoding.x) === null || _encoding_x1 === void 0 ? void 0 : _encoding_x1.type) === 'ordinal';
|
|
2843
|
+
if (isXTemporal) {
|
|
2844
|
+
const firstX = xValuesArray[0];
|
|
2845
|
+
const parsedDate = new Date(firstX);
|
|
2846
|
+
if (!isNaN(parsedDate.getTime())) {
|
|
2847
|
+
// Values are parseable as dates — sort chronologically
|
|
2848
|
+
xValuesArray = xValuesArray.sort((a, b)=>new Date(a).getTime() - new Date(b).getTime());
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
yValuesArray.forEach((yVal)=>{
|
|
2852
|
+
xValuesArray.forEach((xVal)=>{
|
|
2853
|
+
const key = `${xVal}|${yVal}`;
|
|
2854
|
+
var _dataPointMap_get;
|
|
2855
|
+
const value = (_dataPointMap_get = dataPointMap.get(key)) !== null && _dataPointMap_get !== void 0 ? _dataPointMap_get : 0; // Use 0 for missing cells
|
|
2856
|
+
// Update min/max to include filled values
|
|
2857
|
+
if (value !== 0 || dataPointMap.has(key)) {
|
|
2858
|
+
minValue = Math.min(minValue, value);
|
|
2859
|
+
maxValue = Math.max(maxValue, value);
|
|
2860
|
+
}
|
|
2861
|
+
var _rectTextMap_get;
|
|
2862
|
+
completeGridDataPoints.push({
|
|
2863
|
+
x: xVal,
|
|
2864
|
+
y: yVal,
|
|
2865
|
+
value,
|
|
2866
|
+
rectText: (_rectTextMap_get = rectTextMap.get(key)) !== null && _rectTextMap_get !== void 0 ? _rectTextMap_get : value
|
|
2867
|
+
});
|
|
2868
|
+
});
|
|
2869
|
+
});
|
|
2870
|
+
const heatmapData = {
|
|
2871
|
+
legend: '',
|
|
2872
|
+
data: completeGridDataPoints,
|
|
2873
|
+
value: 0
|
|
2874
|
+
};
|
|
2875
|
+
const titles = getVegaLiteTitles(spec);
|
|
2876
|
+
// Create color scale domain and range
|
|
2877
|
+
let domainValues = [];
|
|
2878
|
+
let rangeValues = [];
|
|
2879
|
+
// Check for named color scheme or custom range from encoding
|
|
2880
|
+
const colorScheme = (_encoding_color2 = encoding.color) === null || _encoding_color2 === void 0 ? void 0 : (_encoding_color_scale = _encoding_color2.scale) === null || _encoding_color_scale === void 0 ? void 0 : _encoding_color_scale.scheme;
|
|
2881
|
+
const customRange = (_encoding_color3 = encoding.color) === null || _encoding_color3 === void 0 ? void 0 : (_encoding_color_scale1 = _encoding_color3.scale) === null || _encoding_color_scale1 === void 0 ? void 0 : _encoding_color_scale1.range;
|
|
2882
|
+
if (isNominalColor && nominalColorMap.size > 0) {
|
|
2883
|
+
// For nominal colors, use categorical color scale
|
|
2884
|
+
const numCategories = nominalColorMap.size;
|
|
2885
|
+
domainValues = Array.from({
|
|
2886
|
+
length: numCategories
|
|
2887
|
+
}, (_, i)=>i);
|
|
2888
|
+
if (customRange && customRange.length >= numCategories) {
|
|
2889
|
+
rangeValues = customRange.slice(0, numCategories);
|
|
2890
|
+
} else {
|
|
2891
|
+
// Use distinct categorical colors for each category
|
|
2892
|
+
for(let i = 0; i < numCategories; i++){
|
|
2893
|
+
rangeValues.push((0, _VegaLiteColorAdapter.getVegaColor)(i, colorScheme, customRange, isDarkTheme !== null && isDarkTheme !== void 0 ? isDarkTheme : false));
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
} else {
|
|
2897
|
+
// Quantitative color scale
|
|
2898
|
+
const steps = 5;
|
|
2899
|
+
for(let i = 0; i < steps; i++){
|
|
2900
|
+
const t = i / (steps - 1);
|
|
2901
|
+
domainValues.push(minValue + (maxValue - minValue) * t);
|
|
2902
|
+
}
|
|
2903
|
+
if (customRange && customRange.length > 0) {
|
|
2904
|
+
rangeValues = customRange.length >= steps ? customRange.slice(0, steps) : customRange;
|
|
2905
|
+
} else if (colorScheme) {
|
|
2906
|
+
const schemeColors = (0, _VegaLiteColorAdapter.getSequentialSchemeColors)(colorScheme, steps);
|
|
2907
|
+
if (schemeColors) {
|
|
2908
|
+
var _encoding_color4, _encoding_color_scale2, _encoding_color5;
|
|
2909
|
+
const isReversed = ((_encoding_color4 = encoding.color) === null || _encoding_color4 === void 0 ? void 0 : _encoding_color4.sort) === 'descending' || ((_encoding_color5 = encoding.color) === null || _encoding_color5 === void 0 ? void 0 : (_encoding_color_scale2 = _encoding_color5.scale) === null || _encoding_color_scale2 === void 0 ? void 0 : _encoding_color_scale2.reverse) === true;
|
|
2910
|
+
rangeValues = isReversed ? schemeColors.reverse() : schemeColors;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
// Fall back to default blue-to-red gradient if no scheme matched
|
|
2914
|
+
if (rangeValues.length === 0) {
|
|
2915
|
+
for(let i = 0; i < steps; i++){
|
|
2916
|
+
const t = i / (steps - 1);
|
|
2917
|
+
if (isDarkTheme) {
|
|
2918
|
+
const r = Math.round(0 + 255 * t);
|
|
2919
|
+
const g = Math.round(100 + (165 - 100) * t);
|
|
2920
|
+
const b = Math.round(255 - 255 * t);
|
|
2921
|
+
rangeValues.push(`rgb(${r}, ${g}, ${b})`);
|
|
2922
|
+
} else {
|
|
2923
|
+
const r = Math.round(0 + 255 * t);
|
|
2924
|
+
const g = Math.round(150 - 150 * t);
|
|
2925
|
+
const b = Math.round(255 - 255 * t);
|
|
2926
|
+
rangeValues.push(`rgb(${r}, ${g}, ${b})`);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
var _spec_height;
|
|
2932
|
+
return {
|
|
2933
|
+
chartTitle: titles.chartTitle,
|
|
2934
|
+
data: [
|
|
2935
|
+
heatmapData
|
|
2936
|
+
],
|
|
2937
|
+
domainValuesForColorScale: domainValues,
|
|
2938
|
+
rangeValuesForColorScale: rangeValues,
|
|
2939
|
+
xAxisTitle: titles.xAxisTitle,
|
|
2940
|
+
yAxisTitle: titles.yAxisTitle,
|
|
2941
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
2942
|
+
width: spec.width,
|
|
2943
|
+
height: (_spec_height = spec.height) !== null && _spec_height !== void 0 ? _spec_height : DEFAULT_CHART_HEIGHT,
|
|
2944
|
+
hideLegend: true,
|
|
2945
|
+
showYAxisLables: true,
|
|
2946
|
+
sortOrder: 'none',
|
|
2947
|
+
hideTickOverlap: true,
|
|
2948
|
+
noOfCharsToTruncate: xValuesArray.length > 20 ? 6 : xValuesArray.length > 10 ? 10 : DEFAULT_TRUNCATE_CHARS,
|
|
2949
|
+
showYAxisLablesTooltip: true,
|
|
2950
|
+
wrapXAxisLables: true
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Helper function to get bin center for display
|
|
2955
|
+
*/ function getBinCenter(bin) {
|
|
2956
|
+
return (bin.x0 + bin.x1) / 2;
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Helper function to calculate histogram aggregation function
|
|
2960
|
+
*
|
|
2961
|
+
* @param aggregate - Aggregation type (count, sum, mean, min, max)
|
|
2962
|
+
* @param bin - Binned data values
|
|
2963
|
+
* @returns Aggregated value
|
|
2964
|
+
*/ function calculateHistogramAggregate(aggregate, bin) {
|
|
2965
|
+
switch(aggregate){
|
|
2966
|
+
case 'sum':
|
|
2967
|
+
return (0, _d3array.sum)(bin);
|
|
2968
|
+
case 'mean':
|
|
2969
|
+
case 'average':
|
|
2970
|
+
var _d3Mean;
|
|
2971
|
+
return bin.length === 0 ? 0 : (_d3Mean = (0, _d3array.mean)(bin)) !== null && _d3Mean !== void 0 ? _d3Mean : 0;
|
|
2972
|
+
case 'min':
|
|
2973
|
+
var _d3Min;
|
|
2974
|
+
return (_d3Min = (0, _d3array.min)(bin)) !== null && _d3Min !== void 0 ? _d3Min : 0;
|
|
2975
|
+
case 'max':
|
|
2976
|
+
var _d3Max;
|
|
2977
|
+
return (_d3Max = (0, _d3array.max)(bin)) !== null && _d3Max !== void 0 ? _d3Max : 0;
|
|
2978
|
+
case 'count':
|
|
2979
|
+
default:
|
|
2980
|
+
return bin.length;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
function transformVegaLiteToHistogramProps(spec, colorMap, isDarkTheme) {
|
|
2984
|
+
var _encoding_y, _encoding_x, _encoding_color, _dataValues_, _encoding_y1, _encoding_y_axis, _encoding_y2;
|
|
2985
|
+
// Initialize transformation context
|
|
2986
|
+
const { dataValues, encoding } = initializeTransformContext(spec);
|
|
2987
|
+
// Extract field names
|
|
2988
|
+
const { xField } = extractEncodingFields(encoding);
|
|
2989
|
+
const yAggregate = ((_encoding_y = encoding.y) === null || _encoding_y === void 0 ? void 0 : _encoding_y.aggregate) || 'count';
|
|
2990
|
+
const binConfig = (_encoding_x = encoding.x) === null || _encoding_x === void 0 ? void 0 : _encoding_x.bin;
|
|
2991
|
+
if (!xField || !binConfig) {
|
|
2992
|
+
throw new Error('VegaLiteSchemaAdapter: Histogram requires x encoding with bin property');
|
|
2993
|
+
}
|
|
2994
|
+
// Validate data
|
|
2995
|
+
validateDataArray(dataValues, xField, 'Histogram');
|
|
2996
|
+
validateNoNestedArrays(dataValues, xField);
|
|
2997
|
+
// Extract numeric values from the field
|
|
2998
|
+
const allValues = dataValues.map((row)=>row[xField]).filter((val)=>!(0, _chartutilities.isInvalidValue)(val));
|
|
2999
|
+
const values = allValues.filter((val)=>typeof val === 'number');
|
|
3000
|
+
if (values.length === 0) {
|
|
3001
|
+
// Provide helpful error message based on actual data type
|
|
3002
|
+
const sampleValue = allValues[0];
|
|
3003
|
+
const actualType = typeof sampleValue;
|
|
3004
|
+
let suggestion = '';
|
|
3005
|
+
if (actualType === 'string') {
|
|
3006
|
+
// Check if strings contain numbers
|
|
3007
|
+
const hasEmbeddedNumbers = allValues.some((val)=>typeof val === 'string' && /\d/.test(val));
|
|
3008
|
+
if (hasEmbeddedNumbers) {
|
|
3009
|
+
suggestion = ' The data contains strings with embedded numbers (e.g., "40 salads"). ' + 'Consider extracting the numeric values first, or change the encoding type to "nominal" or "ordinal" for a categorical bar chart.';
|
|
3010
|
+
} else {
|
|
3011
|
+
suggestion = ` The data contains categorical strings (e.g., "${sampleValue}"). ` + 'Change the x encoding type to "nominal" or "ordinal" for a categorical bar chart, ' + 'or remove bin: true to create a simple bar chart.';
|
|
3012
|
+
}
|
|
3013
|
+
} else if (actualType === 'undefined') {
|
|
3014
|
+
suggestion = ' The field may not exist in the data.';
|
|
3015
|
+
}
|
|
3016
|
+
throw new Error(`VegaLiteSchemaAdapter: No numeric values found for histogram binning on field "${xField}". ` + `Found ${actualType} values instead.${suggestion}`);
|
|
3017
|
+
}
|
|
3018
|
+
// Create bins using d3
|
|
3019
|
+
const [minVal, maxVal] = (0, _d3array.extent)(values);
|
|
3020
|
+
const binGenerator = (0, _d3array.bin)().domain([
|
|
3021
|
+
minVal,
|
|
3022
|
+
maxVal
|
|
3023
|
+
]);
|
|
3024
|
+
// Apply bin configuration
|
|
3025
|
+
if (typeof binConfig === 'object') {
|
|
3026
|
+
if (binConfig.maxbins) {
|
|
3027
|
+
binGenerator.thresholds(binConfig.maxbins);
|
|
3028
|
+
}
|
|
3029
|
+
if (binConfig.extent) {
|
|
3030
|
+
binGenerator.domain(binConfig.extent);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const bins = binGenerator(values);
|
|
3034
|
+
// Calculate histogram data points
|
|
3035
|
+
const legend = ((_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : _encoding_color.field) ? String((_dataValues_ = dataValues[0]) === null || _dataValues_ === void 0 ? void 0 : _dataValues_[encoding.color.field]) : 'Frequency';
|
|
3036
|
+
const color = resolveColor(legend, 0, undefined, undefined, colorMap, undefined, undefined, isDarkTheme);
|
|
3037
|
+
const yField = (_encoding_y1 = encoding.y) === null || _encoding_y1 === void 0 ? void 0 : _encoding_y1.field;
|
|
3038
|
+
const histogramData = bins.map((bin)=>{
|
|
3039
|
+
const x = getBinCenter(bin);
|
|
3040
|
+
let y;
|
|
3041
|
+
if (yAggregate !== 'count' && yField) {
|
|
3042
|
+
// For non-count aggregates, collect y-field values for rows whose x-value falls in this bin
|
|
3043
|
+
const yValues = dataValues.filter((row)=>{
|
|
3044
|
+
const xVal = Number(row[xField]);
|
|
3045
|
+
return !isNaN(xVal) && xVal >= bin.x0 && xVal < bin.x1;
|
|
3046
|
+
}).map((row)=>Number(row[yField])).filter((v)=>!isNaN(v));
|
|
3047
|
+
// Include the last bin's upper bound (x1 is inclusive for the last bin)
|
|
3048
|
+
if (bin === bins[bins.length - 1]) {
|
|
3049
|
+
const extraRows = dataValues.filter((row)=>Number(row[xField]) === bin.x1).map((row)=>Number(row[yField])).filter((v)=>!isNaN(v));
|
|
3050
|
+
yValues.push(...extraRows);
|
|
3051
|
+
}
|
|
3052
|
+
y = calculateHistogramAggregate(yAggregate, yValues);
|
|
3053
|
+
} else {
|
|
3054
|
+
y = calculateHistogramAggregate(yAggregate, bin);
|
|
3055
|
+
}
|
|
3056
|
+
const xAxisCalloutData = `[${bin.x0} - ${bin.x1})`;
|
|
3057
|
+
return {
|
|
3058
|
+
x,
|
|
3059
|
+
y,
|
|
3060
|
+
legend,
|
|
3061
|
+
color,
|
|
3062
|
+
xAxisCalloutData
|
|
3063
|
+
};
|
|
3064
|
+
});
|
|
3065
|
+
const titles = getVegaLiteTitles(spec);
|
|
3066
|
+
const annotations = extractAnnotations(spec);
|
|
3067
|
+
const yAxisTickFormat = (_encoding_y2 = encoding.y) === null || _encoding_y2 === void 0 ? void 0 : (_encoding_y_axis = _encoding_y2.axis) === null || _encoding_y_axis === void 0 ? void 0 : _encoding_y_axis.format;
|
|
3068
|
+
return {
|
|
3069
|
+
data: histogramData,
|
|
3070
|
+
chartTitle: titles.chartTitle,
|
|
3071
|
+
xAxisTitle: titles.xAxisTitle || xField,
|
|
3072
|
+
yAxisTitle: titles.yAxisTitle || yAggregate,
|
|
3073
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
3074
|
+
roundCorners: true,
|
|
3075
|
+
hideTickOverlap: true,
|
|
3076
|
+
maxBarWidth: DEFAULT_MAX_BAR_WIDTH,
|
|
3077
|
+
...annotations.length > 0 && {
|
|
3078
|
+
annotations
|
|
3079
|
+
},
|
|
3080
|
+
...yAxisTickFormat && {
|
|
3081
|
+
yAxisTickFormat
|
|
3082
|
+
},
|
|
3083
|
+
mode: 'histogram'
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
function transformVegaLiteToPolarChartProps(spec, colorMap, isDarkTheme) {
|
|
3087
|
+
var _encoding_theta, _encoding_color_legend, _encoding_color;
|
|
3088
|
+
// Initialize transformation context
|
|
3089
|
+
const { dataValues, encoding, markProps, primarySpec } = initializeTransformContext(spec);
|
|
3090
|
+
// Extract field names
|
|
3091
|
+
const { thetaField, radiusField, colorField } = extractEncodingFields(encoding);
|
|
3092
|
+
// Validate polar encodings
|
|
3093
|
+
if (!thetaField || !radiusField) {
|
|
3094
|
+
throw new Error('VegaLiteSchemaAdapter: Both theta and radius encodings are required for polar charts');
|
|
3095
|
+
}
|
|
3096
|
+
validateDataArray(dataValues, thetaField, 'PolarChart');
|
|
3097
|
+
validateDataArray(dataValues, radiusField, 'PolarChart');
|
|
3098
|
+
// Determine mark type for polar chart series type
|
|
3099
|
+
const mark = primarySpec.mark;
|
|
3100
|
+
const markType = typeof mark === 'string' ? mark : mark === null || mark === void 0 ? void 0 : mark.type;
|
|
3101
|
+
// Arc marks with theta+radius should be treated as area polar (radial/rose charts)
|
|
3102
|
+
const isAreaMark = markType === 'area' || markType === 'arc';
|
|
3103
|
+
const isLineMark = markType === 'line';
|
|
3104
|
+
// Extract color configuration
|
|
3105
|
+
const { colorScheme, colorRange } = extractColorConfig(encoding);
|
|
3106
|
+
// Group data by series (color field)
|
|
3107
|
+
const seriesMap = new Map();
|
|
3108
|
+
const colorIndex = new Map();
|
|
3109
|
+
let currentColorIndex = 0;
|
|
3110
|
+
dataValues.forEach((row)=>{
|
|
3111
|
+
const thetaValue = row[thetaField];
|
|
3112
|
+
const radiusValue = row[radiusField];
|
|
3113
|
+
// Skip invalid values
|
|
3114
|
+
if ((0, _chartutilities.isInvalidValue)(thetaValue) || (0, _chartutilities.isInvalidValue)(radiusValue)) {
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
const seriesName = colorField && row[colorField] !== undefined ? String(row[colorField]) : 'default';
|
|
3118
|
+
if (!colorIndex.has(seriesName)) {
|
|
3119
|
+
colorIndex.set(seriesName, currentColorIndex++);
|
|
3120
|
+
}
|
|
3121
|
+
if (!seriesMap.has(seriesName)) {
|
|
3122
|
+
seriesMap.set(seriesName, []);
|
|
3123
|
+
}
|
|
3124
|
+
// Convert theta value - handle different types
|
|
3125
|
+
let theta;
|
|
3126
|
+
if (typeof thetaValue === 'number') {
|
|
3127
|
+
// Numeric theta - assume degrees
|
|
3128
|
+
theta = thetaValue;
|
|
3129
|
+
} else {
|
|
3130
|
+
// Categorical theta
|
|
3131
|
+
theta = String(thetaValue);
|
|
3132
|
+
}
|
|
3133
|
+
// Convert radius value
|
|
3134
|
+
const r = typeof radiusValue === 'number' ? radiusValue : Number(radiusValue);
|
|
3135
|
+
seriesMap.get(seriesName).push({
|
|
3136
|
+
theta,
|
|
3137
|
+
r
|
|
3138
|
+
});
|
|
3139
|
+
});
|
|
3140
|
+
// Convert series map to polar chart data array
|
|
3141
|
+
const polarData = [];
|
|
3142
|
+
seriesMap.forEach((dataPoints, seriesName)=>{
|
|
3143
|
+
const color = resolveColor(seriesName, colorIndex.get(seriesName), undefined, markProps.color, colorMap, colorScheme, colorRange, isDarkTheme);
|
|
3144
|
+
const curveOption = mapInterpolateToCurve(markProps.interpolate);
|
|
3145
|
+
// Build line options with curve, strokeDash, and strokeWidth
|
|
3146
|
+
const lineOptions = {};
|
|
3147
|
+
if (curveOption) {
|
|
3148
|
+
lineOptions.curve = curveOption;
|
|
3149
|
+
}
|
|
3150
|
+
if (markProps.strokeDash) {
|
|
3151
|
+
lineOptions.strokeDasharray = markProps.strokeDash.join(' ');
|
|
3152
|
+
}
|
|
3153
|
+
if (markProps.strokeWidth) {
|
|
3154
|
+
lineOptions.strokeWidth = markProps.strokeWidth;
|
|
3155
|
+
}
|
|
3156
|
+
if (isAreaMark) {
|
|
3157
|
+
const series = {
|
|
3158
|
+
type: 'areapolar',
|
|
3159
|
+
legend: seriesName,
|
|
3160
|
+
color,
|
|
3161
|
+
data: dataPoints,
|
|
3162
|
+
...Object.keys(lineOptions).length > 0 && {
|
|
3163
|
+
lineOptions
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
polarData.push(series);
|
|
3167
|
+
} else if (isLineMark) {
|
|
3168
|
+
const series = {
|
|
3169
|
+
type: 'linepolar',
|
|
3170
|
+
legend: seriesName,
|
|
3171
|
+
color,
|
|
3172
|
+
data: dataPoints,
|
|
3173
|
+
...Object.keys(lineOptions).length > 0 && {
|
|
3174
|
+
lineOptions
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
polarData.push(series);
|
|
3178
|
+
} else {
|
|
3179
|
+
// Default to scatter polar for point marks
|
|
3180
|
+
const series = {
|
|
3181
|
+
type: 'scatterpolar',
|
|
3182
|
+
legend: seriesName,
|
|
3183
|
+
color,
|
|
3184
|
+
data: dataPoints
|
|
3185
|
+
};
|
|
3186
|
+
polarData.push(series);
|
|
3187
|
+
}
|
|
3188
|
+
});
|
|
3189
|
+
// Extract chart titles
|
|
3190
|
+
const titles = getVegaLiteTitles(spec);
|
|
3191
|
+
// Build axis props from encoding
|
|
3192
|
+
const radialAxis = {};
|
|
3193
|
+
const angularAxis = {};
|
|
3194
|
+
// Determine angular axis category order if theta is categorical
|
|
3195
|
+
const thetaType = (_encoding_theta = encoding.theta) === null || _encoding_theta === void 0 ? void 0 : _encoding_theta.type;
|
|
3196
|
+
if (thetaType === 'nominal' || thetaType === 'ordinal') {
|
|
3197
|
+
// Get unique theta values in order for category order
|
|
3198
|
+
const thetaValues = Array.from(new Set(dataValues.map((row)=>String(row[thetaField]))));
|
|
3199
|
+
angularAxis.categoryOrder = thetaValues;
|
|
3200
|
+
}
|
|
3201
|
+
var _encoding_color_legend_disable;
|
|
3202
|
+
return {
|
|
3203
|
+
data: polarData,
|
|
3204
|
+
...titles.chartTitle && {
|
|
3205
|
+
chartTitle: titles.chartTitle
|
|
3206
|
+
},
|
|
3207
|
+
...titles.titleStyles ? titles.titleStyles : {},
|
|
3208
|
+
width: typeof spec.width === 'number' ? spec.width : undefined,
|
|
3209
|
+
height: typeof spec.height === 'number' ? spec.height : 400,
|
|
3210
|
+
hideLegend: (_encoding_color_legend_disable = (_encoding_color = encoding.color) === null || _encoding_color === void 0 ? void 0 : (_encoding_color_legend = _encoding_color.legend) === null || _encoding_color_legend === void 0 ? void 0 : _encoding_color_legend.disable) !== null && _encoding_color_legend_disable !== void 0 ? _encoding_color_legend_disable : false,
|
|
3211
|
+
radialAxis,
|
|
3212
|
+
angularAxis
|
|
3213
|
+
};
|
|
3214
|
+
}
|