@opendata-ai/openchart-engine 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +155 -19
- package/dist/index.js +1513 -164
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +6 -3
- package/src/__tests__/axes.test.ts +168 -4
- package/src/__tests__/compile-chart.test.ts +23 -12
- package/src/__tests__/compile-layer.test.ts +386 -0
- package/src/__tests__/dimensions.test.ts +6 -3
- package/src/__tests__/legend.test.ts +6 -3
- package/src/__tests__/scales.test.ts +176 -2
- package/src/annotations/__tests__/compute.test.ts +8 -4
- package/src/charts/bar/__tests__/compute.test.ts +12 -6
- package/src/charts/bar/compute.ts +21 -5
- package/src/charts/column/__tests__/compute.test.ts +14 -7
- package/src/charts/column/compute.ts +21 -6
- package/src/charts/dot/__tests__/compute.test.ts +10 -5
- package/src/charts/dot/compute.ts +10 -4
- package/src/charts/line/__tests__/compute.test.ts +102 -11
- package/src/charts/line/__tests__/curves.test.ts +51 -0
- package/src/charts/line/__tests__/labels.test.ts +2 -1
- package/src/charts/line/__tests__/mark-options.test.ts +175 -0
- package/src/charts/line/area.ts +19 -8
- package/src/charts/line/compute.ts +64 -25
- package/src/charts/line/curves.ts +40 -0
- package/src/charts/pie/__tests__/compute.test.ts +10 -5
- package/src/charts/pie/compute.ts +2 -1
- package/src/charts/rule/index.ts +127 -0
- package/src/charts/scatter/__tests__/compute.test.ts +10 -5
- package/src/charts/scatter/compute.ts +15 -5
- package/src/charts/text/index.ts +92 -0
- package/src/charts/tick/index.ts +84 -0
- package/src/charts/utils.ts +1 -1
- package/src/compile.ts +175 -23
- package/src/compiler/__tests__/compile.test.ts +4 -4
- package/src/compiler/__tests__/normalize.test.ts +4 -4
- package/src/compiler/__tests__/validate.test.ts +25 -26
- package/src/compiler/index.ts +1 -1
- package/src/compiler/normalize.ts +77 -4
- package/src/compiler/types.ts +6 -2
- package/src/compiler/validate.ts +167 -35
- package/src/graphs/__tests__/compile-graph.test.ts +2 -2
- package/src/graphs/compile-graph.ts +2 -2
- package/src/index.ts +17 -1
- package/src/layout/axes.ts +122 -20
- package/src/layout/dimensions.ts +15 -9
- package/src/layout/scales.ts +320 -31
- package/src/legend/compute.ts +9 -6
- package/src/tables/__tests__/compile-table.test.ts +1 -1
- package/src/tooltips/__tests__/compute.test.ts +10 -5
- package/src/tooltips/compute.ts +32 -14
- package/src/transforms/__tests__/bin.test.ts +88 -0
- package/src/transforms/__tests__/calculate.test.ts +146 -0
- package/src/transforms/__tests__/conditional.test.ts +109 -0
- package/src/transforms/__tests__/filter.test.ts +59 -0
- package/src/transforms/__tests__/index.test.ts +93 -0
- package/src/transforms/__tests__/predicates.test.ts +176 -0
- package/src/transforms/__tests__/timeunit.test.ts +129 -0
- package/src/transforms/bin.ts +87 -0
- package/src/transforms/calculate.ts +60 -0
- package/src/transforms/conditional.ts +46 -0
- package/src/transforms/filter.ts +17 -0
- package/src/transforms/index.ts +48 -0
- package/src/transforms/predicates.ts +90 -0
- package/src/transforms/timeunit.ts +88 -0
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// Main compile API
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
-
export { compileChart, compileGraph, compileTable } from './compile';
|
|
15
|
+
export { compileChart, compileGraph, compileLayer, compileTable } from './compile';
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Graph compilation types
|
|
@@ -57,6 +57,21 @@ export {
|
|
|
57
57
|
registerChartRenderer,
|
|
58
58
|
} from './charts/registry';
|
|
59
59
|
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Data transforms
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
evaluatePredicate,
|
|
66
|
+
isConditionalValueDef,
|
|
67
|
+
resolveConditionalValue,
|
|
68
|
+
runBin,
|
|
69
|
+
runCalculate,
|
|
70
|
+
runFilter,
|
|
71
|
+
runTimeUnit,
|
|
72
|
+
runTransforms,
|
|
73
|
+
} from './transforms';
|
|
74
|
+
|
|
60
75
|
// ---------------------------------------------------------------------------
|
|
61
76
|
// Re-export core types for convenience
|
|
62
77
|
// ---------------------------------------------------------------------------
|
|
@@ -68,6 +83,7 @@ export type {
|
|
|
68
83
|
CompileTableOptions,
|
|
69
84
|
GraphLayout,
|
|
70
85
|
GraphSpec,
|
|
86
|
+
LayerSpec,
|
|
71
87
|
TableLayout,
|
|
72
88
|
TableSpec,
|
|
73
89
|
VizSpec,
|
package/src/layout/axes.ts
CHANGED
|
@@ -172,9 +172,21 @@ export function thinTicksUntilFit(
|
|
|
172
172
|
// Tick generation
|
|
173
173
|
// ---------------------------------------------------------------------------
|
|
174
174
|
|
|
175
|
-
/** Generate ticks for a continuous scale (linear, time, log). */
|
|
175
|
+
/** Generate ticks for a continuous scale (linear, time, log, pow, sqrt, symlog). */
|
|
176
176
|
function continuousTicks(resolvedScale: ResolvedScale, density: AxisLabelDensity): AxisTick[] {
|
|
177
177
|
const scale = resolvedScale.scale as D3ContinuousScale;
|
|
178
|
+
|
|
179
|
+
// Discretizing scales (quantile, quantize, threshold) don't have .ticks().
|
|
180
|
+
// Use their domain thresholds as ticks instead.
|
|
181
|
+
if (!('ticks' in scale) || typeof scale.ticks !== 'function') {
|
|
182
|
+
const domain = scale.domain() as unknown[];
|
|
183
|
+
return domain.map((value: unknown) => ({
|
|
184
|
+
value,
|
|
185
|
+
position: (scale as D3ContinuousScale)(value as number & Date) as number,
|
|
186
|
+
label: formatTickLabel(value, resolvedScale),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
178
190
|
const explicitCount = resolvedScale.channel.axis?.tickCount;
|
|
179
191
|
const count = explicitCount ?? TICK_COUNTS[density];
|
|
180
192
|
const rawTicks: unknown[] = scale.ticks(count);
|
|
@@ -220,16 +232,31 @@ function categoricalTicks(resolvedScale: ResolvedScale, density: AxisLabelDensit
|
|
|
220
232
|
return ticks;
|
|
221
233
|
}
|
|
222
234
|
|
|
235
|
+
/** Set of continuous numeric scale types that should format as numbers. */
|
|
236
|
+
const NUMERIC_SCALE_TYPES = new Set([
|
|
237
|
+
'linear',
|
|
238
|
+
'log',
|
|
239
|
+
'pow',
|
|
240
|
+
'sqrt',
|
|
241
|
+
'symlog',
|
|
242
|
+
'quantile',
|
|
243
|
+
'quantize',
|
|
244
|
+
'threshold',
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
/** Set of temporal scale types. */
|
|
248
|
+
const TEMPORAL_SCALE_TYPES = new Set(['time', 'utc']);
|
|
249
|
+
|
|
223
250
|
/** Format a tick value based on the scale type. */
|
|
224
251
|
function formatTickLabel(value: unknown, resolvedScale: ResolvedScale): string {
|
|
225
252
|
const formatStr = resolvedScale.channel.axis?.format;
|
|
226
253
|
|
|
227
|
-
if (resolvedScale.type
|
|
254
|
+
if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
|
|
228
255
|
if (formatStr) return String(value); // Custom format not implemented yet
|
|
229
256
|
return formatDate(value as Date);
|
|
230
257
|
}
|
|
231
258
|
|
|
232
|
-
if (
|
|
259
|
+
if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
|
|
233
260
|
const num = value as number;
|
|
234
261
|
if (formatStr) {
|
|
235
262
|
const fmt = buildD3Formatter(formatStr);
|
|
@@ -243,6 +270,35 @@ function formatTickLabel(value: unknown, resolvedScale: ResolvedScale): string {
|
|
|
243
270
|
return String(value);
|
|
244
271
|
}
|
|
245
272
|
|
|
273
|
+
/** Resolve explicit tick values from axis config into positioned ticks. */
|
|
274
|
+
function resolveExplicitTicks(values: unknown[], resolvedScale: ResolvedScale): AxisTick[] {
|
|
275
|
+
const scale = resolvedScale.scale;
|
|
276
|
+
return values.map((value) => {
|
|
277
|
+
let position: number;
|
|
278
|
+
if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
|
|
279
|
+
const d = value instanceof Date ? value : new Date(String(value));
|
|
280
|
+
position = (scale as D3ContinuousScale)(d as number & Date) as number;
|
|
281
|
+
} else if (
|
|
282
|
+
resolvedScale.type === 'band' ||
|
|
283
|
+
resolvedScale.type === 'point' ||
|
|
284
|
+
resolvedScale.type === 'ordinal'
|
|
285
|
+
) {
|
|
286
|
+
const s = String(value);
|
|
287
|
+
const bandScale = resolvedScale.type === 'band' ? (scale as ScaleBand<string>) : null;
|
|
288
|
+
position = bandScale
|
|
289
|
+
? (bandScale(s) ?? 0) + bandScale.bandwidth() / 2
|
|
290
|
+
: ((scale(s as string & number) as number | undefined) ?? 0);
|
|
291
|
+
} else {
|
|
292
|
+
position = (scale as D3ContinuousScale)(value as number & Date) as number;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
value,
|
|
296
|
+
position,
|
|
297
|
+
label: formatTickLabel(value, resolvedScale),
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
246
302
|
// ---------------------------------------------------------------------------
|
|
247
303
|
// Public API
|
|
248
304
|
// ---------------------------------------------------------------------------
|
|
@@ -308,10 +364,21 @@ export function computeAxes(
|
|
|
308
364
|
const { fontWeight } = tickLabelStyle;
|
|
309
365
|
|
|
310
366
|
if (scales.x) {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
367
|
+
const axisConfig = scales.x.channel.axis;
|
|
368
|
+
|
|
369
|
+
// Use explicit tick values from axis config if provided
|
|
370
|
+
let allTicks: AxisTick[];
|
|
371
|
+
if (axisConfig?.values) {
|
|
372
|
+
allTicks = resolveExplicitTicks(axisConfig.values, scales.x);
|
|
373
|
+
} else if (
|
|
374
|
+
scales.x.type === 'band' ||
|
|
375
|
+
scales.x.type === 'point' ||
|
|
376
|
+
scales.x.type === 'ordinal'
|
|
377
|
+
) {
|
|
378
|
+
allTicks = categoricalTicks(scales.x, xDensity);
|
|
379
|
+
} else {
|
|
380
|
+
allTicks = continuousTicks(scales.x, xDensity);
|
|
381
|
+
}
|
|
315
382
|
|
|
316
383
|
// Gridlines use the full tick set so they remain visible even when labels
|
|
317
384
|
// are thinned to prevent overlap.
|
|
@@ -321,15 +388,16 @@ export function computeAxes(
|
|
|
321
388
|
}));
|
|
322
389
|
|
|
323
390
|
// Thin tick labels to prevent overlap (skip for band scales which use
|
|
324
|
-
// auto-rotation, and when the user set an explicit tickCount).
|
|
325
|
-
const shouldThin = scales.x.type !== 'band' && !
|
|
391
|
+
// auto-rotation, and when the user set an explicit tickCount or values).
|
|
392
|
+
const shouldThin = scales.x.type !== 'band' && !axisConfig?.tickCount && !axisConfig?.values;
|
|
326
393
|
const ticks = shouldThin
|
|
327
394
|
? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText)
|
|
328
395
|
: allTicks;
|
|
329
396
|
|
|
330
397
|
// Auto-rotate labels when band scale labels would overlap.
|
|
331
398
|
// Uses max label width (not average) since one long label is enough to overlap.
|
|
332
|
-
|
|
399
|
+
// Prefer labelAngle over deprecated tickAngle
|
|
400
|
+
let tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
|
|
333
401
|
if (tickAngle === undefined && scales.x.type === 'band' && ticks.length > 1) {
|
|
334
402
|
const bandwidth = (scales.x.scale as ScaleBand<string>).bandwidth();
|
|
335
403
|
let maxLabelWidth = 0;
|
|
@@ -343,23 +411,45 @@ export function computeAxes(
|
|
|
343
411
|
}
|
|
344
412
|
}
|
|
345
413
|
|
|
414
|
+
// Prefer title over deprecated label
|
|
415
|
+
const axisTitle = axisConfig?.title ?? axisConfig?.label;
|
|
416
|
+
|
|
346
417
|
result.x = {
|
|
347
418
|
ticks,
|
|
348
|
-
gridlines:
|
|
349
|
-
label:
|
|
419
|
+
gridlines: axisConfig?.grid ? gridlines : [],
|
|
420
|
+
label: axisTitle,
|
|
350
421
|
labelStyle: axisLabelStyle,
|
|
351
422
|
tickLabelStyle,
|
|
352
423
|
tickAngle,
|
|
353
424
|
start: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
354
425
|
end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height },
|
|
426
|
+
orient: axisConfig?.orient,
|
|
427
|
+
domainLine: axisConfig?.domain,
|
|
428
|
+
tickMarks: axisConfig?.ticks,
|
|
429
|
+
offset: axisConfig?.offset,
|
|
430
|
+
titlePadding: axisConfig?.titlePadding,
|
|
431
|
+
labelPadding: axisConfig?.labelPadding,
|
|
432
|
+
labelOverlap: axisConfig?.labelOverlap,
|
|
433
|
+
labelFlush: axisConfig?.labelFlush,
|
|
355
434
|
};
|
|
356
435
|
}
|
|
357
436
|
|
|
358
437
|
if (scales.y) {
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
438
|
+
const axisConfig = scales.y.channel.axis;
|
|
439
|
+
|
|
440
|
+
// Use explicit tick values from axis config if provided
|
|
441
|
+
let allTicks: AxisTick[];
|
|
442
|
+
if (axisConfig?.values) {
|
|
443
|
+
allTicks = resolveExplicitTicks(axisConfig.values, scales.y);
|
|
444
|
+
} else if (
|
|
445
|
+
scales.y.type === 'band' ||
|
|
446
|
+
scales.y.type === 'point' ||
|
|
447
|
+
scales.y.type === 'ordinal'
|
|
448
|
+
) {
|
|
449
|
+
allTicks = categoricalTicks(scales.y, yDensity);
|
|
450
|
+
} else {
|
|
451
|
+
allTicks = continuousTicks(scales.y, yDensity);
|
|
452
|
+
}
|
|
363
453
|
|
|
364
454
|
// Gridlines use the full tick set (label thinning shouldn't remove gridlines).
|
|
365
455
|
const gridlines: Gridline[] = allTicks.map((t) => ({
|
|
@@ -367,22 +457,34 @@ export function computeAxes(
|
|
|
367
457
|
major: true,
|
|
368
458
|
}));
|
|
369
459
|
|
|
370
|
-
// Thin tick labels to prevent overlap (skip for band scales
|
|
371
|
-
const shouldThin = scales.y.type !== 'band' && !
|
|
460
|
+
// Thin tick labels to prevent overlap (skip for band scales, explicit tickCount, and values).
|
|
461
|
+
const shouldThin = scales.y.type !== 'band' && !axisConfig?.tickCount && !axisConfig?.values;
|
|
372
462
|
const ticks = shouldThin
|
|
373
463
|
? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText)
|
|
374
464
|
: allTicks;
|
|
375
465
|
|
|
466
|
+
// Prefer title over deprecated label, labelAngle over deprecated tickAngle
|
|
467
|
+
const axisTitle = axisConfig?.title ?? axisConfig?.label;
|
|
468
|
+
const tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
|
|
469
|
+
|
|
376
470
|
result.y = {
|
|
377
471
|
ticks,
|
|
378
472
|
// Y-axis gridlines are shown by default (standard editorial practice)
|
|
379
473
|
gridlines,
|
|
380
|
-
label:
|
|
474
|
+
label: axisTitle,
|
|
381
475
|
labelStyle: axisLabelStyle,
|
|
382
476
|
tickLabelStyle,
|
|
383
|
-
tickAngle
|
|
477
|
+
tickAngle,
|
|
384
478
|
start: { x: chartArea.x, y: chartArea.y },
|
|
385
479
|
end: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
480
|
+
orient: axisConfig?.orient,
|
|
481
|
+
domainLine: axisConfig?.domain,
|
|
482
|
+
tickMarks: axisConfig?.ticks,
|
|
483
|
+
offset: axisConfig?.offset,
|
|
484
|
+
titlePadding: axisConfig?.titlePadding,
|
|
485
|
+
labelPadding: axisConfig?.labelPadding,
|
|
486
|
+
labelOverlap: axisConfig?.labelOverlap,
|
|
487
|
+
labelFlush: axisConfig?.labelFlush,
|
|
386
488
|
};
|
|
387
489
|
}
|
|
388
490
|
|
package/src/layout/dimensions.ts
CHANGED
|
@@ -115,8 +115,8 @@ export function computeDimensions(
|
|
|
115
115
|
// Start with the total rect
|
|
116
116
|
const total: Rect = { x: 0, y: 0, width, height };
|
|
117
117
|
|
|
118
|
-
// Radial charts (
|
|
119
|
-
const isRadial = spec.
|
|
118
|
+
// Radial charts (arc) don't have axes, so skip axis space
|
|
119
|
+
const isRadial = spec.markType === 'arc';
|
|
120
120
|
const encoding = spec.encoding as Encoding;
|
|
121
121
|
|
|
122
122
|
// Estimate x-axis height below chart area: tick labels sit 14px below,
|
|
@@ -151,9 +151,13 @@ export function computeDimensions(
|
|
|
151
151
|
xAxisHeight = hasXAxisLabel ? 48 : 26;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Build margins: padding + chrome + axis space
|
|
154
|
+
// Build margins: padding + chrome + axis space.
|
|
155
|
+
// For radial charts (arc/donut), axes don't exist, so axisMargin is only
|
|
156
|
+
// added when there's actual chrome content that needs separation from the
|
|
157
|
+
// chart area. When chrome is empty the margin is just padding.
|
|
158
|
+
const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin;
|
|
155
159
|
const margins: Margins = {
|
|
156
|
-
top: padding + chrome.topHeight +
|
|
160
|
+
top: padding + chrome.topHeight + topAxisGap,
|
|
157
161
|
right: padding + (isRadial ? padding : axisMargin),
|
|
158
162
|
bottom: padding + chrome.bottomHeight + xAxisHeight,
|
|
159
163
|
left: padding + (isRadial ? padding : axisMargin),
|
|
@@ -162,9 +166,10 @@ export function computeDimensions(
|
|
|
162
166
|
// Dynamic right margin for line/area end-of-line labels.
|
|
163
167
|
// Only reserve space when labels will actually render (density != 'none').
|
|
164
168
|
const labelDensity = spec.labels.density;
|
|
165
|
-
if ((spec.
|
|
169
|
+
if ((spec.markType === 'line' || spec.markType === 'area') && labelDensity !== 'none') {
|
|
166
170
|
// Estimate label width from longest series name (color encoding domain)
|
|
167
|
-
const
|
|
171
|
+
const colorEnc = encoding.color;
|
|
172
|
+
const colorField = colorEnc && 'field' in colorEnc ? colorEnc.field : undefined;
|
|
168
173
|
if (colorField) {
|
|
169
174
|
let maxLabelWidth = 0;
|
|
170
175
|
const seen = new Set<string>();
|
|
@@ -185,8 +190,8 @@ export function computeDimensions(
|
|
|
185
190
|
// Dynamic left margin for y-axis labels
|
|
186
191
|
if (encoding.y && !isRadial) {
|
|
187
192
|
if (
|
|
188
|
-
spec.
|
|
189
|
-
spec.
|
|
193
|
+
spec.markType === 'bar' ||
|
|
194
|
+
spec.markType === 'circle' ||
|
|
190
195
|
encoding.y.type === 'nominal' ||
|
|
191
196
|
encoding.y.type === 'ordinal'
|
|
192
197
|
) {
|
|
@@ -272,7 +277,8 @@ export function computeDimensions(
|
|
|
272
277
|
);
|
|
273
278
|
|
|
274
279
|
// Recalculate top/bottom margins with stripped chrome
|
|
275
|
-
const
|
|
280
|
+
const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin;
|
|
281
|
+
const newTop = padding + fallbackChrome.topHeight + fallbackTopAxisGap;
|
|
276
282
|
const topDelta = margins.top - newTop;
|
|
277
283
|
const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
|
|
278
284
|
const bottomDelta = margins.bottom - newBottom;
|