@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.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 +175 -12
- package/dist/index.js +917 -168
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -630
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1503
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -748
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
package/src/layout/scales.ts
DELETED
|
@@ -1,1053 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scale computation from encoding spec + data.
|
|
3
|
-
*
|
|
4
|
-
* Creates D3 scales that map data values to pixel positions.
|
|
5
|
-
* Temporal -> scaleTime(), quantitative -> scaleLinear(),
|
|
6
|
-
* nominal/ordinal -> scaleBand() or scaleOrdinal(), depending on context.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
DataRow,
|
|
11
|
-
Encoding,
|
|
12
|
-
EncodingChannel,
|
|
13
|
-
FieldFormatContext,
|
|
14
|
-
Rect,
|
|
15
|
-
ScaleType,
|
|
16
|
-
} from '@opendata-ai/openchart-core';
|
|
17
|
-
import { computeFieldFormatContext } from '@opendata-ai/openchart-core';
|
|
18
|
-
import { extent, max, min } from 'd3-array';
|
|
19
|
-
import type {
|
|
20
|
-
ScaleBand,
|
|
21
|
-
ScaleLinear,
|
|
22
|
-
ScaleLogarithmic,
|
|
23
|
-
ScaleOrdinal,
|
|
24
|
-
ScalePoint,
|
|
25
|
-
ScalePower,
|
|
26
|
-
ScaleQuantile,
|
|
27
|
-
ScaleQuantize,
|
|
28
|
-
ScaleSymLog,
|
|
29
|
-
ScaleThreshold,
|
|
30
|
-
ScaleTime,
|
|
31
|
-
} from 'd3-scale';
|
|
32
|
-
import {
|
|
33
|
-
scaleBand,
|
|
34
|
-
scaleLinear,
|
|
35
|
-
scaleLog,
|
|
36
|
-
scaleOrdinal,
|
|
37
|
-
scalePoint,
|
|
38
|
-
scalePow,
|
|
39
|
-
scaleQuantile,
|
|
40
|
-
scaleQuantize,
|
|
41
|
-
scaleSqrt,
|
|
42
|
-
scaleSymlog,
|
|
43
|
-
scaleThreshold,
|
|
44
|
-
scaleTime,
|
|
45
|
-
scaleUtc,
|
|
46
|
-
} from 'd3-scale';
|
|
47
|
-
|
|
48
|
-
import type { NormalizedChartSpec } from '../compiler/types';
|
|
49
|
-
import { DEFAULT_BIN_COUNT, sampleRampColors } from '../legend/continuous';
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Types
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
/** Continuous D3 scales (linear, time, log, pow, sqrt, symlog) that support .ticks() and .nice(). */
|
|
56
|
-
export type D3ContinuousScale =
|
|
57
|
-
| ScaleLinear<number, number>
|
|
58
|
-
| ScaleTime<number, number>
|
|
59
|
-
| ScaleLogarithmic<number, number>
|
|
60
|
-
| ScalePower<number, number>
|
|
61
|
-
| ScaleSymLog<number, number>;
|
|
62
|
-
|
|
63
|
-
/** Discretizing D3 scales (quantile, quantize, threshold). */
|
|
64
|
-
export type D3DiscretizingScale =
|
|
65
|
-
| ScaleQuantile<number>
|
|
66
|
-
| ScaleQuantize<number>
|
|
67
|
-
| ScaleThreshold<number, number>;
|
|
68
|
-
|
|
69
|
-
/** Categorical D3 scales (band, point, ordinal) that support .domain() as string[]. */
|
|
70
|
-
export type D3CategoricalScale =
|
|
71
|
-
| ScaleBand<string>
|
|
72
|
-
| ScalePoint<string>
|
|
73
|
-
| ScaleOrdinal<string, string>;
|
|
74
|
-
|
|
75
|
-
/** Union of all D3 scale types used by the engine. */
|
|
76
|
-
export type D3Scale = D3ContinuousScale | D3CategoricalScale | D3DiscretizingScale;
|
|
77
|
-
|
|
78
|
-
/** A sequential color scale mapping numbers to color strings. */
|
|
79
|
-
export type D3SequentialColorScale = ScaleLinear<string, string>;
|
|
80
|
-
|
|
81
|
-
/** All resolved scale type identifiers. */
|
|
82
|
-
export type ResolvedScaleType = ScaleType | 'sequential';
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* A resolved scale wrapping a d3 scale with type metadata.
|
|
86
|
-
* We need to carry the scale type around so axes and marks know
|
|
87
|
-
* how to interpret the domain/range. Consumers use the `type` discriminant
|
|
88
|
-
* to determine which D3 methods are available on the scale.
|
|
89
|
-
*/
|
|
90
|
-
export interface ResolvedScale {
|
|
91
|
-
/** The d3 scale function. Maps domain value -> pixel position or color. */
|
|
92
|
-
scale: D3Scale;
|
|
93
|
-
/** The scale type for downstream use. */
|
|
94
|
-
type: ResolvedScaleType;
|
|
95
|
-
/** The encoding channel this scale was derived from. */
|
|
96
|
-
channel: EncodingChannel;
|
|
97
|
-
/** Per-field formatting context (extent, allIntegers) from the ORIGINAL data. */
|
|
98
|
-
formatContext?: FieldFormatContext;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** All resolved scales for a chart. */
|
|
102
|
-
export interface ResolvedScales {
|
|
103
|
-
x?: ResolvedScale;
|
|
104
|
-
y?: ResolvedScale;
|
|
105
|
-
color?: ResolvedScale;
|
|
106
|
-
size?: ResolvedScale;
|
|
107
|
-
/** Default color for single-series charts (first categorical palette color or markDef.fill gradient). */
|
|
108
|
-
defaultColor?: string | import('@opendata-ai/openchart-core').GradientDef;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// Helpers
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
/** Extract all non-null values for a field from data. */
|
|
116
|
-
function fieldValues(data: DataRow[], field: string): unknown[] {
|
|
117
|
-
return data.map((d) => d[field]).filter((v) => v != null);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Parse values to dates. */
|
|
121
|
-
function parseDates(values: unknown[]): Date[] {
|
|
122
|
-
return values
|
|
123
|
-
.map((v) => (v instanceof Date ? v : new Date(String(v))))
|
|
124
|
-
.filter((d) => !Number.isNaN(d.getTime()));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Parse values to numbers. */
|
|
128
|
-
function parseNumbers(values: unknown[]): number[] {
|
|
129
|
-
return values
|
|
130
|
-
.map((v) => (typeof v === 'number' ? v : Number(v)))
|
|
131
|
-
.filter((n) => Number.isFinite(n));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Get unique string values preserving order. */
|
|
135
|
-
function uniqueStrings(values: unknown[]): string[] {
|
|
136
|
-
const seen = new Set<string>();
|
|
137
|
-
const result: string[] = [];
|
|
138
|
-
for (const v of values) {
|
|
139
|
-
const s = String(v);
|
|
140
|
-
if (!seen.has(s)) {
|
|
141
|
-
seen.add(s);
|
|
142
|
-
result.push(s);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Apply sort order to categorical domain values.
|
|
150
|
-
* - 'ascending': sort alphabetically/numerically ascending
|
|
151
|
-
* - 'descending': sort descending
|
|
152
|
-
* - null | undefined: preserve data order (no sorting)
|
|
153
|
-
*
|
|
154
|
-
* VL sort forms ('-y', value arrays, { field, op, order }) are resolved into
|
|
155
|
-
* an explicit scale.domain by the pre-validation sugar expansion; any that
|
|
156
|
-
* reach this point fall back to data order.
|
|
157
|
-
*/
|
|
158
|
-
export function applyCategoricalSort(values: string[], sort: EncodingChannel['sort']): string[] {
|
|
159
|
-
if (sort !== 'ascending' && sort !== 'descending') return values;
|
|
160
|
-
|
|
161
|
-
const sorted = [...values].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
162
|
-
if (sort === 'descending') sorted.reverse();
|
|
163
|
-
return sorted;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// Helpers: apply common scale config
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
/** Apply clamp and reverse config to a continuous scale. */
|
|
171
|
-
function applyContinuousConfig(
|
|
172
|
-
scale: { clamp(v: boolean): unknown; range(): number[]; range(r: number[]): unknown },
|
|
173
|
-
channel: EncodingChannel,
|
|
174
|
-
): void {
|
|
175
|
-
if (channel.scale?.clamp) {
|
|
176
|
-
scale.clamp(true);
|
|
177
|
-
}
|
|
178
|
-
if (channel.scale?.reverse) {
|
|
179
|
-
const [r0, r1] = scale.range() as number[];
|
|
180
|
-
scale.range([r1, r0]);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
// Scale builders
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
|
|
188
|
-
function buildTimeScale(
|
|
189
|
-
channel: EncodingChannel,
|
|
190
|
-
data: DataRow[],
|
|
191
|
-
rangeStart: number,
|
|
192
|
-
rangeEnd: number,
|
|
193
|
-
): ResolvedScale {
|
|
194
|
-
const values = parseDates(fieldValues(data, channel.field));
|
|
195
|
-
const domain = channel.scale?.domain
|
|
196
|
-
? [new Date(channel.scale.domain[0] as string), new Date(channel.scale.domain[1] as string)]
|
|
197
|
-
: (extent(values) as [Date, Date]);
|
|
198
|
-
|
|
199
|
-
const scale = scaleTime().domain(domain).range([rangeStart, rangeEnd]);
|
|
200
|
-
|
|
201
|
-
// Temporal scales default to nice: false because date data typically starts
|
|
202
|
-
// at clean boundaries and nice() rounds the domain outward, creating visible
|
|
203
|
-
// gaps (e.g. data starting 2018-01-01 gets rounded to 2017-01-01).
|
|
204
|
-
// Users can opt in with scale: { nice: true }.
|
|
205
|
-
if (!channel.scale?.domain && channel.scale?.nice === true) {
|
|
206
|
-
scale.nice();
|
|
207
|
-
}
|
|
208
|
-
applyContinuousConfig(scale, channel);
|
|
209
|
-
|
|
210
|
-
return { scale, type: 'time', channel };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function buildUtcScale(
|
|
214
|
-
channel: EncodingChannel,
|
|
215
|
-
data: DataRow[],
|
|
216
|
-
rangeStart: number,
|
|
217
|
-
rangeEnd: number,
|
|
218
|
-
): ResolvedScale {
|
|
219
|
-
const values = parseDates(fieldValues(data, channel.field));
|
|
220
|
-
const domain = channel.scale?.domain
|
|
221
|
-
? [new Date(channel.scale.domain[0] as string), new Date(channel.scale.domain[1] as string)]
|
|
222
|
-
: (extent(values) as [Date, Date]);
|
|
223
|
-
|
|
224
|
-
const scale = scaleUtc().domain(domain).range([rangeStart, rangeEnd]);
|
|
225
|
-
|
|
226
|
-
// Temporal scales default to nice: false (see buildTimeScale comment).
|
|
227
|
-
if (!channel.scale?.domain && channel.scale?.nice === true) {
|
|
228
|
-
scale.nice();
|
|
229
|
-
}
|
|
230
|
-
applyContinuousConfig(scale, channel);
|
|
231
|
-
|
|
232
|
-
return { scale, type: 'utc', channel };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function buildLinearScale(
|
|
236
|
-
channel: EncodingChannel,
|
|
237
|
-
data: DataRow[],
|
|
238
|
-
rangeStart: number,
|
|
239
|
-
rangeEnd: number,
|
|
240
|
-
): ResolvedScale {
|
|
241
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
242
|
-
|
|
243
|
-
let domainMin: number;
|
|
244
|
-
let domainMax: number;
|
|
245
|
-
|
|
246
|
-
if (channel.scale?.domain) {
|
|
247
|
-
const [d0, d1] = channel.scale.domain as [number, number];
|
|
248
|
-
domainMin = d0;
|
|
249
|
-
domainMax = d1;
|
|
250
|
-
} else {
|
|
251
|
-
domainMin = min(values) ?? 0;
|
|
252
|
-
domainMax = max(values) ?? 1;
|
|
253
|
-
|
|
254
|
-
// Include zero by default for quantitative scales
|
|
255
|
-
if (channel.scale?.zero !== false) {
|
|
256
|
-
domainMin = Math.min(0, domainMin);
|
|
257
|
-
domainMax = Math.max(0, domainMax);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const scale = scaleLinear().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
262
|
-
|
|
263
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
264
|
-
scale.nice();
|
|
265
|
-
|
|
266
|
-
// nice() can round the domain min down to 0 even when zero: false.
|
|
267
|
-
// Re-nice with more ticks to tighten the domain around the data range.
|
|
268
|
-
if (channel.scale?.zero === false) {
|
|
269
|
-
const [nicedMin, nicedMax] = scale.domain();
|
|
270
|
-
if (nicedMin < domainMin || nicedMax > domainMax) {
|
|
271
|
-
scale.domain([domainMin, domainMax]);
|
|
272
|
-
scale.nice(20);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
applyContinuousConfig(scale, channel);
|
|
277
|
-
|
|
278
|
-
return { scale, type: 'linear', channel };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function buildLogScale(
|
|
282
|
-
channel: EncodingChannel,
|
|
283
|
-
data: DataRow[],
|
|
284
|
-
rangeStart: number,
|
|
285
|
-
rangeEnd: number,
|
|
286
|
-
): ResolvedScale {
|
|
287
|
-
const values = parseNumbers(fieldValues(data, channel.field)).filter((v) => v > 0);
|
|
288
|
-
const domainMin = channel.scale?.domain
|
|
289
|
-
? (channel.scale.domain as [number, number])[0]
|
|
290
|
-
: (min(values) ?? 1);
|
|
291
|
-
const domainMax = channel.scale?.domain
|
|
292
|
-
? (channel.scale.domain as [number, number])[1]
|
|
293
|
-
: (max(values) ?? 10);
|
|
294
|
-
|
|
295
|
-
const scale = scaleLog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
296
|
-
|
|
297
|
-
if (channel.scale?.base !== undefined) {
|
|
298
|
-
scale.base(channel.scale.base);
|
|
299
|
-
}
|
|
300
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
301
|
-
scale.nice();
|
|
302
|
-
}
|
|
303
|
-
applyContinuousConfig(scale, channel);
|
|
304
|
-
|
|
305
|
-
return { scale, type: 'log', channel };
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function buildPowScale(
|
|
309
|
-
channel: EncodingChannel,
|
|
310
|
-
data: DataRow[],
|
|
311
|
-
rangeStart: number,
|
|
312
|
-
rangeEnd: number,
|
|
313
|
-
): ResolvedScale {
|
|
314
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
315
|
-
|
|
316
|
-
let domainMin: number;
|
|
317
|
-
let domainMax: number;
|
|
318
|
-
|
|
319
|
-
if (channel.scale?.domain) {
|
|
320
|
-
[domainMin, domainMax] = channel.scale.domain as [number, number];
|
|
321
|
-
} else {
|
|
322
|
-
domainMin = min(values) ?? 0;
|
|
323
|
-
domainMax = max(values) ?? 1;
|
|
324
|
-
if (channel.scale?.zero !== false) {
|
|
325
|
-
domainMin = Math.min(0, domainMin);
|
|
326
|
-
domainMax = Math.max(0, domainMax);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const scale = scalePow().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
331
|
-
|
|
332
|
-
if (channel.scale?.exponent !== undefined) {
|
|
333
|
-
scale.exponent(channel.scale.exponent);
|
|
334
|
-
}
|
|
335
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
336
|
-
scale.nice();
|
|
337
|
-
}
|
|
338
|
-
applyContinuousConfig(scale, channel);
|
|
339
|
-
|
|
340
|
-
return { scale, type: 'pow', channel };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function buildSqrtScale(
|
|
344
|
-
channel: EncodingChannel,
|
|
345
|
-
data: DataRow[],
|
|
346
|
-
rangeStart: number,
|
|
347
|
-
rangeEnd: number,
|
|
348
|
-
): ResolvedScale {
|
|
349
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
350
|
-
|
|
351
|
-
let domainMin: number;
|
|
352
|
-
let domainMax: number;
|
|
353
|
-
|
|
354
|
-
if (channel.scale?.domain) {
|
|
355
|
-
[domainMin, domainMax] = channel.scale.domain as [number, number];
|
|
356
|
-
} else {
|
|
357
|
-
domainMin = min(values) ?? 0;
|
|
358
|
-
domainMax = max(values) ?? 1;
|
|
359
|
-
if (channel.scale?.zero !== false) {
|
|
360
|
-
domainMin = Math.min(0, domainMin);
|
|
361
|
-
domainMax = Math.max(0, domainMax);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const scale = scaleSqrt().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
366
|
-
|
|
367
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
368
|
-
scale.nice();
|
|
369
|
-
}
|
|
370
|
-
applyContinuousConfig(scale, channel);
|
|
371
|
-
|
|
372
|
-
return { scale, type: 'sqrt', channel };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function buildSymlogScale(
|
|
376
|
-
channel: EncodingChannel,
|
|
377
|
-
data: DataRow[],
|
|
378
|
-
rangeStart: number,
|
|
379
|
-
rangeEnd: number,
|
|
380
|
-
): ResolvedScale {
|
|
381
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
382
|
-
|
|
383
|
-
let domainMin: number;
|
|
384
|
-
let domainMax: number;
|
|
385
|
-
|
|
386
|
-
if (channel.scale?.domain) {
|
|
387
|
-
[domainMin, domainMax] = channel.scale.domain as [number, number];
|
|
388
|
-
} else {
|
|
389
|
-
domainMin = min(values) ?? 0;
|
|
390
|
-
domainMax = max(values) ?? 1;
|
|
391
|
-
if (channel.scale?.zero !== false) {
|
|
392
|
-
domainMin = Math.min(0, domainMin);
|
|
393
|
-
domainMax = Math.max(0, domainMax);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const scale = scaleSymlog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
398
|
-
|
|
399
|
-
if (channel.scale?.constant !== undefined) {
|
|
400
|
-
scale.constant(channel.scale.constant);
|
|
401
|
-
}
|
|
402
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
403
|
-
scale.nice();
|
|
404
|
-
}
|
|
405
|
-
applyContinuousConfig(scale, channel);
|
|
406
|
-
|
|
407
|
-
return { scale, type: 'symlog', channel };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function buildQuantileScale(
|
|
411
|
-
channel: EncodingChannel,
|
|
412
|
-
data: DataRow[],
|
|
413
|
-
rangeStart: number,
|
|
414
|
-
rangeEnd: number,
|
|
415
|
-
): ResolvedScale {
|
|
416
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
417
|
-
const range = channel.scale?.range
|
|
418
|
-
? (channel.scale.range as number[])
|
|
419
|
-
: evenRange(rangeStart, rangeEnd, 4);
|
|
420
|
-
|
|
421
|
-
const scale = scaleQuantile<number>().domain(values).range(range);
|
|
422
|
-
|
|
423
|
-
return { scale: scale as unknown as D3Scale, type: 'quantile', channel };
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function buildQuantizeScale(
|
|
427
|
-
channel: EncodingChannel,
|
|
428
|
-
data: DataRow[],
|
|
429
|
-
rangeStart: number,
|
|
430
|
-
rangeEnd: number,
|
|
431
|
-
): ResolvedScale {
|
|
432
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
433
|
-
const domainMin = channel.scale?.domain
|
|
434
|
-
? (channel.scale.domain as [number, number])[0]
|
|
435
|
-
: (min(values) ?? 0);
|
|
436
|
-
const domainMax = channel.scale?.domain
|
|
437
|
-
? (channel.scale.domain as [number, number])[1]
|
|
438
|
-
: (max(values) ?? 1);
|
|
439
|
-
const range = channel.scale?.range
|
|
440
|
-
? (channel.scale.range as number[])
|
|
441
|
-
: evenRange(rangeStart, rangeEnd, 4);
|
|
442
|
-
|
|
443
|
-
const scale = scaleQuantize<number>().domain([domainMin, domainMax]).range(range);
|
|
444
|
-
|
|
445
|
-
return { scale: scale as unknown as D3Scale, type: 'quantize', channel };
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function buildThresholdScale(
|
|
449
|
-
channel: EncodingChannel,
|
|
450
|
-
_data: DataRow[],
|
|
451
|
-
rangeStart: number,
|
|
452
|
-
rangeEnd: number,
|
|
453
|
-
): ResolvedScale {
|
|
454
|
-
// Threshold scales require explicit domain breakpoints
|
|
455
|
-
const domainBreaks = channel.scale?.domain ? (channel.scale.domain as number[]) : [0.5];
|
|
456
|
-
const range = channel.scale?.range
|
|
457
|
-
? (channel.scale.range as number[])
|
|
458
|
-
: evenRange(rangeStart, rangeEnd, domainBreaks.length + 1);
|
|
459
|
-
|
|
460
|
-
const scale = scaleThreshold<number, number>().domain(domainBreaks).range(range);
|
|
461
|
-
|
|
462
|
-
return { scale: scale as unknown as D3Scale, type: 'threshold', channel };
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** Generate an evenly-spaced range of `count` values between start and end. */
|
|
466
|
-
function evenRange(start: number, end: number, count: number): number[] {
|
|
467
|
-
if (count <= 1) return [start];
|
|
468
|
-
const step = (end - start) / (count - 1);
|
|
469
|
-
return Array.from({ length: count }, (_, i) => start + step * i);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Estimate d3 scaleBand bandwidth for a given plot width and category count,
|
|
474
|
-
* mirroring buildBandScale's padding resolution. d3 sets both paddingInner and
|
|
475
|
-
* paddingOuter to `padding`, then paddingInner/paddingOuter overrides adjust
|
|
476
|
-
* each independently. step = width / (n - paddingInner + 2*paddingOuter);
|
|
477
|
-
* bandwidth = step * (1 - paddingInner).
|
|
478
|
-
*/
|
|
479
|
-
/** d3 scaleBand padding resolution: `padding` sets both, overrides win. */
|
|
480
|
-
function resolveBandPadding(scaleConfig: EncodingChannel['scale'] | undefined): {
|
|
481
|
-
paddingInner: number;
|
|
482
|
-
paddingOuter: number;
|
|
483
|
-
} {
|
|
484
|
-
const padding = scaleConfig?.padding ?? 0.35;
|
|
485
|
-
return {
|
|
486
|
-
paddingInner: scaleConfig?.paddingInner ?? padding,
|
|
487
|
-
paddingOuter: scaleConfig?.paddingOuter ?? padding,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export function estimateBandwidth(
|
|
492
|
-
scaleConfig: EncodingChannel['scale'] | undefined,
|
|
493
|
-
plotWidth: number,
|
|
494
|
-
n: number,
|
|
495
|
-
): number {
|
|
496
|
-
const { paddingInner } = resolveBandPadding(scaleConfig);
|
|
497
|
-
return estimateBandStep(scaleConfig, plotWidth, n) * (1 - paddingInner);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Estimate d3 scaleBand step (center-to-center band distance) for a given
|
|
502
|
-
* plot width and category count, using the same padding resolution as
|
|
503
|
-
* estimateBandwidth. The step — not the bandwidth — is the anchor spacing
|
|
504
|
-
* that drives rotated-label collision checks.
|
|
505
|
-
*/
|
|
506
|
-
export function estimateBandStep(
|
|
507
|
-
scaleConfig: EncodingChannel['scale'] | undefined,
|
|
508
|
-
plotWidth: number,
|
|
509
|
-
n: number,
|
|
510
|
-
): number {
|
|
511
|
-
if (n <= 0) return 0;
|
|
512
|
-
const { paddingInner, paddingOuter } = resolveBandPadding(scaleConfig);
|
|
513
|
-
const denom = n - paddingInner + 2 * paddingOuter;
|
|
514
|
-
if (denom <= 0) return 0;
|
|
515
|
-
return plotWidth / denom;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Gap between bands, as a fraction of the step. The 0.35 default is a *bar*
|
|
520
|
-
* value: bars need air between them to read as separate quantities. Heatmap
|
|
521
|
-
* cells are the opposite -- they tile, and the gap is a hairline gutter that
|
|
522
|
-
* only exists to keep adjacent fills from bleeding into each other.
|
|
523
|
-
*/
|
|
524
|
-
const DEFAULT_BAND_PADDING = 0.35;
|
|
525
|
-
const HEATMAP_BAND_PADDING = 0.04;
|
|
526
|
-
|
|
527
|
-
function buildBandScale(
|
|
528
|
-
channel: EncodingChannel,
|
|
529
|
-
data: DataRow[],
|
|
530
|
-
rangeStart: number,
|
|
531
|
-
rangeEnd: number,
|
|
532
|
-
defaultPadding: number = DEFAULT_BAND_PADDING,
|
|
533
|
-
): ResolvedScale {
|
|
534
|
-
const values = channel.scale?.domain
|
|
535
|
-
? (channel.scale.domain as string[])
|
|
536
|
-
: applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
537
|
-
|
|
538
|
-
const padding = channel.scale?.padding ?? defaultPadding;
|
|
539
|
-
const scale = scaleBand().domain(values).range([rangeStart, rangeEnd]).padding(padding);
|
|
540
|
-
|
|
541
|
-
if (channel.scale?.paddingInner !== undefined) {
|
|
542
|
-
scale.paddingInner(channel.scale.paddingInner);
|
|
543
|
-
}
|
|
544
|
-
if (channel.scale?.paddingOuter !== undefined) {
|
|
545
|
-
scale.paddingOuter(channel.scale.paddingOuter);
|
|
546
|
-
}
|
|
547
|
-
if (channel.scale?.reverse) {
|
|
548
|
-
const [r0, r1] = scale.range();
|
|
549
|
-
scale.range([r1, r0]);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
return { scale, type: 'band', channel };
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function buildPointScale(
|
|
556
|
-
channel: EncodingChannel,
|
|
557
|
-
data: DataRow[],
|
|
558
|
-
rangeStart: number,
|
|
559
|
-
rangeEnd: number,
|
|
560
|
-
): ResolvedScale {
|
|
561
|
-
const values = channel.scale?.domain
|
|
562
|
-
? (channel.scale.domain as string[])
|
|
563
|
-
: applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
564
|
-
|
|
565
|
-
// Point scales have a single padding knob (outer padding only -- there are no
|
|
566
|
-
// bands, so paddingInner is meaningless). Accept `paddingOuter` as an alias so
|
|
567
|
-
// a spec written for a band scale doesn't silently no-op when the mark is a
|
|
568
|
-
// line; explicit `padding` wins if both are set.
|
|
569
|
-
const padding = channel.scale?.padding ?? channel.scale?.paddingOuter ?? 0.5;
|
|
570
|
-
const scale = scalePoint().domain(values).range([rangeStart, rangeEnd]).padding(padding);
|
|
571
|
-
|
|
572
|
-
if (channel.scale?.reverse) {
|
|
573
|
-
const [r0, r1] = scale.range();
|
|
574
|
-
scale.range([r1, r0]);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return { scale, type: 'point', channel };
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function buildOrdinalColorScale(
|
|
581
|
-
channel: EncodingChannel,
|
|
582
|
-
data: DataRow[],
|
|
583
|
-
palette: string[],
|
|
584
|
-
): ResolvedScale {
|
|
585
|
-
// Use explicit domain if provided, otherwise derive from data
|
|
586
|
-
const explicitDomain = channel.scale?.domain as string[] | undefined;
|
|
587
|
-
const values = explicitDomain
|
|
588
|
-
? explicitDomain.map(String)
|
|
589
|
-
: applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
590
|
-
|
|
591
|
-
// Use explicit range if provided, otherwise fall back to theme palette
|
|
592
|
-
const explicitRange = channel.scale?.range as string[] | undefined;
|
|
593
|
-
const colors = explicitRange ?? palette;
|
|
594
|
-
|
|
595
|
-
const scale = scaleOrdinal<string>().domain(values).range(colors);
|
|
596
|
-
|
|
597
|
-
return { scale, type: 'ordinal', channel };
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function buildSequentialColorScale(
|
|
601
|
-
channel: EncodingChannel,
|
|
602
|
-
data: DataRow[],
|
|
603
|
-
palette: string[],
|
|
604
|
-
): ResolvedScale {
|
|
605
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
606
|
-
const domainMin = min(values) ?? 0;
|
|
607
|
-
const domainMax = max(values) ?? 1;
|
|
608
|
-
|
|
609
|
-
// Use explicit range if provided, otherwise fall back to theme palette endpoints
|
|
610
|
-
const explicitRange = channel.scale?.range as string[] | undefined;
|
|
611
|
-
const colors = explicitRange ?? palette;
|
|
612
|
-
|
|
613
|
-
// Explicit multi-stop ranges (e.g. a diverging scheme) interpolate piecewise
|
|
614
|
-
// through every stop, with evenly-spaced domain pivots to match. Without an
|
|
615
|
-
// explicit range the endpoints-only form is kept so applyColorScaleRange can
|
|
616
|
-
// swap in the theme ramp endpoints post-hoc.
|
|
617
|
-
const stops =
|
|
618
|
-
explicitRange && explicitRange.length > 2
|
|
619
|
-
? explicitRange
|
|
620
|
-
: [colors[0], colors[colors.length - 1]];
|
|
621
|
-
const domain =
|
|
622
|
-
stops.length > 2
|
|
623
|
-
? stops.map((_, i) => domainMin + ((domainMax - domainMin) * i) / (stops.length - 1))
|
|
624
|
-
: [domainMin, domainMax];
|
|
625
|
-
|
|
626
|
-
const scale = scaleLinear<string>().domain(domain).range(stops).clamp(true);
|
|
627
|
-
|
|
628
|
-
// Cast: sequential color scale (number -> string) is structurally incompatible
|
|
629
|
-
// with D3Scale (number -> number), but is only ever accessed via scales.color
|
|
630
|
-
// where consumers already cast appropriately.
|
|
631
|
-
return { scale: scale as unknown as D3Scale, type: 'sequential', channel };
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Build a binned color scale (quantile/quantize/threshold) for a quantitative
|
|
636
|
-
* color encoding with an explicit `scale.type`. The range holds colors: an
|
|
637
|
-
* explicit `scale.range` wins; otherwise a placeholder sampled from the
|
|
638
|
-
* default palette that `applyColorScaleRange` replaces with the theme's
|
|
639
|
-
* sequential ramp.
|
|
640
|
-
*/
|
|
641
|
-
function buildBinnedColorScale(
|
|
642
|
-
channel: EncodingChannel,
|
|
643
|
-
data: DataRow[],
|
|
644
|
-
palette: string[],
|
|
645
|
-
): ResolvedScale {
|
|
646
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
647
|
-
const explicitRange = channel.scale?.range as string[] | undefined;
|
|
648
|
-
const scaleType = channel.scale?.type as 'quantile' | 'quantize' | 'threshold';
|
|
649
|
-
|
|
650
|
-
if (scaleType === 'threshold') {
|
|
651
|
-
// Threshold scales require explicit domain breakpoints; colors = breaks + 1.
|
|
652
|
-
const breaks = (channel.scale?.domain as number[] | undefined) ?? [0.5];
|
|
653
|
-
const colors = sampleRampColors(explicitRange ?? palette, breaks.length + 1);
|
|
654
|
-
const scale = scaleThreshold<number, string>().domain(breaks).range(colors);
|
|
655
|
-
return { scale: scale as unknown as D3Scale, type: 'threshold', channel };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const binCount = explicitRange?.length ?? DEFAULT_BIN_COUNT;
|
|
659
|
-
const colors = explicitRange ?? sampleRampColors(palette, binCount);
|
|
660
|
-
|
|
661
|
-
if (scaleType === 'quantile') {
|
|
662
|
-
const scale = scaleQuantile<string>().domain(values).range(colors);
|
|
663
|
-
return { scale: scale as unknown as D3Scale, type: 'quantile', channel };
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const explicitDomain = channel.scale?.domain as [number, number] | undefined;
|
|
667
|
-
const domainMin = explicitDomain?.[0] ?? min(values) ?? 0;
|
|
668
|
-
const domainMax = explicitDomain?.[1] ?? max(values) ?? 1;
|
|
669
|
-
const scale = scaleQuantize<string>().domain([domainMin, domainMax]).range(colors);
|
|
670
|
-
return { scale: scale as unknown as D3Scale, type: 'quantize', channel };
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// ---------------------------------------------------------------------------
|
|
674
|
-
// Positional scale selection
|
|
675
|
-
// ---------------------------------------------------------------------------
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Choose the right scale type for a positional channel (x or y).
|
|
679
|
-
* Respects explicit scale.type overrides from the spec.
|
|
680
|
-
*/
|
|
681
|
-
function buildPositionalScale(
|
|
682
|
-
channel: EncodingChannel,
|
|
683
|
-
data: DataRow[],
|
|
684
|
-
rangeStart: number,
|
|
685
|
-
rangeEnd: number,
|
|
686
|
-
chartType: string,
|
|
687
|
-
axis: 'x' | 'y',
|
|
688
|
-
): ResolvedScale {
|
|
689
|
-
// Explicit scale type override
|
|
690
|
-
if (channel.scale?.type) {
|
|
691
|
-
switch (channel.scale.type) {
|
|
692
|
-
case 'time':
|
|
693
|
-
return buildTimeScale(channel, data, rangeStart, rangeEnd);
|
|
694
|
-
case 'utc':
|
|
695
|
-
return buildUtcScale(channel, data, rangeStart, rangeEnd);
|
|
696
|
-
case 'linear':
|
|
697
|
-
return buildLinearScale(channel, data, rangeStart, rangeEnd);
|
|
698
|
-
case 'log':
|
|
699
|
-
return buildLogScale(channel, data, rangeStart, rangeEnd);
|
|
700
|
-
case 'pow':
|
|
701
|
-
return buildPowScale(channel, data, rangeStart, rangeEnd);
|
|
702
|
-
case 'sqrt':
|
|
703
|
-
return buildSqrtScale(channel, data, rangeStart, rangeEnd);
|
|
704
|
-
case 'symlog':
|
|
705
|
-
return buildSymlogScale(channel, data, rangeStart, rangeEnd);
|
|
706
|
-
case 'quantile':
|
|
707
|
-
return buildQuantileScale(channel, data, rangeStart, rangeEnd);
|
|
708
|
-
case 'quantize':
|
|
709
|
-
return buildQuantizeScale(channel, data, rangeStart, rangeEnd);
|
|
710
|
-
case 'threshold':
|
|
711
|
-
return buildThresholdScale(channel, data, rangeStart, rangeEnd);
|
|
712
|
-
case 'band':
|
|
713
|
-
return buildBandScale(channel, data, rangeStart, rangeEnd);
|
|
714
|
-
case 'point':
|
|
715
|
-
return buildPointScale(channel, data, rangeStart, rangeEnd);
|
|
716
|
-
case 'ordinal':
|
|
717
|
-
return buildBandScale(channel, data, rangeStart, rangeEnd);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Infer from field type
|
|
722
|
-
switch (channel.type) {
|
|
723
|
-
case 'temporal':
|
|
724
|
-
return buildTimeScale(channel, data, rangeStart, rangeEnd);
|
|
725
|
-
case 'quantitative':
|
|
726
|
-
return buildLinearScale(channel, data, rangeStart, rangeEnd);
|
|
727
|
-
case 'nominal':
|
|
728
|
-
case 'ordinal':
|
|
729
|
-
// Bar and range charts use band scales for their categorical axis (both
|
|
730
|
-
// orientations). Beeswarm lanes are band scales too: each category gets
|
|
731
|
-
// a band whose center anchors one swarm, on whichever axis carries the
|
|
732
|
-
// nominal channel.
|
|
733
|
-
//
|
|
734
|
-
// `rect` is the only mark that needs a band on *both* axes: a heatmap cell
|
|
735
|
-
// is sized by the bandwidth of each axis, so it takes no `axis` guard. A
|
|
736
|
-
// point scale would give it zero width and height (and it did: `rect` cells
|
|
737
|
-
// rendered as invisible zero-area marks). It also tiles, so it takes the
|
|
738
|
-
// hairline gutter rather than the bar-sized gap.
|
|
739
|
-
if (chartType === 'rect') {
|
|
740
|
-
return buildBandScale(channel, data, rangeStart, rangeEnd, HEATMAP_BAND_PADDING);
|
|
741
|
-
}
|
|
742
|
-
if (
|
|
743
|
-
chartType === 'bar' ||
|
|
744
|
-
chartType === 'beeswarm' ||
|
|
745
|
-
chartType === 'range' ||
|
|
746
|
-
((chartType === 'circle' || chartType === 'lollipop') && axis === 'y')
|
|
747
|
-
) {
|
|
748
|
-
return buildBandScale(channel, data, rangeStart, rangeEnd);
|
|
749
|
-
}
|
|
750
|
-
return buildPointScale(channel, data, rangeStart, rangeEnd);
|
|
751
|
-
default:
|
|
752
|
-
return buildLinearScale(channel, data, rangeStart, rangeEnd);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// ---------------------------------------------------------------------------
|
|
757
|
-
// Public API
|
|
758
|
-
// ---------------------------------------------------------------------------
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Compute D3 scales from encoding channels and data.
|
|
762
|
-
*
|
|
763
|
-
* @param spec - Normalized chart spec.
|
|
764
|
-
* @param chartArea - The computed chart drawing area.
|
|
765
|
-
* @param data - Data rows.
|
|
766
|
-
* @returns ResolvedScales with d3 scale instances.
|
|
767
|
-
*/
|
|
768
|
-
export function computeScales(
|
|
769
|
-
spec: NormalizedChartSpec,
|
|
770
|
-
chartArea: Rect,
|
|
771
|
-
data: DataRow[],
|
|
772
|
-
): ResolvedScales {
|
|
773
|
-
const result: ResolvedScales = {};
|
|
774
|
-
const encoding = spec.encoding as Encoding;
|
|
775
|
-
|
|
776
|
-
// Marks that encode position (not length) default to zero: false so the
|
|
777
|
-
// domain fits the data range. Line charts show trends — anchoring at zero
|
|
778
|
-
// wastes space. Scatter/beeswarm encode raw observations whose spread is
|
|
779
|
-
// the story. Text marks must match the point layer they label.
|
|
780
|
-
if (
|
|
781
|
-
spec.markType === 'point' ||
|
|
782
|
-
spec.markType === 'line' ||
|
|
783
|
-
spec.markType === 'beeswarm' ||
|
|
784
|
-
spec.markType === 'range' ||
|
|
785
|
-
spec.markType === 'text'
|
|
786
|
-
) {
|
|
787
|
-
if (encoding.x?.type === 'quantitative' && encoding.x.scale?.zero === undefined) {
|
|
788
|
-
if (!encoding.x.scale) {
|
|
789
|
-
(encoding.x as { scale?: Record<string, unknown> }).scale = { zero: false };
|
|
790
|
-
} else {
|
|
791
|
-
(encoding.x.scale as Record<string, unknown>).zero = false;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
if (encoding.y?.type === 'quantitative' && encoding.y.scale?.zero === undefined) {
|
|
795
|
-
if (!encoding.y.scale) {
|
|
796
|
-
(encoding.y as { scale?: Record<string, unknown> }).scale = { zero: false };
|
|
797
|
-
} else {
|
|
798
|
-
(encoding.y.scale as Record<string, unknown>).zero = false;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (encoding.x) {
|
|
804
|
-
// For stacked bars, the x-domain needs the max category sum, not max individual value.
|
|
805
|
-
// Without this, stacked bars would clip past the chart area.
|
|
806
|
-
let xData = data;
|
|
807
|
-
let xChannel = encoding.x;
|
|
808
|
-
// Range charts span x to x2: the x-domain must cover both endpoint fields.
|
|
809
|
-
// Synthetic rows map x2 values into the x field so buildLinearScale sees them.
|
|
810
|
-
if (spec.markType === 'range' && encoding.x2 && encoding.x.type === 'quantitative') {
|
|
811
|
-
const xField = encoding.x.field;
|
|
812
|
-
const x2Field = encoding.x2.field;
|
|
813
|
-
xData = [...data, ...data.map((row) => ({ [xField]: row[x2Field] }) as DataRow)];
|
|
814
|
-
}
|
|
815
|
-
const xStackEnabled =
|
|
816
|
-
encoding.x.stack === true ||
|
|
817
|
-
encoding.x.stack === 'zero' ||
|
|
818
|
-
encoding.x.stack === 'normalize' ||
|
|
819
|
-
encoding.x.stack === 'center';
|
|
820
|
-
if (
|
|
821
|
-
spec.markType === 'bar' &&
|
|
822
|
-
encoding.color &&
|
|
823
|
-
encoding.x.type === 'quantitative' &&
|
|
824
|
-
xStackEnabled
|
|
825
|
-
) {
|
|
826
|
-
if (encoding.x.stack === 'normalize') {
|
|
827
|
-
// Normalize: domain is [0, 1], default to percentage axis
|
|
828
|
-
const existingAxis = encoding.x.axis;
|
|
829
|
-
const axis =
|
|
830
|
-
existingAxis === false || existingAxis?.format
|
|
831
|
-
? existingAxis
|
|
832
|
-
: { ...(typeof existingAxis === 'object' ? existingAxis : {}), format: '.0%' };
|
|
833
|
-
xChannel = {
|
|
834
|
-
...encoding.x,
|
|
835
|
-
scale: { ...encoding.x.scale, domain: [0, 1], nice: false },
|
|
836
|
-
axis,
|
|
837
|
-
};
|
|
838
|
-
} else if (encoding.x.stack === 'center') {
|
|
839
|
-
// Center: compute max half-sum for symmetric domain
|
|
840
|
-
const yField = encoding.y?.field;
|
|
841
|
-
const xField = encoding.x.field;
|
|
842
|
-
if (yField) {
|
|
843
|
-
const sums = new Map<string, number>();
|
|
844
|
-
for (const row of data) {
|
|
845
|
-
const cat = String(row[yField] ?? '');
|
|
846
|
-
const val = Number(row[xField] ?? 0);
|
|
847
|
-
if (Number.isFinite(val) && val > 0) {
|
|
848
|
-
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
const maxSum = Math.max(...sums.values(), 0);
|
|
852
|
-
const half = maxSum / 2;
|
|
853
|
-
xChannel = {
|
|
854
|
-
...encoding.x,
|
|
855
|
-
scale: { ...encoding.x.scale, domain: [-half, half], zero: true },
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
} else {
|
|
859
|
-
// Zero (default): domain extends to max category sum
|
|
860
|
-
const yField = encoding.y?.field;
|
|
861
|
-
const xField = encoding.x.field;
|
|
862
|
-
if (yField) {
|
|
863
|
-
const sums = new Map<string, number>();
|
|
864
|
-
for (const row of data) {
|
|
865
|
-
const cat = String(row[yField] ?? '');
|
|
866
|
-
const val = Number(row[xField] ?? 0);
|
|
867
|
-
if (Number.isFinite(val) && val > 0) {
|
|
868
|
-
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
const maxSum = Math.max(...sums.values(), 0);
|
|
872
|
-
// Create a synthetic row with the max stack sum so buildLinearScale sees it
|
|
873
|
-
xData = [...data, { [xField]: maxSum } as DataRow];
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
result.x = buildPositionalScale(
|
|
879
|
-
xChannel,
|
|
880
|
-
xData,
|
|
881
|
-
chartArea.x,
|
|
882
|
-
chartArea.x + chartArea.width,
|
|
883
|
-
spec.markType,
|
|
884
|
-
'x',
|
|
885
|
-
);
|
|
886
|
-
if (result.x && encoding.x.type === 'quantitative' && encoding.x.field) {
|
|
887
|
-
result.x.formatContext = computeFieldFormatContext(data.map((r) => r[encoding.x!.field]));
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
if (encoding.y) {
|
|
892
|
-
// For stacked vertical bars and stacked areas, the y-domain needs the max
|
|
893
|
-
// category sum, not the max individual value. Without this, stacked marks
|
|
894
|
-
// would clip above the chart area.
|
|
895
|
-
// Vertical bar = x is categorical and y is quantitative (old 'column' chart type).
|
|
896
|
-
let yData = data;
|
|
897
|
-
let yChannel = encoding.y;
|
|
898
|
-
// Range charts span y to y2 (vertical form): cover both endpoint fields.
|
|
899
|
-
if (spec.markType === 'range' && encoding.y2 && encoding.y.type === 'quantitative') {
|
|
900
|
-
const yField = encoding.y.field;
|
|
901
|
-
const y2Field = encoding.y2.field;
|
|
902
|
-
yData = [...data, ...data.map((row) => ({ [yField]: row[y2Field] }) as DataRow)];
|
|
903
|
-
}
|
|
904
|
-
const isVerticalBar =
|
|
905
|
-
spec.markType === 'bar' &&
|
|
906
|
-
(encoding.x?.type === 'nominal' || encoding.x?.type === 'ordinal') &&
|
|
907
|
-
encoding.y.type === 'quantitative';
|
|
908
|
-
// Both bar and area require explicit opt-in for stacked domain expansion.
|
|
909
|
-
const stackProp = encoding.y.stack;
|
|
910
|
-
const isExplicitlyStacked =
|
|
911
|
-
stackProp === true ||
|
|
912
|
-
stackProp === 'zero' ||
|
|
913
|
-
stackProp === 'normalize' ||
|
|
914
|
-
stackProp === 'center';
|
|
915
|
-
const isAreaStacked = spec.markType === 'area' && isExplicitlyStacked;
|
|
916
|
-
const isBarStacked = isVerticalBar && isExplicitlyStacked;
|
|
917
|
-
|
|
918
|
-
// Sparkline tightening: drop the default `zero: true` baseline so the
|
|
919
|
-
// y-domain hugs the actual data range. Without this, a series with
|
|
920
|
-
// values in the 4000s renders as a near-flat line because most of the
|
|
921
|
-
// chart area gets reserved for the gap between zero and the data.
|
|
922
|
-
//
|
|
923
|
-
// Applies to:
|
|
924
|
-
// - Line / area sparklines (always — variation is the whole point)
|
|
925
|
-
// - Vertical bar sparklines, but ONLY when no real stacking is in
|
|
926
|
-
// play. Two ways to opt OUT of bar tightening:
|
|
927
|
-
// 1. Real stacking — color/group encoding plus a non-disabled
|
|
928
|
-
// stack — needs the zero baseline to keep segment arithmetic
|
|
929
|
-
// summing.
|
|
930
|
-
// 2. Any explicit `encoding.y.stack` value signals the user
|
|
931
|
-
// wants stack semantics even on a single series; respect that.
|
|
932
|
-
const hasStackingGroup = isBarStacked && encoding.color !== undefined;
|
|
933
|
-
const userRequestedStack = isExplicitlyStacked;
|
|
934
|
-
const isLineOrArea = spec.markType === 'line' || spec.markType === 'area';
|
|
935
|
-
const sparklineTightenBar =
|
|
936
|
-
isVerticalBar && !hasStackingGroup && !userRequestedStack && !isAreaStacked;
|
|
937
|
-
const sparklineTightenLineArea = isLineOrArea && !isAreaStacked;
|
|
938
|
-
if (
|
|
939
|
-
spec.display === 'sparkline' &&
|
|
940
|
-
(sparklineTightenBar || sparklineTightenLineArea) &&
|
|
941
|
-
encoding.y.type === 'quantitative' &&
|
|
942
|
-
encoding.y.scale?.zero === undefined
|
|
943
|
-
) {
|
|
944
|
-
yChannel = {
|
|
945
|
-
...encoding.y,
|
|
946
|
-
scale: { ...encoding.y.scale, zero: false },
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
if ((isBarStacked || isAreaStacked) && encoding.color && encoding.y.type === 'quantitative') {
|
|
950
|
-
if (encoding.y.stack === 'normalize') {
|
|
951
|
-
// Normalize: domain is [0, 1] (VL convention), default to percentage axis
|
|
952
|
-
const existingAxis = encoding.y.axis;
|
|
953
|
-
const axis =
|
|
954
|
-
existingAxis === false || existingAxis?.format
|
|
955
|
-
? existingAxis
|
|
956
|
-
: { ...(typeof existingAxis === 'object' ? existingAxis : {}), format: '.0%' };
|
|
957
|
-
yChannel = {
|
|
958
|
-
...encoding.y,
|
|
959
|
-
scale: { ...encoding.y.scale, domain: [0, 1], nice: false },
|
|
960
|
-
axis,
|
|
961
|
-
};
|
|
962
|
-
} else if (encoding.y.stack === 'center') {
|
|
963
|
-
// Center: compute max half-sum for symmetric domain
|
|
964
|
-
const xField = encoding.x?.field;
|
|
965
|
-
const yField = encoding.y.field;
|
|
966
|
-
if (xField) {
|
|
967
|
-
const sums = new Map<string, number>();
|
|
968
|
-
for (const row of data) {
|
|
969
|
-
const cat = String(row[xField] ?? '');
|
|
970
|
-
const val = Number(row[yField] ?? 0);
|
|
971
|
-
if (Number.isFinite(val) && val > 0) {
|
|
972
|
-
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
const maxSum = Math.max(...sums.values(), 0);
|
|
976
|
-
const half = maxSum / 2;
|
|
977
|
-
yChannel = {
|
|
978
|
-
...encoding.y,
|
|
979
|
-
scale: { ...encoding.y.scale, domain: [-half, half], zero: true },
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
} else {
|
|
983
|
-
// Zero (default): domain extends to max category sum
|
|
984
|
-
const xField = encoding.x?.field;
|
|
985
|
-
const yField = encoding.y.field;
|
|
986
|
-
if (xField) {
|
|
987
|
-
const sums = new Map<string, number>();
|
|
988
|
-
for (const row of data) {
|
|
989
|
-
const cat = String(row[xField] ?? '');
|
|
990
|
-
const val = Number(row[yField] ?? 0);
|
|
991
|
-
if (Number.isFinite(val) && val > 0) {
|
|
992
|
-
sums.set(cat, (sums.get(cat) ?? 0) + val);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
const maxSum = Math.max(...sums.values(), 0);
|
|
996
|
-
// Create a synthetic row with the max stack sum so buildLinearScale sees it
|
|
997
|
-
yData = [...data, { [yField]: maxSum } as DataRow];
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// Y axis: range is inverted (SVG y goes down, data y goes up)
|
|
1003
|
-
result.y = buildPositionalScale(
|
|
1004
|
-
yChannel,
|
|
1005
|
-
yData,
|
|
1006
|
-
chartArea.y + chartArea.height,
|
|
1007
|
-
chartArea.y,
|
|
1008
|
-
spec.markType,
|
|
1009
|
-
'y',
|
|
1010
|
-
);
|
|
1011
|
-
if (result.y && encoding.y.type === 'quantitative' && encoding.y.field) {
|
|
1012
|
-
result.y.formatContext = computeFieldFormatContext(data.map((r) => r[encoding.y!.field]));
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
if (encoding.color) {
|
|
1017
|
-
const defaultPalette = [
|
|
1018
|
-
'#1b7fa3',
|
|
1019
|
-
'#c44e52',
|
|
1020
|
-
'#6a9f58',
|
|
1021
|
-
'#d47215',
|
|
1022
|
-
'#507e79',
|
|
1023
|
-
'#9a6a8d',
|
|
1024
|
-
'#c4636b',
|
|
1025
|
-
'#9c755f',
|
|
1026
|
-
'#a88f22',
|
|
1027
|
-
'#858078',
|
|
1028
|
-
];
|
|
1029
|
-
|
|
1030
|
-
// Only build color scales for field-based encodings, not conditional value defs
|
|
1031
|
-
if ('field' in encoding.color) {
|
|
1032
|
-
if (encoding.color.type === 'quantitative') {
|
|
1033
|
-
const colorScaleType = encoding.color.scale?.type;
|
|
1034
|
-
if (
|
|
1035
|
-
colorScaleType === 'quantile' ||
|
|
1036
|
-
colorScaleType === 'quantize' ||
|
|
1037
|
-
colorScaleType === 'threshold'
|
|
1038
|
-
) {
|
|
1039
|
-
// Binned color scale: discrete classes for value-based coloring
|
|
1040
|
-
result.color = buildBinnedColorScale(encoding.color, data, defaultPalette);
|
|
1041
|
-
} else {
|
|
1042
|
-
// Sequential color scale for value-based coloring
|
|
1043
|
-
result.color = buildSequentialColorScale(encoding.color, data, defaultPalette);
|
|
1044
|
-
}
|
|
1045
|
-
} else {
|
|
1046
|
-
// Categorical color scale for nominal/ordinal grouping
|
|
1047
|
-
result.color = buildOrdinalColorScale(encoding.color, data, defaultPalette);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
return result;
|
|
1053
|
-
}
|