@opendata-ai/openchart-engine 6.0.0 → 6.1.1
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/layout/scales.ts
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* nominal/ordinal -> scaleBand() or scaleOrdinal(), depending on context.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
DataRow,
|
|
11
|
+
Encoding,
|
|
12
|
+
EncodingChannel,
|
|
13
|
+
Rect,
|
|
14
|
+
ScaleType,
|
|
15
|
+
} from '@opendata-ai/openchart-core';
|
|
10
16
|
import { extent, max, min } from 'd3-array';
|
|
11
17
|
import type {
|
|
12
18
|
ScaleBand,
|
|
@@ -14,9 +20,28 @@ import type {
|
|
|
14
20
|
ScaleLogarithmic,
|
|
15
21
|
ScaleOrdinal,
|
|
16
22
|
ScalePoint,
|
|
23
|
+
ScalePower,
|
|
24
|
+
ScaleQuantile,
|
|
25
|
+
ScaleQuantize,
|
|
26
|
+
ScaleSymLog,
|
|
27
|
+
ScaleThreshold,
|
|
17
28
|
ScaleTime,
|
|
18
29
|
} from 'd3-scale';
|
|
19
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
scaleBand,
|
|
32
|
+
scaleLinear,
|
|
33
|
+
scaleLog,
|
|
34
|
+
scaleOrdinal,
|
|
35
|
+
scalePoint,
|
|
36
|
+
scalePow,
|
|
37
|
+
scaleQuantile,
|
|
38
|
+
scaleQuantize,
|
|
39
|
+
scaleSqrt,
|
|
40
|
+
scaleSymlog,
|
|
41
|
+
scaleThreshold,
|
|
42
|
+
scaleTime,
|
|
43
|
+
scaleUtc,
|
|
44
|
+
} from 'd3-scale';
|
|
20
45
|
|
|
21
46
|
import type { NormalizedChartSpec } from '../compiler/types';
|
|
22
47
|
|
|
@@ -24,11 +49,19 @@ import type { NormalizedChartSpec } from '../compiler/types';
|
|
|
24
49
|
// Types
|
|
25
50
|
// ---------------------------------------------------------------------------
|
|
26
51
|
|
|
27
|
-
/** Continuous D3 scales (linear, time, log) that support .ticks() and .nice(). */
|
|
52
|
+
/** Continuous D3 scales (linear, time, log, pow, sqrt, symlog) that support .ticks() and .nice(). */
|
|
28
53
|
export type D3ContinuousScale =
|
|
29
54
|
| ScaleLinear<number, number>
|
|
30
55
|
| ScaleTime<number, number>
|
|
31
|
-
| ScaleLogarithmic<number, number
|
|
56
|
+
| ScaleLogarithmic<number, number>
|
|
57
|
+
| ScalePower<number, number>
|
|
58
|
+
| ScaleSymLog<number, number>;
|
|
59
|
+
|
|
60
|
+
/** Discretizing D3 scales (quantile, quantize, threshold). */
|
|
61
|
+
export type D3DiscretizingScale =
|
|
62
|
+
| ScaleQuantile<number>
|
|
63
|
+
| ScaleQuantize<number>
|
|
64
|
+
| ScaleThreshold<number, number>;
|
|
32
65
|
|
|
33
66
|
/** Categorical D3 scales (band, point, ordinal) that support .domain() as string[]. */
|
|
34
67
|
export type D3CategoricalScale =
|
|
@@ -37,11 +70,14 @@ export type D3CategoricalScale =
|
|
|
37
70
|
| ScaleOrdinal<string, string>;
|
|
38
71
|
|
|
39
72
|
/** Union of all D3 scale types used by the engine. */
|
|
40
|
-
export type D3Scale = D3ContinuousScale | D3CategoricalScale;
|
|
73
|
+
export type D3Scale = D3ContinuousScale | D3CategoricalScale | D3DiscretizingScale;
|
|
41
74
|
|
|
42
75
|
/** A sequential color scale mapping numbers to color strings. */
|
|
43
76
|
export type D3SequentialColorScale = ScaleLinear<string, string>;
|
|
44
77
|
|
|
78
|
+
/** All resolved scale type identifiers. */
|
|
79
|
+
export type ResolvedScaleType = ScaleType | 'sequential';
|
|
80
|
+
|
|
45
81
|
/**
|
|
46
82
|
* A resolved scale wrapping a d3 scale with type metadata.
|
|
47
83
|
* We need to carry the scale type around so axes and marks know
|
|
@@ -52,7 +88,7 @@ export interface ResolvedScale {
|
|
|
52
88
|
/** The d3 scale function. Maps domain value -> pixel position or color. */
|
|
53
89
|
scale: D3Scale;
|
|
54
90
|
/** The scale type for downstream use. */
|
|
55
|
-
type:
|
|
91
|
+
type: ResolvedScaleType;
|
|
56
92
|
/** The encoding channel this scale was derived from. */
|
|
57
93
|
channel: EncodingChannel;
|
|
58
94
|
}
|
|
@@ -104,6 +140,24 @@ function uniqueStrings(values: unknown[]): string[] {
|
|
|
104
140
|
return result;
|
|
105
141
|
}
|
|
106
142
|
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Helpers: apply common scale config
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/** Apply clamp and reverse config to a continuous scale. */
|
|
148
|
+
function applyContinuousConfig(
|
|
149
|
+
scale: { clamp(v: boolean): unknown; range(): number[]; range(r: number[]): unknown },
|
|
150
|
+
channel: EncodingChannel,
|
|
151
|
+
): void {
|
|
152
|
+
if (channel.scale?.clamp) {
|
|
153
|
+
scale.clamp(true);
|
|
154
|
+
}
|
|
155
|
+
if (channel.scale?.reverse) {
|
|
156
|
+
const [r0, r1] = scale.range() as number[];
|
|
157
|
+
scale.range([r1, r0]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
107
161
|
// ---------------------------------------------------------------------------
|
|
108
162
|
// Scale builders
|
|
109
163
|
// ---------------------------------------------------------------------------
|
|
@@ -124,10 +178,32 @@ function buildTimeScale(
|
|
|
124
178
|
if (channel.scale?.nice !== false) {
|
|
125
179
|
scale.nice();
|
|
126
180
|
}
|
|
181
|
+
applyContinuousConfig(scale, channel);
|
|
127
182
|
|
|
128
183
|
return { scale, type: 'time', channel };
|
|
129
184
|
}
|
|
130
185
|
|
|
186
|
+
function buildUtcScale(
|
|
187
|
+
channel: EncodingChannel,
|
|
188
|
+
data: DataRow[],
|
|
189
|
+
rangeStart: number,
|
|
190
|
+
rangeEnd: number,
|
|
191
|
+
): ResolvedScale {
|
|
192
|
+
const values = parseDates(fieldValues(data, channel.field));
|
|
193
|
+
const domain = channel.scale?.domain
|
|
194
|
+
? [new Date(channel.scale.domain[0] as string), new Date(channel.scale.domain[1] as string)]
|
|
195
|
+
: (extent(values) as [Date, Date]);
|
|
196
|
+
|
|
197
|
+
const scale = scaleUtc().domain(domain).range([rangeStart, rangeEnd]);
|
|
198
|
+
|
|
199
|
+
if (channel.scale?.nice !== false) {
|
|
200
|
+
scale.nice();
|
|
201
|
+
}
|
|
202
|
+
applyContinuousConfig(scale, channel);
|
|
203
|
+
|
|
204
|
+
return { scale, type: 'utc', channel };
|
|
205
|
+
}
|
|
206
|
+
|
|
131
207
|
function buildLinearScale(
|
|
132
208
|
channel: EncodingChannel,
|
|
133
209
|
data: DataRow[],
|
|
@@ -159,6 +235,7 @@ function buildLinearScale(
|
|
|
159
235
|
if (channel.scale?.nice !== false) {
|
|
160
236
|
scale.nice();
|
|
161
237
|
}
|
|
238
|
+
applyContinuousConfig(scale, channel);
|
|
162
239
|
|
|
163
240
|
return { scale, type: 'linear', channel };
|
|
164
241
|
}
|
|
@@ -170,14 +247,190 @@ function buildLogScale(
|
|
|
170
247
|
rangeEnd: number,
|
|
171
248
|
): ResolvedScale {
|
|
172
249
|
const values = parseNumbers(fieldValues(data, channel.field)).filter((v) => v > 0);
|
|
173
|
-
const domainMin =
|
|
174
|
-
|
|
250
|
+
const domainMin = channel.scale?.domain
|
|
251
|
+
? (channel.scale.domain as [number, number])[0]
|
|
252
|
+
: (min(values) ?? 1);
|
|
253
|
+
const domainMax = channel.scale?.domain
|
|
254
|
+
? (channel.scale.domain as [number, number])[1]
|
|
255
|
+
: (max(values) ?? 10);
|
|
175
256
|
|
|
176
|
-
const scale = scaleLog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd])
|
|
257
|
+
const scale = scaleLog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
258
|
+
|
|
259
|
+
if (channel.scale?.base !== undefined) {
|
|
260
|
+
scale.base(channel.scale.base);
|
|
261
|
+
}
|
|
262
|
+
if (channel.scale?.nice !== false) {
|
|
263
|
+
scale.nice();
|
|
264
|
+
}
|
|
265
|
+
applyContinuousConfig(scale, channel);
|
|
177
266
|
|
|
178
267
|
return { scale, type: 'log', channel };
|
|
179
268
|
}
|
|
180
269
|
|
|
270
|
+
function buildPowScale(
|
|
271
|
+
channel: EncodingChannel,
|
|
272
|
+
data: DataRow[],
|
|
273
|
+
rangeStart: number,
|
|
274
|
+
rangeEnd: number,
|
|
275
|
+
): ResolvedScale {
|
|
276
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
277
|
+
|
|
278
|
+
let domainMin: number;
|
|
279
|
+
let domainMax: number;
|
|
280
|
+
|
|
281
|
+
if (channel.scale?.domain) {
|
|
282
|
+
[domainMin, domainMax] = channel.scale.domain as [number, number];
|
|
283
|
+
} else {
|
|
284
|
+
domainMin = min(values) ?? 0;
|
|
285
|
+
domainMax = max(values) ?? 1;
|
|
286
|
+
if (channel.scale?.zero !== false) {
|
|
287
|
+
domainMin = Math.min(0, domainMin);
|
|
288
|
+
domainMax = Math.max(0, domainMax);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const scale = scalePow().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
293
|
+
|
|
294
|
+
if (channel.scale?.exponent !== undefined) {
|
|
295
|
+
scale.exponent(channel.scale.exponent);
|
|
296
|
+
}
|
|
297
|
+
if (channel.scale?.nice !== false) {
|
|
298
|
+
scale.nice();
|
|
299
|
+
}
|
|
300
|
+
applyContinuousConfig(scale, channel);
|
|
301
|
+
|
|
302
|
+
return { scale, type: 'pow', channel };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildSqrtScale(
|
|
306
|
+
channel: EncodingChannel,
|
|
307
|
+
data: DataRow[],
|
|
308
|
+
rangeStart: number,
|
|
309
|
+
rangeEnd: number,
|
|
310
|
+
): ResolvedScale {
|
|
311
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
312
|
+
|
|
313
|
+
let domainMin: number;
|
|
314
|
+
let domainMax: number;
|
|
315
|
+
|
|
316
|
+
if (channel.scale?.domain) {
|
|
317
|
+
[domainMin, domainMax] = channel.scale.domain as [number, number];
|
|
318
|
+
} else {
|
|
319
|
+
domainMin = min(values) ?? 0;
|
|
320
|
+
domainMax = max(values) ?? 1;
|
|
321
|
+
if (channel.scale?.zero !== false) {
|
|
322
|
+
domainMin = Math.min(0, domainMin);
|
|
323
|
+
domainMax = Math.max(0, domainMax);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const scale = scaleSqrt().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
328
|
+
|
|
329
|
+
if (channel.scale?.nice !== false) {
|
|
330
|
+
scale.nice();
|
|
331
|
+
}
|
|
332
|
+
applyContinuousConfig(scale, channel);
|
|
333
|
+
|
|
334
|
+
return { scale, type: 'sqrt', channel };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildSymlogScale(
|
|
338
|
+
channel: EncodingChannel,
|
|
339
|
+
data: DataRow[],
|
|
340
|
+
rangeStart: number,
|
|
341
|
+
rangeEnd: number,
|
|
342
|
+
): ResolvedScale {
|
|
343
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
344
|
+
|
|
345
|
+
let domainMin: number;
|
|
346
|
+
let domainMax: number;
|
|
347
|
+
|
|
348
|
+
if (channel.scale?.domain) {
|
|
349
|
+
[domainMin, domainMax] = channel.scale.domain as [number, number];
|
|
350
|
+
} else {
|
|
351
|
+
domainMin = min(values) ?? 0;
|
|
352
|
+
domainMax = max(values) ?? 1;
|
|
353
|
+
if (channel.scale?.zero !== false) {
|
|
354
|
+
domainMin = Math.min(0, domainMin);
|
|
355
|
+
domainMax = Math.max(0, domainMax);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const scale = scaleSymlog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
360
|
+
|
|
361
|
+
if (channel.scale?.constant !== undefined) {
|
|
362
|
+
scale.constant(channel.scale.constant);
|
|
363
|
+
}
|
|
364
|
+
if (channel.scale?.nice !== false) {
|
|
365
|
+
scale.nice();
|
|
366
|
+
}
|
|
367
|
+
applyContinuousConfig(scale, channel);
|
|
368
|
+
|
|
369
|
+
return { scale, type: 'symlog', channel };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function buildQuantileScale(
|
|
373
|
+
channel: EncodingChannel,
|
|
374
|
+
data: DataRow[],
|
|
375
|
+
rangeStart: number,
|
|
376
|
+
rangeEnd: number,
|
|
377
|
+
): ResolvedScale {
|
|
378
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
379
|
+
const range = channel.scale?.range
|
|
380
|
+
? (channel.scale.range as number[])
|
|
381
|
+
: evenRange(rangeStart, rangeEnd, 4);
|
|
382
|
+
|
|
383
|
+
const scale = scaleQuantile<number>().domain(values).range(range);
|
|
384
|
+
|
|
385
|
+
return { scale: scale as unknown as D3Scale, type: 'quantile', channel };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildQuantizeScale(
|
|
389
|
+
channel: EncodingChannel,
|
|
390
|
+
data: DataRow[],
|
|
391
|
+
rangeStart: number,
|
|
392
|
+
rangeEnd: number,
|
|
393
|
+
): ResolvedScale {
|
|
394
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
395
|
+
const domainMin = channel.scale?.domain
|
|
396
|
+
? (channel.scale.domain as [number, number])[0]
|
|
397
|
+
: (min(values) ?? 0);
|
|
398
|
+
const domainMax = channel.scale?.domain
|
|
399
|
+
? (channel.scale.domain as [number, number])[1]
|
|
400
|
+
: (max(values) ?? 1);
|
|
401
|
+
const range = channel.scale?.range
|
|
402
|
+
? (channel.scale.range as number[])
|
|
403
|
+
: evenRange(rangeStart, rangeEnd, 4);
|
|
404
|
+
|
|
405
|
+
const scale = scaleQuantize<number>().domain([domainMin, domainMax]).range(range);
|
|
406
|
+
|
|
407
|
+
return { scale: scale as unknown as D3Scale, type: 'quantize', channel };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function buildThresholdScale(
|
|
411
|
+
channel: EncodingChannel,
|
|
412
|
+
_data: DataRow[],
|
|
413
|
+
rangeStart: number,
|
|
414
|
+
rangeEnd: number,
|
|
415
|
+
): ResolvedScale {
|
|
416
|
+
// Threshold scales require explicit domain breakpoints
|
|
417
|
+
const domainBreaks = channel.scale?.domain ? (channel.scale.domain as number[]) : [0.5];
|
|
418
|
+
const range = channel.scale?.range
|
|
419
|
+
? (channel.scale.range as number[])
|
|
420
|
+
: evenRange(rangeStart, rangeEnd, domainBreaks.length + 1);
|
|
421
|
+
|
|
422
|
+
const scale = scaleThreshold<number, number>().domain(domainBreaks).range(range);
|
|
423
|
+
|
|
424
|
+
return { scale: scale as unknown as D3Scale, type: 'threshold', channel };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Generate an evenly-spaced range of `count` values between start and end. */
|
|
428
|
+
function evenRange(start: number, end: number, count: number): number[] {
|
|
429
|
+
if (count <= 1) return [start];
|
|
430
|
+
const step = (end - start) / (count - 1);
|
|
431
|
+
return Array.from({ length: count }, (_, i) => start + step * i);
|
|
432
|
+
}
|
|
433
|
+
|
|
181
434
|
function buildBandScale(
|
|
182
435
|
channel: EncodingChannel,
|
|
183
436
|
data: DataRow[],
|
|
@@ -188,7 +441,19 @@ function buildBandScale(
|
|
|
188
441
|
? (channel.scale.domain as string[])
|
|
189
442
|
: uniqueStrings(fieldValues(data, channel.field));
|
|
190
443
|
|
|
191
|
-
const
|
|
444
|
+
const padding = channel.scale?.padding ?? 0.35;
|
|
445
|
+
const scale = scaleBand().domain(values).range([rangeStart, rangeEnd]).padding(padding);
|
|
446
|
+
|
|
447
|
+
if (channel.scale?.paddingInner !== undefined) {
|
|
448
|
+
scale.paddingInner(channel.scale.paddingInner);
|
|
449
|
+
}
|
|
450
|
+
if (channel.scale?.paddingOuter !== undefined) {
|
|
451
|
+
scale.paddingOuter(channel.scale.paddingOuter);
|
|
452
|
+
}
|
|
453
|
+
if (channel.scale?.reverse) {
|
|
454
|
+
const [r0, r1] = scale.range();
|
|
455
|
+
scale.range([r1, r0]);
|
|
456
|
+
}
|
|
192
457
|
|
|
193
458
|
return { scale, type: 'band', channel };
|
|
194
459
|
}
|
|
@@ -203,7 +468,13 @@ function buildPointScale(
|
|
|
203
468
|
? (channel.scale.domain as string[])
|
|
204
469
|
: uniqueStrings(fieldValues(data, channel.field));
|
|
205
470
|
|
|
206
|
-
const
|
|
471
|
+
const padding = channel.scale?.padding ?? 0.5;
|
|
472
|
+
const scale = scalePoint().domain(values).range([rangeStart, rangeEnd]).padding(padding);
|
|
473
|
+
|
|
474
|
+
if (channel.scale?.reverse) {
|
|
475
|
+
const [r0, r1] = scale.range();
|
|
476
|
+
scale.range([r1, r0]);
|
|
477
|
+
}
|
|
207
478
|
|
|
208
479
|
return { scale, type: 'point', channel };
|
|
209
480
|
}
|
|
@@ -261,10 +532,24 @@ function buildPositionalScale(
|
|
|
261
532
|
switch (channel.scale.type) {
|
|
262
533
|
case 'time':
|
|
263
534
|
return buildTimeScale(channel, data, rangeStart, rangeEnd);
|
|
535
|
+
case 'utc':
|
|
536
|
+
return buildUtcScale(channel, data, rangeStart, rangeEnd);
|
|
264
537
|
case 'linear':
|
|
265
538
|
return buildLinearScale(channel, data, rangeStart, rangeEnd);
|
|
266
539
|
case 'log':
|
|
267
540
|
return buildLogScale(channel, data, rangeStart, rangeEnd);
|
|
541
|
+
case 'pow':
|
|
542
|
+
return buildPowScale(channel, data, rangeStart, rangeEnd);
|
|
543
|
+
case 'sqrt':
|
|
544
|
+
return buildSqrtScale(channel, data, rangeStart, rangeEnd);
|
|
545
|
+
case 'symlog':
|
|
546
|
+
return buildSymlogScale(channel, data, rangeStart, rangeEnd);
|
|
547
|
+
case 'quantile':
|
|
548
|
+
return buildQuantileScale(channel, data, rangeStart, rangeEnd);
|
|
549
|
+
case 'quantize':
|
|
550
|
+
return buildQuantizeScale(channel, data, rangeStart, rangeEnd);
|
|
551
|
+
case 'threshold':
|
|
552
|
+
return buildThresholdScale(channel, data, rangeStart, rangeEnd);
|
|
268
553
|
case 'band':
|
|
269
554
|
return buildBandScale(channel, data, rangeStart, rangeEnd);
|
|
270
555
|
case 'point':
|
|
@@ -282,12 +567,8 @@ function buildPositionalScale(
|
|
|
282
567
|
return buildLinearScale(channel, data, rangeStart, rangeEnd);
|
|
283
568
|
case 'nominal':
|
|
284
569
|
case 'ordinal':
|
|
285
|
-
// Bar
|
|
286
|
-
if (
|
|
287
|
-
(chartType === 'bar' && axis === 'y') ||
|
|
288
|
-
(chartType === 'column' && axis === 'x') ||
|
|
289
|
-
(chartType === 'dot' && axis === 'y')
|
|
290
|
-
) {
|
|
570
|
+
// Bar charts use band scales for their categorical axis (both orientations)
|
|
571
|
+
if (chartType === 'bar' || (chartType === 'circle' && axis === 'y')) {
|
|
291
572
|
return buildBandScale(channel, data, rangeStart, rangeEnd);
|
|
292
573
|
}
|
|
293
574
|
return buildPointScale(channel, data, rangeStart, rangeEnd);
|
|
@@ -317,7 +598,7 @@ export function computeScales(
|
|
|
317
598
|
const encoding = spec.encoding as Encoding;
|
|
318
599
|
|
|
319
600
|
// Scatter/bubble charts should NOT include zero by default (tight domain fits data range)
|
|
320
|
-
if (spec.
|
|
601
|
+
if (spec.markType === 'point') {
|
|
321
602
|
if (encoding.x?.type === 'quantitative' && encoding.x.scale?.zero === undefined) {
|
|
322
603
|
if (!encoding.x.scale) {
|
|
323
604
|
(encoding.x as { scale?: Record<string, unknown> }).scale = { zero: false };
|
|
@@ -338,7 +619,7 @@ export function computeScales(
|
|
|
338
619
|
// For stacked bars, the x-domain needs the max category sum, not max individual value.
|
|
339
620
|
// Without this, stacked bars would clip past the chart area.
|
|
340
621
|
let xData = data;
|
|
341
|
-
if (spec.
|
|
622
|
+
if (spec.markType === 'bar' && encoding.color && encoding.x.type === 'quantitative') {
|
|
342
623
|
const yField = encoding.y?.field;
|
|
343
624
|
const xField = encoding.x.field;
|
|
344
625
|
if (yField) {
|
|
@@ -361,18 +642,23 @@ export function computeScales(
|
|
|
361
642
|
xData,
|
|
362
643
|
chartArea.x,
|
|
363
644
|
chartArea.x + chartArea.width,
|
|
364
|
-
spec.
|
|
645
|
+
spec.markType,
|
|
365
646
|
'x',
|
|
366
647
|
);
|
|
367
648
|
}
|
|
368
649
|
|
|
369
650
|
if (encoding.y) {
|
|
370
|
-
// For stacked
|
|
371
|
-
// sum, not the max individual value. Without this, stacked marks
|
|
372
|
-
// above the chart area.
|
|
651
|
+
// For stacked vertical bars and stacked areas, the y-domain needs the max
|
|
652
|
+
// category sum, not the max individual value. Without this, stacked marks
|
|
653
|
+
// would clip above the chart area.
|
|
654
|
+
// Vertical bar = x is categorical and y is quantitative (old 'column' chart type).
|
|
373
655
|
let yData = data;
|
|
656
|
+
const isVerticalBar =
|
|
657
|
+
spec.markType === 'bar' &&
|
|
658
|
+
(encoding.x?.type === 'nominal' || encoding.x?.type === 'ordinal') &&
|
|
659
|
+
encoding.y.type === 'quantitative';
|
|
374
660
|
if (
|
|
375
|
-
(
|
|
661
|
+
(isVerticalBar || spec.markType === 'area') &&
|
|
376
662
|
encoding.color &&
|
|
377
663
|
encoding.y.type === 'quantitative'
|
|
378
664
|
) {
|
|
@@ -399,7 +685,7 @@ export function computeScales(
|
|
|
399
685
|
yData,
|
|
400
686
|
chartArea.y + chartArea.height,
|
|
401
687
|
chartArea.y,
|
|
402
|
-
spec.
|
|
688
|
+
spec.markType,
|
|
403
689
|
'y',
|
|
404
690
|
);
|
|
405
691
|
}
|
|
@@ -418,12 +704,15 @@ export function computeScales(
|
|
|
418
704
|
'#858078',
|
|
419
705
|
];
|
|
420
706
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
707
|
+
// Only build color scales for field-based encodings, not conditional value defs
|
|
708
|
+
if ('field' in encoding.color) {
|
|
709
|
+
if (encoding.color.type === 'quantitative') {
|
|
710
|
+
// Sequential color scale for value-based coloring
|
|
711
|
+
result.color = buildSequentialColorScale(encoding.color, data, defaultPalette);
|
|
712
|
+
} else {
|
|
713
|
+
// Categorical color scale for nominal/ordinal grouping
|
|
714
|
+
result.color = buildOrdinalColorScale(encoding.color, data, defaultPalette);
|
|
715
|
+
}
|
|
427
716
|
}
|
|
428
717
|
}
|
|
429
718
|
|
package/src/legend/compute.ts
CHANGED
|
@@ -44,13 +44,13 @@ const TOP_LEGEND_MAX_ROWS = 2;
|
|
|
44
44
|
// Helpers
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
|
|
47
|
-
/** Determine the swatch shape based on
|
|
48
|
-
function swatchShapeForType(
|
|
49
|
-
switch (
|
|
47
|
+
/** Determine the swatch shape based on mark type. */
|
|
48
|
+
function swatchShapeForType(markType: string): LegendEntry['shape'] {
|
|
49
|
+
switch (markType) {
|
|
50
50
|
case 'line':
|
|
51
51
|
return 'line';
|
|
52
|
-
case '
|
|
53
|
-
case '
|
|
52
|
+
case 'point':
|
|
53
|
+
case 'circle':
|
|
54
54
|
return 'circle';
|
|
55
55
|
default:
|
|
56
56
|
return 'square';
|
|
@@ -62,12 +62,15 @@ function extractColorEntries(spec: NormalizedChartSpec, theme: ResolvedTheme): L
|
|
|
62
62
|
const colorEnc = spec.encoding.color;
|
|
63
63
|
if (!colorEnc) return [];
|
|
64
64
|
|
|
65
|
+
// Conditional color definitions don't produce legend entries
|
|
66
|
+
if ('condition' in colorEnc) return [];
|
|
67
|
+
|
|
65
68
|
// Sequential (quantitative) color doesn't produce discrete legend entries
|
|
66
69
|
if (colorEnc.type === 'quantitative') return [];
|
|
67
70
|
|
|
68
71
|
const uniqueValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
|
|
69
72
|
const palette = theme.colors.categorical;
|
|
70
|
-
const shape = swatchShapeForType(spec.
|
|
73
|
+
const shape = swatchShapeForType(spec.markType);
|
|
71
74
|
|
|
72
75
|
return uniqueValues.map((value, i) => ({
|
|
73
76
|
label: value,
|
|
@@ -11,7 +11,8 @@ const _chartArea: Rect = { x: 50, y: 20, width: 500, height: 300 };
|
|
|
11
11
|
|
|
12
12
|
function makeLineSpec(): NormalizedChartSpec {
|
|
13
13
|
return {
|
|
14
|
-
|
|
14
|
+
markType: 'line',
|
|
15
|
+
markDef: { type: 'line' },
|
|
15
16
|
data: [
|
|
16
17
|
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
17
18
|
{ date: '2021-01-01', value: 40, country: 'US' },
|
|
@@ -34,7 +35,8 @@ function makeLineSpec(): NormalizedChartSpec {
|
|
|
34
35
|
|
|
35
36
|
function makeBarSpec(): NormalizedChartSpec {
|
|
36
37
|
return {
|
|
37
|
-
|
|
38
|
+
markType: 'bar',
|
|
39
|
+
markDef: { type: 'bar' },
|
|
38
40
|
data: [
|
|
39
41
|
{ category: 'A', value: 100 },
|
|
40
42
|
{ category: 'B', value: 200 },
|
|
@@ -55,7 +57,8 @@ function makeBarSpec(): NormalizedChartSpec {
|
|
|
55
57
|
|
|
56
58
|
function makePieSpec(): NormalizedChartSpec {
|
|
57
59
|
return {
|
|
58
|
-
|
|
60
|
+
markType: 'arc',
|
|
61
|
+
markDef: { type: 'arc' },
|
|
59
62
|
data: [
|
|
60
63
|
{ segment: 'Alpha', amount: 30 },
|
|
61
64
|
{ segment: 'Beta', amount: 50 },
|
|
@@ -282,7 +285,8 @@ describe('computeTooltipDescriptors', () => {
|
|
|
282
285
|
const spec = makeLineSpec();
|
|
283
286
|
const marks: Mark[] = [
|
|
284
287
|
{
|
|
285
|
-
|
|
288
|
+
markType: 'line',
|
|
289
|
+
markDef: { type: 'line' },
|
|
286
290
|
points: [
|
|
287
291
|
{ x: 100, y: 200 },
|
|
288
292
|
{ x: 300, y: 100 },
|
|
@@ -302,7 +306,8 @@ describe('computeTooltipDescriptors', () => {
|
|
|
302
306
|
const spec = makeLineSpec();
|
|
303
307
|
const marks: Mark[] = [
|
|
304
308
|
{
|
|
305
|
-
|
|
309
|
+
markType: 'area',
|
|
310
|
+
markDef: { type: 'area' },
|
|
306
311
|
topPoints: [{ x: 100, y: 200 }],
|
|
307
312
|
bottomPoints: [{ x: 100, y: 300 }],
|
|
308
313
|
path: 'M0,0',
|
package/src/tooltips/compute.ts
CHANGED
|
@@ -70,8 +70,8 @@ function buildFields(row: DataRow, encoding: Encoding, color?: string): TooltipF
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// Size (for scatter/bubble)
|
|
74
|
-
if (encoding.size) {
|
|
73
|
+
// Size (for scatter/bubble) - skip conditional size definitions
|
|
74
|
+
if (encoding.size && 'field' in encoding.size) {
|
|
75
75
|
fields.push({
|
|
76
76
|
label: encoding.size.axis?.label ?? encoding.size.field,
|
|
77
77
|
value: formatValue(row[encoding.size.field], encoding.size.type, encoding.size.axis?.format),
|
|
@@ -103,8 +103,8 @@ function getTooltipTitle(row: DataRow, encoding: Encoding): string | undefined {
|
|
|
103
103
|
return String(row[encoding.y.field] ?? '');
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// For color-encoded series, use the series name
|
|
107
|
-
if (encoding.color) {
|
|
106
|
+
// For color-encoded series, use the series name (skip conditional defs)
|
|
107
|
+
if (encoding.color && 'field' in encoding.color) {
|
|
108
108
|
return String(row[encoding.color.field] ?? '');
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -116,12 +116,20 @@ function getTooltipTitle(row: DataRow, encoding: Encoding): string | undefined {
|
|
|
116
116
|
// ---------------------------------------------------------------------------
|
|
117
117
|
|
|
118
118
|
function tooltipsForLine(
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
mark: LineMark,
|
|
120
|
+
encoding: Encoding,
|
|
121
121
|
_markIndex: number,
|
|
122
122
|
): Array<[string, TooltipContent]> {
|
|
123
|
-
//
|
|
124
|
-
//
|
|
123
|
+
// Populate tooltip content on each dataPoint for voronoi overlay lookup.
|
|
124
|
+
// Line marks don't emit per-mark tooltip descriptors (the overlay handles it).
|
|
125
|
+
if (mark.dataPoints) {
|
|
126
|
+
for (const dp of mark.dataPoints) {
|
|
127
|
+
dp.tooltip = {
|
|
128
|
+
title: getTooltipTitle(dp.datum, encoding),
|
|
129
|
+
fields: buildFields(dp.datum, encoding, mark.stroke),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
125
133
|
return [];
|
|
126
134
|
}
|
|
127
135
|
|
|
@@ -156,8 +164,9 @@ function tooltipsForArc(
|
|
|
156
164
|
const fields: TooltipField[] = [];
|
|
157
165
|
|
|
158
166
|
// For pie/donut, show the category and its value
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
const colorEnc = encoding.color && 'field' in encoding.color ? encoding.color : undefined;
|
|
168
|
+
if (colorEnc) {
|
|
169
|
+
const categoryName = String(row[colorEnc.field] ?? '');
|
|
161
170
|
if (encoding.y) {
|
|
162
171
|
fields.push({
|
|
163
172
|
label: categoryName,
|
|
@@ -173,17 +182,26 @@ function tooltipsForArc(
|
|
|
173
182
|
});
|
|
174
183
|
}
|
|
175
184
|
|
|
176
|
-
const title =
|
|
185
|
+
const title = colorEnc ? String(row[colorEnc.field] ?? '') : undefined;
|
|
177
186
|
|
|
178
187
|
return [[`arc-${markIndex}`, { title, fields }]];
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
function tooltipsForArea(
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
mark: AreaMark,
|
|
192
|
+
encoding: Encoding,
|
|
184
193
|
_markIndex: number,
|
|
185
194
|
): Array<[string, TooltipContent]> {
|
|
186
|
-
//
|
|
195
|
+
// Populate tooltip content on each dataPoint for voronoi overlay lookup.
|
|
196
|
+
// Area marks don't emit per-mark tooltip descriptors (the overlay handles it).
|
|
197
|
+
if (mark.dataPoints) {
|
|
198
|
+
for (const dp of mark.dataPoints) {
|
|
199
|
+
dp.tooltip = {
|
|
200
|
+
title: getTooltipTitle(dp.datum, encoding),
|
|
201
|
+
fields: buildFields(dp.datum, encoding, mark.fill),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
187
205
|
return [];
|
|
188
206
|
}
|
|
189
207
|
|