@opendata-ai/openchart-engine 6.10.0 → 6.12.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 +9 -10
- package/dist/index.js +148 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +3 -0
- package/src/charts/bar/compute.ts +9 -5
- package/src/charts/bar/labels.ts +2 -1
- package/src/charts/column/compute.ts +9 -5
- package/src/charts/column/labels.ts +2 -1
- package/src/charts/dot/labels.ts +6 -2
- package/src/charts/line/area.ts +3 -2
- package/src/charts/line/compute.ts +5 -2
- package/src/charts/pie/compute.ts +24 -3
- package/src/charts/rule/index.ts +6 -3
- package/src/charts/scatter/compute.ts +2 -1
- package/src/charts/text/index.ts +6 -3
- package/src/charts/tick/index.ts +6 -3
- package/src/charts/utils.ts +3 -3
- package/src/compile.ts +27 -14
- package/src/compiler/__tests__/normalize.test.ts +110 -0
- package/src/compiler/normalize.ts +20 -1
- package/src/compiler/types.ts +4 -0
- package/src/graphs/compile-graph.ts +8 -0
- package/src/graphs/types.ts +2 -0
- package/src/layout/dimensions.ts +3 -0
- package/src/layout/scales.ts +2 -2
- package/src/legend/compute.ts +3 -1
- package/src/sankey/compile-sankey.ts +12 -2
- package/src/sankey/types.ts +1 -0
- package/src/tables/compile-table.ts +5 -0
- package/src/tooltips/compute.ts +11 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.12.0",
|
|
4
4
|
"description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@opendata-ai/openchart-core": "6.
|
|
48
|
+
"@opendata-ai/openchart-core": "6.12.0",
|
|
49
49
|
"d3-array": "^3.2.0",
|
|
50
50
|
"d3-format": "^3.1.2",
|
|
51
51
|
"d3-interpolate": "^3.0.0",
|
|
@@ -73,6 +73,7 @@ export function makeLineSpec(): NormalizedChartSpec {
|
|
|
73
73
|
labels: { density: 'auto', format: '', prefix: '' },
|
|
74
74
|
hiddenSeries: [],
|
|
75
75
|
seriesStyles: {},
|
|
76
|
+
watermark: true,
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -102,6 +103,7 @@ export function makeBarSpec(): NormalizedChartSpec {
|
|
|
102
103
|
labels: { density: 'auto', format: '', prefix: '' },
|
|
103
104
|
hiddenSeries: [],
|
|
104
105
|
seriesStyles: {},
|
|
106
|
+
watermark: true,
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -133,5 +135,6 @@ export function makeScatterSpec(): NormalizedChartSpec {
|
|
|
133
135
|
labels: { density: 'auto', format: '', prefix: '' },
|
|
134
136
|
hiddenSeries: [],
|
|
135
137
|
seriesStyles: {},
|
|
138
|
+
watermark: true,
|
|
136
139
|
};
|
|
137
140
|
}
|
|
@@ -10,12 +10,13 @@ import type {
|
|
|
10
10
|
ConditionalValueDef,
|
|
11
11
|
DataRow,
|
|
12
12
|
Encoding,
|
|
13
|
+
GradientDef,
|
|
13
14
|
LayoutStrategy,
|
|
14
15
|
MarkAria,
|
|
15
16
|
Rect,
|
|
16
17
|
RectMark,
|
|
17
18
|
} from '@opendata-ai/openchart-core';
|
|
18
|
-
import { abbreviateNumber, formatNumber } from '@opendata-ai/openchart-core';
|
|
19
|
+
import { abbreviateNumber, formatNumber, isGradientDef } from '@opendata-ai/openchart-core';
|
|
19
20
|
import type { ScaleBand, ScaleLinear } from 'd3-scale';
|
|
20
21
|
|
|
21
22
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
@@ -339,11 +340,14 @@ function computeSimpleBars(
|
|
|
339
340
|
const bandY = yScale(category);
|
|
340
341
|
if (bandY === undefined) continue;
|
|
341
342
|
|
|
342
|
-
let color: string;
|
|
343
|
+
let color: string | GradientDef;
|
|
343
344
|
if (conditionalColor) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
const resolved = resolveConditionalValue(row, conditionalColor);
|
|
346
|
+
if (resolved != null) {
|
|
347
|
+
color = isGradientDef(resolved) ? resolved : String(resolved);
|
|
348
|
+
} else {
|
|
349
|
+
color = getColor(scales, '__default__');
|
|
350
|
+
}
|
|
347
351
|
} else if (sequentialColor) {
|
|
348
352
|
color = getSequentialColor(scales, value);
|
|
349
353
|
} else {
|
package/src/charts/bar/labels.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
import {
|
|
21
21
|
buildD3Formatter,
|
|
22
22
|
estimateTextWidth,
|
|
23
|
+
getRepresentativeColor,
|
|
23
24
|
resolveCollisions,
|
|
24
25
|
} from '@opendata-ai/openchart-core';
|
|
25
26
|
|
|
@@ -141,7 +142,7 @@ export function computeBarLabels(
|
|
|
141
142
|
} else {
|
|
142
143
|
// Outside: just past the bar's right edge
|
|
143
144
|
anchorX = mark.x + mark.width + LABEL_PADDING;
|
|
144
|
-
fill = mark.fill;
|
|
145
|
+
fill = getRepresentativeColor(mark.fill);
|
|
145
146
|
textAnchor = 'start';
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -14,12 +14,13 @@ import type {
|
|
|
14
14
|
ConditionalValueDef,
|
|
15
15
|
DataRow,
|
|
16
16
|
Encoding,
|
|
17
|
+
GradientDef,
|
|
17
18
|
LayoutStrategy,
|
|
18
19
|
MarkAria,
|
|
19
20
|
Rect,
|
|
20
21
|
RectMark,
|
|
21
22
|
} from '@opendata-ai/openchart-core';
|
|
22
|
-
import { abbreviateNumber, formatNumber } from '@opendata-ai/openchart-core';
|
|
23
|
+
import { abbreviateNumber, formatNumber, isGradientDef } from '@opendata-ai/openchart-core';
|
|
23
24
|
import type { ScaleBand, ScaleLinear } from 'd3-scale';
|
|
24
25
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
25
26
|
import type { ResolvedScales } from '../../layout/scales';
|
|
@@ -169,11 +170,14 @@ function computeSimpleColumns(
|
|
|
169
170
|
const bandX = xScale(category);
|
|
170
171
|
if (bandX === undefined) continue;
|
|
171
172
|
|
|
172
|
-
let color: string;
|
|
173
|
+
let color: string | GradientDef;
|
|
173
174
|
if (conditionalColor) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
const resolved = resolveConditionalValue(row, conditionalColor);
|
|
176
|
+
if (resolved != null) {
|
|
177
|
+
color = isGradientDef(resolved) ? resolved : String(resolved);
|
|
178
|
+
} else {
|
|
179
|
+
color = getColor(scales, '__default__');
|
|
180
|
+
}
|
|
177
181
|
} else if (sequentialColor) {
|
|
178
182
|
color = getSequentialColor(scales, value);
|
|
179
183
|
} else {
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
import {
|
|
21
21
|
buildD3Formatter,
|
|
22
22
|
estimateTextWidth,
|
|
23
|
+
getRepresentativeColor,
|
|
23
24
|
resolveCollisions,
|
|
24
25
|
} from '@opendata-ai/openchart-core';
|
|
25
26
|
|
|
@@ -99,7 +100,7 @@ export function computeColumnLabels(
|
|
|
99
100
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
100
101
|
fontSize: LABEL_FONT_SIZE,
|
|
101
102
|
fontWeight: LABEL_FONT_WEIGHT,
|
|
102
|
-
fill: mark.fill,
|
|
103
|
+
fill: getRepresentativeColor(mark.fill),
|
|
103
104
|
lineHeight: 1.2,
|
|
104
105
|
textAnchor: 'middle',
|
|
105
106
|
dominantBaseline: isNegative ? 'hanging' : 'auto',
|
package/src/charts/dot/labels.ts
CHANGED
|
@@ -17,7 +17,11 @@ import type {
|
|
|
17
17
|
Rect,
|
|
18
18
|
ResolvedLabel,
|
|
19
19
|
} from '@opendata-ai/openchart-core';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
estimateTextWidth,
|
|
22
|
+
getRepresentativeColor,
|
|
23
|
+
resolveCollisions,
|
|
24
|
+
} from '@opendata-ai/openchart-core';
|
|
21
25
|
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
23
27
|
// Constants
|
|
@@ -74,7 +78,7 @@ export function computeDotLabels(
|
|
|
74
78
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
75
79
|
fontSize: LABEL_FONT_SIZE,
|
|
76
80
|
fontWeight: LABEL_FONT_WEIGHT,
|
|
77
|
-
fill: mark.fill,
|
|
81
|
+
fill: getRepresentativeColor(mark.fill),
|
|
78
82
|
lineHeight: 1.2,
|
|
79
83
|
textAnchor: 'start',
|
|
80
84
|
dominantBaseline: 'central',
|
package/src/charts/line/area.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { AreaMark, DataRow, Encoding, MarkAria, Rect } from '@opendata-ai/openchart-core';
|
|
10
|
+
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
10
11
|
import type { ScaleLinear } from 'd3-scale';
|
|
11
12
|
import { area, line, stack, stackOffsetNone, stackOrderNone } from 'd3-shape';
|
|
12
13
|
|
|
@@ -122,7 +123,7 @@ function computeSingleArea(
|
|
|
122
123
|
topPath: topPathStr,
|
|
123
124
|
fill: color,
|
|
124
125
|
fillOpacity: DEFAULT_FILL_OPACITY,
|
|
125
|
-
stroke: color,
|
|
126
|
+
stroke: getRepresentativeColor(color),
|
|
126
127
|
strokeWidth: 2,
|
|
127
128
|
seriesKey: seriesKey === '__default__' ? undefined : seriesKey,
|
|
128
129
|
data: validPoints.map((p) => p.row),
|
|
@@ -258,7 +259,7 @@ function computeStackedArea(
|
|
|
258
259
|
topPath: topPathStr,
|
|
259
260
|
fill: color,
|
|
260
261
|
fillOpacity: 0.7, // Higher opacity for stacked so layers are visible
|
|
261
|
-
stroke: color,
|
|
262
|
+
stroke: getRepresentativeColor(color),
|
|
262
263
|
strokeWidth: 1,
|
|
263
264
|
seriesKey,
|
|
264
265
|
data: layer.map((d) => {
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
import type {
|
|
11
11
|
DataRow,
|
|
12
12
|
Encoding,
|
|
13
|
+
GradientDef,
|
|
13
14
|
LayoutStrategy,
|
|
14
15
|
LineMark,
|
|
15
16
|
MarkAria,
|
|
16
17
|
PointMark,
|
|
17
18
|
Rect,
|
|
18
19
|
} from '@opendata-ai/openchart-core';
|
|
20
|
+
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
19
21
|
import { line } from 'd3-shape';
|
|
20
22
|
|
|
21
23
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
@@ -68,9 +70,10 @@ export function computeLineMarks(
|
|
|
68
70
|
|
|
69
71
|
for (const [seriesKey, rows] of groups) {
|
|
70
72
|
// For sequential color, use a mid-range color for the line stroke
|
|
71
|
-
const color = isSequentialColor
|
|
73
|
+
const color: string | GradientDef = isSequentialColor
|
|
72
74
|
? getSequentialColor(scales, _getMidValue(rows, sequentialColorField!))
|
|
73
75
|
: getColor(scales, seriesKey);
|
|
76
|
+
const strokeColor = getRepresentativeColor(color);
|
|
74
77
|
|
|
75
78
|
// Sort rows by x-axis field so lines draw left-to-right
|
|
76
79
|
const sortedRows = sortByField(rows, xChannel.field);
|
|
@@ -165,7 +168,7 @@ export function computeLineMarks(
|
|
|
165
168
|
type: 'line',
|
|
166
169
|
points: allPoints,
|
|
167
170
|
path: combinedPath,
|
|
168
|
-
stroke:
|
|
171
|
+
stroke: strokeColor,
|
|
169
172
|
strokeWidth: styleOverride?.strokeWidth ?? DEFAULT_STROKE_WIDTH,
|
|
170
173
|
strokeDasharray,
|
|
171
174
|
opacity: styleOverride?.opacity,
|
|
@@ -8,17 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
10
|
ArcMark,
|
|
11
|
+
ConditionalValueDef,
|
|
11
12
|
DataRow,
|
|
12
13
|
Encoding,
|
|
14
|
+
GradientDef,
|
|
13
15
|
LayoutStrategy,
|
|
14
16
|
MarkAria,
|
|
15
17
|
Rect,
|
|
16
18
|
} from '@opendata-ai/openchart-core';
|
|
19
|
+
import { isConditionalDef, isGradientDef } from '@opendata-ai/openchart-core';
|
|
17
20
|
import type { PieArcDatum } from 'd3-shape';
|
|
18
21
|
import { arc as d3Arc, pie as d3Pie } from 'd3-shape';
|
|
19
22
|
|
|
20
23
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
21
24
|
import type { ResolvedScales } from '../../layout/scales';
|
|
25
|
+
import { resolveConditionalValue } from '../../transforms/conditional';
|
|
22
26
|
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
24
28
|
// Constants
|
|
@@ -109,6 +113,10 @@ export function computePieMarks(
|
|
|
109
113
|
const valueChannel = encoding.y ?? encoding.x;
|
|
110
114
|
const categoryField =
|
|
111
115
|
encoding.color && 'field' in encoding.color ? encoding.color.field : undefined;
|
|
116
|
+
const conditionalColor =
|
|
117
|
+
encoding.color && isConditionalDef(encoding.color)
|
|
118
|
+
? (encoding.color as ConditionalValueDef)
|
|
119
|
+
: undefined;
|
|
112
120
|
|
|
113
121
|
if (!valueChannel) return [];
|
|
114
122
|
|
|
@@ -190,9 +198,22 @@ export function computePieMarks(
|
|
|
190
198
|
const arcDatum = arcs[i];
|
|
191
199
|
const slice = arcDatum.data;
|
|
192
200
|
|
|
193
|
-
// Get color
|
|
194
|
-
let color: string;
|
|
195
|
-
if (
|
|
201
|
+
// Get color: conditional (supports gradients) > scale > default palette
|
|
202
|
+
let color: string | GradientDef;
|
|
203
|
+
if (conditionalColor) {
|
|
204
|
+
const resolved = resolveConditionalValue(
|
|
205
|
+
slice.originalRow as Record<string, unknown>,
|
|
206
|
+
conditionalColor,
|
|
207
|
+
);
|
|
208
|
+
if (resolved != null) {
|
|
209
|
+
color = isGradientDef(resolved) ? resolved : String(resolved);
|
|
210
|
+
} else if (scales.color && categoryField) {
|
|
211
|
+
const colorScale = scales.color.scale as (v: string) => string;
|
|
212
|
+
color = colorScale(slice.label);
|
|
213
|
+
} else {
|
|
214
|
+
color = DEFAULT_PALETTE[i % DEFAULT_PALETTE.length];
|
|
215
|
+
}
|
|
216
|
+
} else if (scales.color && categoryField) {
|
|
196
217
|
const colorScale = scales.color.scale as (v: string) => string;
|
|
197
218
|
color = colorScale(slice.label);
|
|
198
219
|
} else {
|
package/src/charts/rule/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Encoding, Mark, MarkAria, Rect, RuleMarkLayout } from '@opendata-ai/openchart-core';
|
|
10
|
+
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
10
11
|
|
|
11
12
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
12
13
|
import type { ResolvedScales } from '../../layout/scales';
|
|
@@ -84,9 +85,11 @@ export function computeRuleMarks(
|
|
|
84
85
|
if (y2Val != null) y2 = y2Val;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
const color =
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const color = getRepresentativeColor(
|
|
89
|
+
colorField
|
|
90
|
+
? getColor(scales, String(row[colorField] ?? '__default__'))
|
|
91
|
+
: getColor(scales, '__default__'),
|
|
92
|
+
);
|
|
90
93
|
|
|
91
94
|
const strokeDashEncoding =
|
|
92
95
|
encoding.strokeDash && 'field' in encoding.strokeDash ? encoding.strokeDash : undefined;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type {
|
|
11
11
|
Encoding,
|
|
12
12
|
FieldType,
|
|
13
|
+
GradientDef,
|
|
13
14
|
LayoutStrategy,
|
|
14
15
|
MarkAria,
|
|
15
16
|
PointMark,
|
|
@@ -139,7 +140,7 @@ export function computeScatterMarks(
|
|
|
139
140
|
if (cx === undefined || cy === undefined) continue;
|
|
140
141
|
|
|
141
142
|
const category = colorField && !isSequentialColor ? String(row[colorField] ?? '') : undefined;
|
|
142
|
-
let color: string;
|
|
143
|
+
let color: string | GradientDef;
|
|
143
144
|
if (isSequentialColor && colorField) {
|
|
144
145
|
const val = Number(row[colorField]);
|
|
145
146
|
color = Number.isFinite(val)
|
package/src/charts/text/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Encoding, Mark, MarkAria, TextMarkLayout } from '@opendata-ai/openchart-core';
|
|
10
|
+
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
10
11
|
|
|
11
12
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
12
13
|
import type { ResolvedScales } from '../../layout/scales';
|
|
@@ -52,9 +53,11 @@ export function computeTextMarks(
|
|
|
52
53
|
const text = String(row[textChannel.field] ?? '');
|
|
53
54
|
if (!text) continue;
|
|
54
55
|
|
|
55
|
-
const color =
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const color = getRepresentativeColor(
|
|
57
|
+
colorField
|
|
58
|
+
? getColor(scales, String(row[colorField] ?? '__default__'))
|
|
59
|
+
: getColor(scales, '__default__'),
|
|
60
|
+
);
|
|
58
61
|
|
|
59
62
|
const fontSize = sizeEncoding
|
|
60
63
|
? Math.max(8, Math.min(48, Number(row[sizeEncoding.field]) || 12))
|
package/src/charts/tick/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Encoding, Mark, MarkAria, Rect, TickMarkLayout } from '@opendata-ai/openchart-core';
|
|
10
|
+
import { getRepresentativeColor } from '@opendata-ai/openchart-core';
|
|
10
11
|
|
|
11
12
|
import type { NormalizedChartSpec } from '../../compiler/types';
|
|
12
13
|
import type { ResolvedScales } from '../../layout/scales';
|
|
@@ -48,9 +49,11 @@ export function computeTickMarks(
|
|
|
48
49
|
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
49
50
|
if (xVal == null || yVal == null) continue;
|
|
50
51
|
|
|
51
|
-
const color =
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const color = getRepresentativeColor(
|
|
53
|
+
colorField
|
|
54
|
+
? getColor(scales, String(row[colorField] ?? '__default__'))
|
|
55
|
+
: getColor(scales, '__default__'),
|
|
56
|
+
);
|
|
54
57
|
|
|
55
58
|
const aria: MarkAria = {
|
|
56
59
|
label: `${row[xChannel.field]}, ${row[yChannel.field]}`,
|
package/src/charts/utils.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* data grouping, color lookup, and shared constants.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { DataRow } from '@opendata-ai/openchart-core';
|
|
8
|
+
import type { DataRow, GradientDef } from '@opendata-ai/openchart-core';
|
|
9
9
|
import type { ScaleBand, ScaleLinear, ScalePoint, ScaleTime } from 'd3-scale';
|
|
10
10
|
import type { D3Scale, ResolvedScales } from '../layout/scales';
|
|
11
11
|
|
|
@@ -143,7 +143,7 @@ export function getColor(
|
|
|
143
143
|
key: string,
|
|
144
144
|
_index?: number,
|
|
145
145
|
fallback: string = DEFAULT_COLOR,
|
|
146
|
-
): string {
|
|
146
|
+
): string | GradientDef {
|
|
147
147
|
if (scales.color && key !== '__default__') {
|
|
148
148
|
const colorScale = scales.color.scale as (v: string) => string;
|
|
149
149
|
return colorScale(key);
|
|
@@ -159,7 +159,7 @@ export function getSequentialColor(
|
|
|
159
159
|
scales: ResolvedScales,
|
|
160
160
|
value: number,
|
|
161
161
|
fallback: string = DEFAULT_COLOR,
|
|
162
|
-
): string {
|
|
162
|
+
): string | GradientDef {
|
|
163
163
|
if (scales.color?.type === 'sequential') {
|
|
164
164
|
const colorScale = scales.color.scale as unknown as (v: number) => string;
|
|
165
165
|
return colorScale(value);
|
package/src/compile.ts
CHANGED
|
@@ -222,6 +222,10 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
222
222
|
|
|
223
223
|
let chartSpec = normalized as NormalizedChartSpec;
|
|
224
224
|
|
|
225
|
+
// Resolve watermark: explicit spec value wins, then options fallback, then default true.
|
|
226
|
+
const rawWatermark = (spec as Record<string, unknown>).watermark;
|
|
227
|
+
const watermark = rawWatermark !== undefined ? chartSpec.watermark : (options.watermark ?? true);
|
|
228
|
+
|
|
225
229
|
// Run data transforms (filter, bin, calculate, timeUnit) before any other data processing.
|
|
226
230
|
// Transforms are defined on the original spec, not the normalized spec, since
|
|
227
231
|
// NormalizedChartSpec doesn't carry the transform field.
|
|
@@ -306,10 +310,10 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
306
310
|
width: options.width,
|
|
307
311
|
height: options.height,
|
|
308
312
|
};
|
|
309
|
-
const legendLayout = computeLegend(chartSpec, strategy, theme, preliminaryArea);
|
|
313
|
+
const legendLayout = computeLegend(chartSpec, strategy, theme, preliminaryArea, watermark);
|
|
310
314
|
|
|
311
315
|
// Compute dimensions (accounts for chrome + legend + responsive strategy)
|
|
312
|
-
const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy);
|
|
316
|
+
const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy, watermark);
|
|
313
317
|
const chartArea = dims.chartArea;
|
|
314
318
|
|
|
315
319
|
// Recompute legend bounds relative to actual chart area.
|
|
@@ -332,7 +336,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
332
336
|
break;
|
|
333
337
|
}
|
|
334
338
|
}
|
|
335
|
-
const finalLegend = computeLegend(chartSpec, strategy, theme, legendArea);
|
|
339
|
+
const finalLegend = computeLegend(chartSpec, strategy, theme, legendArea, watermark);
|
|
336
340
|
|
|
337
341
|
// Apply data filtering after legend (so legend retains all series), but before
|
|
338
342
|
// scale computation (so hidden/clipped data doesn't affect domains or marks).
|
|
@@ -386,8 +390,9 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
386
390
|
}
|
|
387
391
|
}
|
|
388
392
|
|
|
389
|
-
// Set default color for single-series charts
|
|
390
|
-
|
|
393
|
+
// Set default color for single-series charts. If the user set a fill on the mark def
|
|
394
|
+
// (string or gradient), that takes priority over the theme's first categorical color.
|
|
395
|
+
scales.defaultColor = chartSpec.markDef.fill ?? theme.colors.categorical[0];
|
|
391
396
|
|
|
392
397
|
// Arc charts (pie/donut) don't use axes or gridlines
|
|
393
398
|
const isRadial = chartSpec.markType === 'arc';
|
|
@@ -443,14 +448,16 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
443
448
|
// Add brand watermark as an obstacle so annotations avoid overlapping it.
|
|
444
449
|
// The brand is right-aligned on the same baseline as the first bottom chrome element,
|
|
445
450
|
// offset below the chart area by x-axis extent (tick labels + axis title).
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
451
|
+
if (watermark) {
|
|
452
|
+
const brandPadding = theme.spacing.padding;
|
|
453
|
+
const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH;
|
|
454
|
+
const xAxisExtent = axes.x?.label ? 48 : axes.x ? 26 : 0;
|
|
455
|
+
const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
|
|
456
|
+
const brandY = firstBottomChrome
|
|
457
|
+
? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y
|
|
458
|
+
: chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
|
|
459
|
+
obstacles.push({ x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH, height: 30 });
|
|
460
|
+
}
|
|
454
461
|
const annotations: ResolvedAnnotation[] = computeAnnotations(
|
|
455
462
|
chartSpec,
|
|
456
463
|
scales,
|
|
@@ -546,6 +553,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
546
553
|
height: options.height,
|
|
547
554
|
},
|
|
548
555
|
animation: resolvedAnimation,
|
|
556
|
+
watermark,
|
|
549
557
|
};
|
|
550
558
|
}
|
|
551
559
|
|
|
@@ -655,6 +663,7 @@ function buildPrimarySpec(leaves: ChartSpec[], layerSpec: LayerSpec): ChartSpec
|
|
|
655
663
|
responsive: layerSpec.responsive ?? leaves[0].responsive,
|
|
656
664
|
theme: layerSpec.theme ?? leaves[0].theme,
|
|
657
665
|
darkMode: layerSpec.darkMode ?? leaves[0].darkMode,
|
|
666
|
+
watermark: layerSpec.watermark ?? leaves[0].watermark,
|
|
658
667
|
hiddenSeries: layerSpec.hiddenSeries ?? leaves[0].hiddenSeries,
|
|
659
668
|
};
|
|
660
669
|
|
|
@@ -696,7 +705,11 @@ export function compileTable(spec: unknown, options: CompileTableOptions): Table
|
|
|
696
705
|
theme = adaptTheme(theme);
|
|
697
706
|
}
|
|
698
707
|
|
|
699
|
-
|
|
708
|
+
// Resolve watermark: spec-level wins, then options, then default true
|
|
709
|
+
const rawWatermark = (spec as Record<string, unknown>).watermark;
|
|
710
|
+
const watermark = rawWatermark !== undefined ? tableSpec.watermark : (options.watermark ?? true);
|
|
711
|
+
|
|
712
|
+
return compileTableLayout({ ...tableSpec, watermark }, options, theme);
|
|
700
713
|
}
|
|
701
714
|
|
|
702
715
|
// ---------------------------------------------------------------------------
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChartSpec,
|
|
3
3
|
GraphSpec,
|
|
4
|
+
LayerSpec,
|
|
4
5
|
RangeAnnotation,
|
|
5
6
|
RefLineAnnotation,
|
|
7
|
+
SankeySpec,
|
|
6
8
|
TableSpec,
|
|
7
9
|
TextAnnotation,
|
|
8
10
|
} from '@opendata-ai/openchart-core';
|
|
9
11
|
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import type { NormalizedSankeySpec } from '../../sankey/types';
|
|
10
13
|
import { normalizeSpec } from '../normalize';
|
|
11
14
|
import type { NormalizedChartSpec, NormalizedGraphSpec, NormalizedTableSpec } from '../types';
|
|
12
15
|
|
|
@@ -52,6 +55,17 @@ describe('normalizeSpec', () => {
|
|
|
52
55
|
});
|
|
53
56
|
});
|
|
54
57
|
|
|
58
|
+
it('watermark defaults to true when not specified', () => {
|
|
59
|
+
const result = normalizeSpec(lineSpec) as NormalizedChartSpec;
|
|
60
|
+
expect(result.watermark).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('watermark respects explicit false', () => {
|
|
64
|
+
const spec: ChartSpec = { ...lineSpec, watermark: false };
|
|
65
|
+
const result = normalizeSpec(spec) as NormalizedChartSpec;
|
|
66
|
+
expect(result.watermark).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
55
69
|
it('preserves explicit values', () => {
|
|
56
70
|
const spec: ChartSpec = {
|
|
57
71
|
...lineSpec,
|
|
@@ -205,6 +219,27 @@ describe('normalizeSpec', () => {
|
|
|
205
219
|
expect(result.responsive).toBe(true);
|
|
206
220
|
expect(result.darkMode).toBe('off');
|
|
207
221
|
});
|
|
222
|
+
|
|
223
|
+
it('watermark defaults to true when not specified', () => {
|
|
224
|
+
const spec: TableSpec = {
|
|
225
|
+
type: 'table',
|
|
226
|
+
data: [{ name: 'Alice', age: 30 }],
|
|
227
|
+
columns: [{ key: 'name' }, { key: 'age' }],
|
|
228
|
+
};
|
|
229
|
+
const result = normalizeSpec(spec) as NormalizedTableSpec;
|
|
230
|
+
expect(result.watermark).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('watermark respects explicit false', () => {
|
|
234
|
+
const spec: TableSpec = {
|
|
235
|
+
type: 'table',
|
|
236
|
+
data: [{ name: 'Alice', age: 30 }],
|
|
237
|
+
columns: [{ key: 'name' }, { key: 'age' }],
|
|
238
|
+
watermark: false,
|
|
239
|
+
};
|
|
240
|
+
const result = normalizeSpec(spec) as NormalizedTableSpec;
|
|
241
|
+
expect(result.watermark).toBe(false);
|
|
242
|
+
});
|
|
208
243
|
});
|
|
209
244
|
|
|
210
245
|
describe('graph spec normalization', () => {
|
|
@@ -221,5 +256,80 @@ describe('normalizeSpec', () => {
|
|
|
221
256
|
expect(result.annotations).toEqual([]);
|
|
222
257
|
expect(result.darkMode).toBe('off');
|
|
223
258
|
});
|
|
259
|
+
|
|
260
|
+
it('watermark defaults to true when not specified', () => {
|
|
261
|
+
const spec: GraphSpec = {
|
|
262
|
+
type: 'graph',
|
|
263
|
+
nodes: [{ id: 'a' }, { id: 'b' }],
|
|
264
|
+
edges: [{ source: 'a', target: 'b' }],
|
|
265
|
+
};
|
|
266
|
+
const result = normalizeSpec(spec) as NormalizedGraphSpec;
|
|
267
|
+
expect(result.watermark).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('watermark respects explicit false', () => {
|
|
271
|
+
const spec: GraphSpec = {
|
|
272
|
+
type: 'graph',
|
|
273
|
+
nodes: [{ id: 'a' }, { id: 'b' }],
|
|
274
|
+
edges: [{ source: 'a', target: 'b' }],
|
|
275
|
+
watermark: false,
|
|
276
|
+
};
|
|
277
|
+
const result = normalizeSpec(spec) as NormalizedGraphSpec;
|
|
278
|
+
expect(result.watermark).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('sankey spec normalization', () => {
|
|
283
|
+
const baseSankeySpec: SankeySpec = {
|
|
284
|
+
type: 'sankey',
|
|
285
|
+
data: [{ source: 'A', target: 'B', value: 10 }],
|
|
286
|
+
encoding: {
|
|
287
|
+
source: { field: 'source' },
|
|
288
|
+
target: { field: 'target' },
|
|
289
|
+
value: { field: 'value' },
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
it('watermark defaults to true when not specified', () => {
|
|
294
|
+
const result = normalizeSpec(baseSankeySpec) as NormalizedSankeySpec;
|
|
295
|
+
expect(result.watermark).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('watermark respects explicit false', () => {
|
|
299
|
+
const spec: SankeySpec = { ...baseSankeySpec, watermark: false };
|
|
300
|
+
const result = normalizeSpec(spec) as NormalizedSankeySpec;
|
|
301
|
+
expect(result.watermark).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('layer spec normalization', () => {
|
|
306
|
+
const baseLeaf: ChartSpec = {
|
|
307
|
+
mark: 'line',
|
|
308
|
+
data: [{ x: 1, y: 2 }],
|
|
309
|
+
encoding: {
|
|
310
|
+
x: { field: 'x', type: 'quantitative' },
|
|
311
|
+
y: { field: 'y', type: 'quantitative' },
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
it('watermark defaults to true for layer leaves', () => {
|
|
316
|
+
const layerSpec: LayerSpec = { layer: [baseLeaf] };
|
|
317
|
+
const result = normalizeSpec(layerSpec) as NormalizedChartSpec;
|
|
318
|
+
expect(result.watermark).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('layer-level watermark: false propagates to leaves', () => {
|
|
322
|
+
const layerSpec: LayerSpec = { layer: [baseLeaf], watermark: false };
|
|
323
|
+
const result = normalizeSpec(layerSpec) as NormalizedChartSpec;
|
|
324
|
+
expect(result.watermark).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('leaf-level watermark overrides layer-level', () => {
|
|
328
|
+
const leaf: ChartSpec = { ...baseLeaf, watermark: true };
|
|
329
|
+
const layerSpec: LayerSpec = { layer: [leaf], watermark: false };
|
|
330
|
+
const result = normalizeSpec(layerSpec) as NormalizedChartSpec;
|
|
331
|
+
// Leaf explicitly sets true, which should be preserved
|
|
332
|
+
expect(result.watermark).toBe(true);
|
|
333
|
+
});
|
|
224
334
|
});
|
|
225
335
|
});
|