@pushwoosh/kit-charts 0.0.1 → 0.0.3
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/ai-documentation.md +19 -0
- package/core/Chart/Axes.js +38 -51
- package/core/Chart/Chart.js +45 -53
- package/core/Chart/Hinted.js +1 -2
- package/core/Chart/Legend.js +14 -15
- package/core/Chart/PlotDecorations.js +63 -36
- package/core/Chart/StackLabels.js +6 -7
- package/core/Chart/Titles.js +14 -17
- package/core/Chart/Tooltip.js +12 -13
- package/core/data/points.js +1 -1
- package/core/geometry/path.js +1 -2
- package/core/layout/legend-layout.js +12 -12
- package/core/layout/measure-text.d.ts +5 -0
- package/core/layout/measure-text.js +8 -3
- package/core/layout/metrics.d.ts +7 -0
- package/core/layout/metrics.js +7 -0
- package/core/layout/title-layout.js +9 -15
- package/core/model/build-model.js +44 -69
- package/core/model/palette.d.ts +8 -0
- package/core/model/palette.js +31 -0
- package/core/options/normalize.js +1 -2
- package/core/registry/registry.js +1 -1
- package/core/types/options.d.ts +4 -0
- package/package.json +2 -2
- package/series/area/area.js +2 -3
- package/series/column/column.js +4 -8
- package/series/heatmap/heatmap.js +5 -8
- package/series/line/line.js +2 -3
- package/series/pie/pie.js +6 -8
- package/series/shared/DataLabels.js +8 -8
- package/series/shared/Markers.js +8 -8
|
@@ -18,8 +18,7 @@ const DEFAULT_SPACING = [10, 10, 15, 10];
|
|
|
18
18
|
const isBand = scale => scale.kind === 'band';
|
|
19
19
|
const isContinuous = scale => scale.kind === 'linear' || scale.kind === 'time';
|
|
20
20
|
function tickLabel(value, axis, timePattern, index, count, kind, numberFormat) {
|
|
21
|
-
|
|
22
|
-
const formatter = (_axis$labels = axis.labels) === null || _axis$labels === void 0 ? void 0 : _axis$labels.formatter;
|
|
21
|
+
const formatter = axis.labels?.formatter;
|
|
23
22
|
if (formatter) {
|
|
24
23
|
const node = formatter({
|
|
25
24
|
value,
|
|
@@ -37,7 +36,7 @@ function tickLabel(value, axis, timePattern, index, count, kind, numberFormat) {
|
|
|
37
36
|
}
|
|
38
37
|
return '';
|
|
39
38
|
}
|
|
40
|
-
if (
|
|
39
|
+
if (axis.labels?.format) {
|
|
41
40
|
return applyValueFormat(axis.labels.format, value);
|
|
42
41
|
}
|
|
43
42
|
if (typeof value === 'number') {
|
|
@@ -46,8 +45,7 @@ function tickLabel(value, axis, timePattern, index, count, kind, numberFormat) {
|
|
|
46
45
|
return String(value);
|
|
47
46
|
}
|
|
48
47
|
function collectCategories(axis, series, numeric = false) {
|
|
49
|
-
|
|
50
|
-
if ((_axis$categories = axis.categories) !== null && _axis$categories !== void 0 && _axis$categories.length) {
|
|
48
|
+
if (axis.categories?.length) {
|
|
51
49
|
return [...axis.categories];
|
|
52
50
|
}
|
|
53
51
|
const seen = [];
|
|
@@ -104,11 +102,10 @@ function buildXScale(axis, kind, extent, categories, range, hint) {
|
|
|
104
102
|
});
|
|
105
103
|
}
|
|
106
104
|
function buildYScale(axis, extent, range, hint, linked) {
|
|
107
|
-
|
|
108
|
-
if ((_axis$categories2 = axis.categories) !== null && _axis$categories2 !== void 0 && _axis$categories2.length) {
|
|
105
|
+
if (axis.categories?.length) {
|
|
109
106
|
return createBandScale({
|
|
110
107
|
domain: [...axis.categories],
|
|
111
|
-
range:
|
|
108
|
+
range: linked?.kind === 'band' ? linked.range() : range,
|
|
112
109
|
paddingInner: 0,
|
|
113
110
|
paddingOuter: 0,
|
|
114
111
|
// A y axis counts up from the bottom; `reversed` puts the first row on top.
|
|
@@ -133,13 +130,12 @@ function buildYScale(axis, extent, range, hint, linked) {
|
|
|
133
130
|
});
|
|
134
131
|
}
|
|
135
132
|
function buildColorScale(options, extent, colors) {
|
|
136
|
-
var _axis$stops;
|
|
137
133
|
const axis = options.colorAxis;
|
|
138
134
|
if (!axis) {
|
|
139
135
|
return undefined;
|
|
140
136
|
}
|
|
141
137
|
const [min, max] = settled(extent, [0, 1]);
|
|
142
|
-
const stops =
|
|
138
|
+
const stops = axis.stops?.length ? axis.stops : [[0, DEFAULT_SURFACE_COLOR], [1, colorAt(colors, 0)]];
|
|
143
139
|
return createColorScale({
|
|
144
140
|
domain: [axis.min ?? min, axis.max ?? max],
|
|
145
141
|
stops
|
|
@@ -151,15 +147,14 @@ const EXPLICIT_ANCHOR = {
|
|
|
151
147
|
right: 'end'
|
|
152
148
|
};
|
|
153
149
|
function tickAnchor(label, position, axis, bounds) {
|
|
154
|
-
|
|
155
|
-
const explicit = ((_axis$labels3 = axis.labels) === null || _axis$labels3 === void 0 ? void 0 : _axis$labels3.align) && EXPLICIT_ANCHOR[axis.labels.align];
|
|
150
|
+
const explicit = axis.labels?.align && EXPLICIT_ANCHOR[axis.labels.align];
|
|
156
151
|
if (explicit) {
|
|
157
152
|
return explicit;
|
|
158
153
|
}
|
|
159
154
|
if (!bounds || !label) {
|
|
160
155
|
return 'middle';
|
|
161
156
|
}
|
|
162
|
-
const half = measureText(label,
|
|
157
|
+
const half = measureText(label, axis.labels?.style) / 2;
|
|
163
158
|
if (position - half < bounds[0]) {
|
|
164
159
|
return 'start';
|
|
165
160
|
}
|
|
@@ -174,11 +169,10 @@ function tickAnchor(label, position, axis, bounds) {
|
|
|
174
169
|
* axis readable instead of turning it into a smear.
|
|
175
170
|
*/
|
|
176
171
|
function dropOverlapping(ticks, axis, along) {
|
|
177
|
-
|
|
178
|
-
if (((_axis$labels5 = axis.labels) === null || _axis$labels5 === void 0 ? void 0 : _axis$labels5.allowOverlap) === true || ticks.length < 2) {
|
|
172
|
+
if (axis.labels?.allowOverlap === true || ticks.length < 2) {
|
|
179
173
|
return ticks;
|
|
180
174
|
}
|
|
181
|
-
const style =
|
|
175
|
+
const style = axis.labels?.style;
|
|
182
176
|
// A y axis counts upwards, so its tick positions descend. Comparing distance
|
|
183
177
|
// rather than order keeps one rule for both directions.
|
|
184
178
|
let kept = null;
|
|
@@ -201,9 +195,8 @@ function dropOverlapping(ticks, axis, along) {
|
|
|
201
195
|
});
|
|
202
196
|
}
|
|
203
197
|
function axisTicks(axis, scale, hint, timePattern, along, numberFormat, bounds) {
|
|
204
|
-
var _axis$tickPositions;
|
|
205
198
|
let values;
|
|
206
|
-
if (
|
|
199
|
+
if (axis.tickPositions?.length) {
|
|
207
200
|
values = [...axis.tickPositions];
|
|
208
201
|
} else if (isBand(scale)) {
|
|
209
202
|
values = scale.ticks();
|
|
@@ -218,7 +211,6 @@ function axisTicks(axis, scale, hint, timePattern, along, numberFormat, bounds)
|
|
|
218
211
|
}
|
|
219
212
|
const offset = isBand(scale) ? scale.bandwidth() / 2 : 0;
|
|
220
213
|
const built = values.map((value, index) => {
|
|
221
|
-
var _axis$labels7, _axis$labels7$hint;
|
|
222
214
|
const position = (isBand(scale) ? scale.map(String(value)) : scale.map(Number(value))) + offset;
|
|
223
215
|
const label = tickLabel(value, axis, timePattern, index, values.length, scale.kind, numberFormat);
|
|
224
216
|
return {
|
|
@@ -226,7 +218,7 @@ function axisTicks(axis, scale, hint, timePattern, along, numberFormat, bounds)
|
|
|
226
218
|
position,
|
|
227
219
|
label,
|
|
228
220
|
anchor: tickAnchor(label, position, axis, bounds),
|
|
229
|
-
hint:
|
|
221
|
+
hint: axis.labels?.hint?.({
|
|
230
222
|
value,
|
|
231
223
|
index,
|
|
232
224
|
isFirst: index === 0,
|
|
@@ -272,8 +264,7 @@ export function buildChartModel({
|
|
|
272
264
|
renderers,
|
|
273
265
|
hidden
|
|
274
266
|
}) {
|
|
275
|
-
|
|
276
|
-
const colors = (_options$colors = options.colors) !== null && _options$colors !== void 0 && _options$colors.length ? options.colors : DEFAULT_COLORS;
|
|
267
|
+
const colors = options.colors?.length ? options.colors : DEFAULT_COLORS;
|
|
277
268
|
const numberFormat = options.numberFormat ?? String;
|
|
278
269
|
const {
|
|
279
270
|
xAxes,
|
|
@@ -292,8 +283,8 @@ export function buildChartModel({
|
|
|
292
283
|
index,
|
|
293
284
|
color: series.color ?? colorAt(colors, index),
|
|
294
285
|
points: normalizePoints(series.data, series.name ?? `series ${index}`),
|
|
295
|
-
hiddenPoints: new Set(normalizePoints(series.data, series.name ?? `series ${index}`).filter(point => hidden
|
|
296
|
-
hidden: !isSeriesVisible(series) || (
|
|
286
|
+
hiddenPoints: new Set(normalizePoints(series.data, series.name ?? `series ${index}`).filter(point => hidden?.has(hiddenKey(index, point.index))).map(point => point.index)),
|
|
287
|
+
hidden: !isSeriesVisible(series) || (hidden?.has(hiddenKey(index)) ?? false)
|
|
297
288
|
});
|
|
298
289
|
});
|
|
299
290
|
// Everything that shapes the chart — domains, stacks, column slots — comes from
|
|
@@ -303,12 +294,12 @@ export function buildChartModel({
|
|
|
303
294
|
// the series being switched off would jump there before it finished fading, so
|
|
304
295
|
// the chart holds the shape it had until something is switched back on.
|
|
305
296
|
const resolved = shown.length ? shown : all;
|
|
306
|
-
const explicitCategories = Boolean(
|
|
297
|
+
const explicitCategories = Boolean(xAxisOptions.categories?.length);
|
|
307
298
|
const axisless = resolved.length > 0 && resolved.every(({
|
|
308
299
|
renderer
|
|
309
300
|
}) => renderer.scales.x === 'none');
|
|
310
301
|
const labelled = collectCategories(xAxisOptions, resolved);
|
|
311
|
-
const rendererXKind =
|
|
302
|
+
const rendererXKind = resolved[0]?.renderer.scales.x;
|
|
312
303
|
const xKind = (() => {
|
|
313
304
|
if (xAxisOptions.type === 'datetime') {
|
|
314
305
|
return 'time';
|
|
@@ -365,7 +356,7 @@ export function buildChartModel({
|
|
|
365
356
|
grow(xExtent, point.x);
|
|
366
357
|
}
|
|
367
358
|
grow(colorExtent, point.value);
|
|
368
|
-
const cumulative = stacked
|
|
359
|
+
const cumulative = stacked?.[pointIndex];
|
|
369
360
|
if (cumulative) {
|
|
370
361
|
grow(yExtents[yIndex], cumulative.from);
|
|
371
362
|
grow(yExtents[yIndex], cumulative.to);
|
|
@@ -399,16 +390,16 @@ export function buildChartModel({
|
|
|
399
390
|
max: acc.x.max + pad
|
|
400
391
|
} : acc.x;
|
|
401
392
|
const yDomains = acc.y;
|
|
402
|
-
const spacing =
|
|
393
|
+
const spacing = options.chart?.spacing ?? DEFAULT_SPACING;
|
|
403
394
|
const [spaceTop, spaceRight, spaceBottom, spaceLeft] = spacing;
|
|
404
395
|
const labelsOnTop = xAxisOptions.opposite === true;
|
|
405
|
-
const labelStyle =
|
|
396
|
+
const labelStyle = xAxisOptions.labels?.style;
|
|
406
397
|
// A tick label is drawn a tick's length plus a fixed drop away from the axis
|
|
407
398
|
// line, and `labels.y` pushes it further still. Reserving only its text height
|
|
408
399
|
// leaves it hanging over the chart edge, where the svg clips it.
|
|
409
|
-
const xLabelPush = labelsOnTop ? Math.max(0, -(
|
|
410
|
-
const xLabelHeight = axisless ||
|
|
411
|
-
const legendEnabled =
|
|
400
|
+
const xLabelPush = labelsOnTop ? Math.max(0, -(xAxisOptions.labels?.y ?? 0)) : Math.max(0, xAxisOptions.labels?.y ?? 0);
|
|
401
|
+
const xLabelHeight = axisless || xAxisOptions.labels?.enabled === false ? 0 : textHeight(labelStyle) + (xAxisOptions.tickLength ?? 0) + (labelsOnTop ? AXIS_LABEL_GAP : X_AXIS_LABEL_DROP) + xLabelPush;
|
|
402
|
+
const legendEnabled = options.legend?.enabled !== false;
|
|
412
403
|
const legend = legendEnabled ? all.flatMap(({
|
|
413
404
|
renderer,
|
|
414
405
|
options: series,
|
|
@@ -419,59 +410,47 @@ export function buildChartModel({
|
|
|
419
410
|
return items.map((item, ordinal) => ({
|
|
420
411
|
...item,
|
|
421
412
|
color: item.color || colorAt(colors, items.length > 1 ? ordinal : index),
|
|
422
|
-
visible: !off && !
|
|
413
|
+
visible: !off && !hidden?.has(hiddenKey(index, item.pointIndex))
|
|
423
414
|
}));
|
|
424
415
|
}) : [];
|
|
425
416
|
const legendMetrics = measureLegend(legend, options.legend);
|
|
426
417
|
const legendHeight = legendMetrics.height;
|
|
427
|
-
const legendAtTop = (
|
|
418
|
+
const legendAtTop = (options.legend?.verticalAlign ?? 'bottom') === 'top';
|
|
428
419
|
const titles = layoutTitles(options, size, spacing);
|
|
429
420
|
const titleHeight = titles.height;
|
|
430
|
-
const yLabelsShown = !axisless &&
|
|
421
|
+
const yLabelsShown = !axisless && yAxes[0]?.visible !== false && yAxes[0]?.labels?.enabled !== false;
|
|
431
422
|
const markerReach = resolved.some(({
|
|
432
423
|
options: series
|
|
433
|
-
}) =>
|
|
434
|
-
var _series$marker;
|
|
435
|
-
return ((_series$marker = series.marker) === null || _series$marker === void 0 ? void 0 : _series$marker.enabled) !== false;
|
|
436
|
-
}) ? MARKER_REACH : 0;
|
|
424
|
+
}) => series.marker?.enabled !== false) ? MARKER_REACH : 0;
|
|
437
425
|
// The topmost tick label is centred on the plot's top edge, so half of it hangs
|
|
438
426
|
// above the plot: without this the highest grid line and its label sit on the
|
|
439
427
|
// chart border.
|
|
440
|
-
const topOverflow = Math.max(yLabelsShown ? Math.ceil(textHeight(
|
|
428
|
+
const topOverflow = Math.max(yLabelsShown ? Math.ceil(textHeight(yAxes[0]?.labels?.style) / 2) : 0, axisless ? 0 : markerReach);
|
|
441
429
|
// Point and stack labels are drawn above the shape they belong to, so the plot
|
|
442
430
|
// has to end below the chart edge — otherwise the topmost value sits on the
|
|
443
431
|
// border, or gets pushed inside the shape to avoid being cut off.
|
|
444
432
|
const outsideLabels = axisless ? undefined : resolved.find(({
|
|
445
433
|
options: series,
|
|
446
434
|
renderer
|
|
447
|
-
}) =>
|
|
448
|
-
|
|
449
|
-
return ((_series$dataLabels = series.dataLabels) === null || _series$dataLabels === void 0 ? void 0 : _series$dataLabels.enabled) && !series.stacking && renderer.scales.y === 'linear';
|
|
450
|
-
});
|
|
451
|
-
const stackLabels = yAxes.find(axis => {
|
|
452
|
-
var _axis$stackLabels;
|
|
453
|
-
return (_axis$stackLabels = axis.stackLabels) === null || _axis$stackLabels === void 0 ? void 0 : _axis$stackLabels.enabled;
|
|
454
|
-
});
|
|
435
|
+
}) => series.dataLabels?.enabled && !series.stacking && renderer.scales.y === 'linear');
|
|
436
|
+
const stackLabels = yAxes.find(axis => axis.stackLabels?.enabled);
|
|
455
437
|
const labelHeadroom = (() => {
|
|
456
438
|
if (outsideLabels) {
|
|
457
|
-
|
|
458
|
-
return textHeight((_outsideLabels$option = outsideLabels.options.dataLabels) === null || _outsideLabels$option === void 0 ? void 0 : _outsideLabels$option.style) + VALUE_LABEL_HEADROOM;
|
|
439
|
+
return textHeight(outsideLabels.options.dataLabels?.style) + VALUE_LABEL_HEADROOM;
|
|
459
440
|
}
|
|
460
441
|
if (stackLabels && !axisless) {
|
|
461
|
-
|
|
462
|
-
return textHeight((_stackLabels$stackLab = stackLabels.stackLabels) === null || _stackLabels$stackLab === void 0 ? void 0 : _stackLabels$stackLab.style) + VALUE_LABEL_HEADROOM;
|
|
442
|
+
return textHeight(stackLabels.stackLabels?.style) + VALUE_LABEL_HEADROOM;
|
|
463
443
|
}
|
|
464
444
|
return 0;
|
|
465
445
|
})();
|
|
466
446
|
const marginTop = spaceTop + titleHeight + (legendAtTop ? legendHeight : 0) + (labelsOnTop ? xLabelHeight : 0) + Math.max(labelHeadroom, topOverflow);
|
|
467
447
|
const marginBottom = spaceBottom + (labelsOnTop ? 0 : xLabelHeight) + (legendAtTop ? 0 : legendHeight);
|
|
468
448
|
const measureGutter = estimate => {
|
|
469
|
-
var _axis$labels8, _axis$categories3, _axis$title;
|
|
470
449
|
const axis = yAxes[0] ?? {};
|
|
471
|
-
if (
|
|
450
|
+
if (axis.labels?.enabled === false) {
|
|
472
451
|
return estimate;
|
|
473
452
|
}
|
|
474
|
-
const labels =
|
|
453
|
+
const labels = axis.categories?.length ? [...axis.categories] : axisTicks(axis, createLinearScale({
|
|
475
454
|
domain: (() => {
|
|
476
455
|
const [min, max] = settled(yDomains[0] ?? emptyExtent(), [0, 1]);
|
|
477
456
|
return [axis.min ?? min, axis.max ?? max];
|
|
@@ -481,21 +460,18 @@ export function buildChartModel({
|
|
|
481
460
|
tickHint: axis.tickAmount ?? 5,
|
|
482
461
|
allowDecimals: axis.allowDecimals
|
|
483
462
|
}), axis.tickAmount ?? 5, undefined, 'y', numberFormat).map(tick => tick.label);
|
|
484
|
-
const widest = labels.reduce((widest_, label) =>
|
|
485
|
-
|
|
486
|
-
return Math.max(widest_, measureText(label, (_axis$labels9 = axis.labels) === null || _axis$labels9 === void 0 ? void 0 : _axis$labels9.style));
|
|
487
|
-
}, 0);
|
|
488
|
-
const title = (_axis$title = axis.title) !== null && _axis$title !== void 0 && _axis$title.text && (axis.title.rotation ?? -90) !== 0 ? textHeight(axis.title.style) + AXIS_TITLE_GAP : 0;
|
|
463
|
+
const widest = labels.reduce((widest_, label) => Math.max(widest_, measureText(label, axis.labels?.style)), 0);
|
|
464
|
+
const title = axis.title?.text && (axis.title.rotation ?? -90) !== 0 ? textHeight(axis.title.style) + AXIS_TITLE_GAP : 0;
|
|
489
465
|
return Math.ceil(widest) + AXIS_GUTTER_PADDING + title;
|
|
490
466
|
};
|
|
491
|
-
const gutter = axisless ||
|
|
492
|
-
const legendAlign =
|
|
467
|
+
const gutter = axisless || yAxes[0]?.visible === false ? 0 : measureGutter(32);
|
|
468
|
+
const legendAlign = options.legend?.align ?? 'left';
|
|
493
469
|
const legendBesideLeft = legendMetrics.beside && legendAlign === 'left';
|
|
494
470
|
// Highcharts margins replace the computed edge distance rather than adding to it.
|
|
495
|
-
const left =
|
|
496
|
-
const right =
|
|
497
|
-
const top =
|
|
498
|
-
const bottom =
|
|
471
|
+
const left = options.chart?.marginLeft ?? spaceLeft + gutter + (legendBesideLeft ? legendMetrics.width : 0);
|
|
472
|
+
const right = options.chart?.marginRight ?? spaceRight + (legendMetrics.beside && !legendBesideLeft ? legendMetrics.width : 0);
|
|
473
|
+
const top = options.chart?.marginTop ?? marginTop;
|
|
474
|
+
const bottom = options.chart?.marginBottom ?? marginBottom;
|
|
499
475
|
const plot = {
|
|
500
476
|
x: left,
|
|
501
477
|
y: top,
|
|
@@ -517,11 +493,10 @@ export function buildChartModel({
|
|
|
517
493
|
stacks
|
|
518
494
|
});
|
|
519
495
|
const yAxisModels = yAxes.reduce((built, axis, index) => {
|
|
520
|
-
var _axis$categories4;
|
|
521
496
|
const hint = axis.tickAmount ?? 5;
|
|
522
497
|
const target = axis.linkedTo === undefined ? undefined : built[axis.linkedTo];
|
|
523
|
-
const range =
|
|
524
|
-
const scale = buildYScale(axis, yDomains[index] ?? emptyExtent(), range, hint, target
|
|
498
|
+
const range = axis.categories?.length ? [plot.y, plot.y + plot.height] : [plot.y + plot.height, plot.y];
|
|
499
|
+
const scale = buildYScale(axis, yDomains[index] ?? emptyExtent(), range, hint, target?.scale);
|
|
525
500
|
built.push({
|
|
526
501
|
id: `y${index}`,
|
|
527
502
|
options: axis,
|
package/core/model/palette.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export declare const DEFAULT_AXIS_COLOR: string;
|
|
|
10
10
|
export declare const DEFAULT_TEXT_COLOR: string;
|
|
11
11
|
export declare const DEFAULT_MUTED_TEXT_COLOR: string;
|
|
12
12
|
export declare const DEFAULT_SURFACE_COLOR: string;
|
|
13
|
+
/**
|
|
14
|
+
* Text drawn on a colour the caller chose — a plot label badge, and nothing else
|
|
15
|
+
* yet. Two neutrals, not one: white on the mid greys a threshold is painted in
|
|
16
|
+
* reads at 2.8:1, far under the 4.5:1 small text needs.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_ON_FILL_TEXT_COLOR: string;
|
|
19
|
+
/** Whichever of the two neutrals reads better on a fill. */
|
|
20
|
+
export declare function textColorOn(fill: string): string;
|
|
13
21
|
export declare const DEFAULT_TOOLTIP_SURFACE_COLOR: string;
|
|
14
22
|
export declare const DEFAULT_TOOLTIP_TEXT_COLOR: string;
|
|
15
23
|
export declare const colorAt: (colors: readonly string[], index: number) => string;
|
package/core/model/palette.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Color } from '@pushwoosh/kit-constants';
|
|
2
|
+
import { parseColor } from '../scale/color';
|
|
2
3
|
/**
|
|
3
4
|
* Series palette, in the order series are drawn. These are the design system's
|
|
4
5
|
* service and brand colours rather than a set invented here, so a chart matches
|
|
@@ -11,6 +12,36 @@ export const DEFAULT_AXIS_COLOR = Color.FORM;
|
|
|
11
12
|
export const DEFAULT_TEXT_COLOR = Color.MAIN;
|
|
12
13
|
export const DEFAULT_MUTED_TEXT_COLOR = Color.LOCKED;
|
|
13
14
|
export const DEFAULT_SURFACE_COLOR = Color.CLEAR;
|
|
15
|
+
/**
|
|
16
|
+
* Text drawn on a colour the caller chose — a plot label badge, and nothing else
|
|
17
|
+
* yet. Two neutrals, not one: white on the mid greys a threshold is painted in
|
|
18
|
+
* reads at 2.8:1, far under the 4.5:1 small text needs.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_ON_FILL_TEXT_COLOR = Color.CLEAR;
|
|
21
|
+
const ON_FILL_DARK_TEXT_COLOR = Color.BRAND_BLACK;
|
|
22
|
+
const linearChannel = value => {
|
|
23
|
+
const unit = value / 255;
|
|
24
|
+
return unit <= 0.03928 ? unit / 12.92 : ((unit + 0.055) / 1.055) ** 2.4;
|
|
25
|
+
};
|
|
26
|
+
/** WCAG relative luminance, or null for a colour that cannot be read. */
|
|
27
|
+
const luminance = color => {
|
|
28
|
+
const rgba = parseColor(color);
|
|
29
|
+
if (!rgba) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return 0.2126 * linearChannel(rgba[0]) + 0.7152 * linearChannel(rgba[1]) + 0.0722 * linearChannel(rgba[2]);
|
|
33
|
+
};
|
|
34
|
+
const contrast = (one, other) => (Math.max(one, other) + 0.05) / (Math.min(one, other) + 0.05);
|
|
35
|
+
const LIGHT_TEXT_LUMINANCE = luminance(DEFAULT_ON_FILL_TEXT_COLOR) ?? 1;
|
|
36
|
+
const DARK_TEXT_LUMINANCE = luminance(ON_FILL_DARK_TEXT_COLOR) ?? 0;
|
|
37
|
+
/** Whichever of the two neutrals reads better on a fill. */
|
|
38
|
+
export function textColorOn(fill) {
|
|
39
|
+
const behind = luminance(fill);
|
|
40
|
+
if (behind === null) {
|
|
41
|
+
return DEFAULT_ON_FILL_TEXT_COLOR;
|
|
42
|
+
}
|
|
43
|
+
return contrast(behind, LIGHT_TEXT_LUMINANCE) >= contrast(behind, DARK_TEXT_LUMINANCE) ? DEFAULT_ON_FILL_TEXT_COLOR : ON_FILL_DARK_TEXT_COLOR;
|
|
44
|
+
}
|
|
14
45
|
// Chart tooltips follow the product hint style: dark bubble, light text.
|
|
15
46
|
export const DEFAULT_TOOLTIP_SURFACE_COLOR = Color.BRAND_BLACK;
|
|
16
47
|
export const DEFAULT_TOOLTIP_TEXT_COLOR = Color.CLEAR;
|
|
@@ -23,8 +23,7 @@ export const toAxisArray = axis => {
|
|
|
23
23
|
return Array.isArray(axis) ? [...axis] : [axis];
|
|
24
24
|
};
|
|
25
25
|
export function resolveSeries(options) {
|
|
26
|
-
|
|
27
|
-
const defaultType = ((_options$chart = options.chart) === null || _options$chart === void 0 ? void 0 : _options$chart.type) ?? 'line';
|
|
26
|
+
const defaultType = options.chart?.type ?? 'line';
|
|
28
27
|
const plotOptions = options.plotOptions ?? {};
|
|
29
28
|
return (options.series ?? []).map(series => {
|
|
30
29
|
const type = series.type ?? defaultType;
|
|
@@ -16,7 +16,7 @@ export function clearSeriesRegistry() {
|
|
|
16
16
|
globalRegistry.clear();
|
|
17
17
|
}
|
|
18
18
|
export function resolveRenderers(local) {
|
|
19
|
-
if (!
|
|
19
|
+
if (!local?.length) {
|
|
20
20
|
return globalRegistry;
|
|
21
21
|
}
|
|
22
22
|
const resolved = new Map(globalRegistry);
|
package/core/types/options.d.ts
CHANGED
|
@@ -113,6 +113,10 @@ export interface PlotLabelOptions {
|
|
|
113
113
|
x?: number;
|
|
114
114
|
y?: number;
|
|
115
115
|
rotation?: number;
|
|
116
|
+
/** Draws the label on a filled pill, the way a threshold states its value. */
|
|
117
|
+
backgroundColor?: string;
|
|
118
|
+
borderRadius?: number;
|
|
119
|
+
padding?: number;
|
|
116
120
|
style?: TextStyle;
|
|
117
121
|
}
|
|
118
122
|
export interface StackLabelsOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushwoosh/kit-charts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Small React chart library on SVG, with a Highcharts-shaped configuration model",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@pushwoosh/kit-constants": "^1.8.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@pushwoosh/frontend-builder-engine": "^3.
|
|
41
|
+
"@pushwoosh/frontend-builder-engine": "^3.1.0",
|
|
42
42
|
"@testing-library/jest-dom": "^6.9.1",
|
|
43
43
|
"@testing-library/react": "^16.3.0",
|
|
44
44
|
"@types/jest": "^30.0.0",
|
package/series/area/area.js
CHANGED
|
@@ -13,7 +13,6 @@ export const area = {
|
|
|
13
13
|
},
|
|
14
14
|
extendDomain: (series, acc) => includeZero(series, acc),
|
|
15
15
|
render: ctx => {
|
|
16
|
-
var _ctx$hovered, _ctx$series$marker;
|
|
17
16
|
const slots = projectSeries(ctx);
|
|
18
17
|
const projected = drawable(slots);
|
|
19
18
|
if (!projected.length) {
|
|
@@ -21,7 +20,7 @@ export const area = {
|
|
|
21
20
|
}
|
|
22
21
|
const color = ctx.color;
|
|
23
22
|
const lineWidth = ctx.series.lineWidth ?? 2;
|
|
24
|
-
const hoveredIndex =
|
|
23
|
+
const hoveredIndex = ctx.hovered?.pointIndex;
|
|
25
24
|
const hovered = hoveredIndex === undefined ? undefined : projected.find(({
|
|
26
25
|
point
|
|
27
26
|
}) => point.index === hoveredIndex);
|
|
@@ -45,7 +44,7 @@ export const area = {
|
|
|
45
44
|
}), hovered ? _jsx("circle", {
|
|
46
45
|
cx: hovered.px,
|
|
47
46
|
cy: hovered.py,
|
|
48
|
-
r:
|
|
47
|
+
r: ctx.series.marker?.states?.hover?.radius ?? 4,
|
|
49
48
|
fill: color,
|
|
50
49
|
stroke: "#FFFFFF",
|
|
51
50
|
strokeWidth: 2
|
package/series/column/column.js
CHANGED
|
@@ -42,12 +42,11 @@ function toBars(ctx) {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
const fillOf = (ctx, bar, hovered) => {
|
|
45
|
-
var _ctx$series$states;
|
|
46
45
|
const base = bar.point.color ?? ctx.color;
|
|
47
46
|
if (!hovered) {
|
|
48
47
|
return base;
|
|
49
48
|
}
|
|
50
|
-
return brighten(base,
|
|
49
|
+
return brighten(base, ctx.series.states?.hover?.brightness ?? DEFAULT_HOVER_BRIGHTNESS);
|
|
51
50
|
};
|
|
52
51
|
/**
|
|
53
52
|
* A translucent body under a solid rule is the funnel look: the series keeps its
|
|
@@ -55,12 +54,11 @@ const fillOf = (ctx, bar, hovered) => {
|
|
|
55
54
|
* bar is washed out. Hovering deepens it.
|
|
56
55
|
*/
|
|
57
56
|
const opacityOf = (ctx, hovered) => {
|
|
58
|
-
var _ctx$series$states2;
|
|
59
57
|
const base = ctx.series.fillOpacity;
|
|
60
58
|
if (!hovered) {
|
|
61
59
|
return base;
|
|
62
60
|
}
|
|
63
|
-
return
|
|
61
|
+
return ctx.series.states?.hover?.fillOpacity ?? base;
|
|
64
62
|
};
|
|
65
63
|
export const column = {
|
|
66
64
|
type: 'column',
|
|
@@ -71,12 +69,11 @@ export const column = {
|
|
|
71
69
|
exit: 'collapse',
|
|
72
70
|
extendDomain: (series, acc) => includeZero(series, acc),
|
|
73
71
|
render: ctx => {
|
|
74
|
-
var _ctx$hovered;
|
|
75
72
|
const bars = toBars(ctx);
|
|
76
73
|
if (!bars.length) {
|
|
77
74
|
return null;
|
|
78
75
|
}
|
|
79
|
-
const hoveredIndex =
|
|
76
|
+
const hoveredIndex = ctx.hovered?.pointIndex;
|
|
80
77
|
const radius = ctx.series.borderRadius;
|
|
81
78
|
const borderWidth = ctx.series.borderWidth ?? 0;
|
|
82
79
|
const capped = borderWidth > 0 && ctx.series.borderPosition === 'top';
|
|
@@ -120,7 +117,6 @@ export const column = {
|
|
|
120
117
|
});
|
|
121
118
|
},
|
|
122
119
|
hitTest: (ctx, pointer) => {
|
|
123
|
-
var _ctx$slot;
|
|
124
120
|
const bars = toBars(ctx);
|
|
125
121
|
if (!bars.length) {
|
|
126
122
|
return null;
|
|
@@ -130,7 +126,7 @@ export const column = {
|
|
|
130
126
|
if (pointer.y < top || pointer.y > bottom) {
|
|
131
127
|
return null;
|
|
132
128
|
}
|
|
133
|
-
const step =
|
|
129
|
+
const step = ctx.slot?.step ?? ctx.plot.width / 2;
|
|
134
130
|
let candidate;
|
|
135
131
|
let candidateGap = Infinity;
|
|
136
132
|
bars.forEach(bar => {
|
|
@@ -74,18 +74,16 @@ function toCells(ctx) {
|
|
|
74
74
|
}, []);
|
|
75
75
|
}
|
|
76
76
|
const fillOf = (ctx, cell, hovered) => {
|
|
77
|
-
|
|
78
|
-
const scaled = cell.point.value === null ? undefined : (_ctx$scales$color = ctx.scales.color) === null || _ctx$scales$color === void 0 ? void 0 : _ctx$scales$color.map(cell.point.value);
|
|
77
|
+
const scaled = cell.point.value === null ? undefined : ctx.scales.color?.map(cell.point.value);
|
|
79
78
|
const base = cell.point.color ?? scaled ?? ctx.color;
|
|
80
79
|
if (!hovered) {
|
|
81
80
|
return base;
|
|
82
81
|
}
|
|
83
|
-
return brighten(base,
|
|
82
|
+
return brighten(base, ctx.series.states?.hover?.brightness ?? DEFAULT_HOVER_BRIGHTNESS);
|
|
84
83
|
};
|
|
85
84
|
const labelX = (ctx, cell) => {
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const align = (_ctx$series$dataLabel2 = ctx.series.dataLabels) === null || _ctx$series$dataLabel2 === void 0 ? void 0 : _ctx$series$dataLabel2.align;
|
|
85
|
+
const padding = ctx.series.dataLabels?.padding ?? VALUE_LABEL_GAP;
|
|
86
|
+
const align = ctx.series.dataLabels?.align;
|
|
89
87
|
if (align === 'left') {
|
|
90
88
|
return cell.x + padding;
|
|
91
89
|
}
|
|
@@ -117,12 +115,11 @@ export const heatmap = {
|
|
|
117
115
|
};
|
|
118
116
|
},
|
|
119
117
|
render: ctx => {
|
|
120
|
-
var _ctx$hovered;
|
|
121
118
|
const cells = toCells(ctx);
|
|
122
119
|
if (!cells.length) {
|
|
123
120
|
return null;
|
|
124
121
|
}
|
|
125
|
-
const hoveredIndex =
|
|
122
|
+
const hoveredIndex = ctx.hovered?.pointIndex;
|
|
126
123
|
const borderWidth = ctx.series.borderWidth ?? 0;
|
|
127
124
|
const radius = ctx.series.borderRadius;
|
|
128
125
|
return _jsxs("g", {
|
package/series/line/line.js
CHANGED
|
@@ -11,14 +11,13 @@ export const line = {
|
|
|
11
11
|
},
|
|
12
12
|
extendDomain: (_series, acc) => acc,
|
|
13
13
|
render: ctx => {
|
|
14
|
-
var _ctx$hovered, _ctx$series$marker;
|
|
15
14
|
const slots = projectSeries(ctx);
|
|
16
15
|
const projected = drawable(slots);
|
|
17
16
|
if (!projected.length) {
|
|
18
17
|
return null;
|
|
19
18
|
}
|
|
20
19
|
const color = ctx.color;
|
|
21
|
-
const hoveredIndex =
|
|
20
|
+
const hoveredIndex = ctx.hovered?.pointIndex;
|
|
22
21
|
const hovered = hoveredIndex === undefined ? undefined : projected.find(({
|
|
23
22
|
point
|
|
24
23
|
}) => point.index === hoveredIndex);
|
|
@@ -37,7 +36,7 @@ export const line = {
|
|
|
37
36
|
}), hovered ? _jsx("circle", {
|
|
38
37
|
cx: hovered.px,
|
|
39
38
|
cy: hovered.py,
|
|
40
|
-
r:
|
|
39
|
+
r: ctx.series.marker?.states?.hover?.radius ?? 4,
|
|
41
40
|
fill: color,
|
|
42
41
|
stroke: "#FFFFFF",
|
|
43
42
|
strokeWidth: 2
|
package/series/pie/pie.js
CHANGED
|
@@ -95,11 +95,10 @@ function Labels({
|
|
|
95
95
|
geometry
|
|
96
96
|
}) {
|
|
97
97
|
const labels = ctx.series.dataLabels;
|
|
98
|
-
if (!
|
|
98
|
+
if (!labels?.enabled) {
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
101
|
return slices.filter(slice => !slice.hidden).map(slice => {
|
|
102
|
-
var _labels$style, _labels$style2, _labels$style3, _labels$style4;
|
|
103
102
|
const mid = (slice.from + slice.to) / 2;
|
|
104
103
|
const at = polar(geometry.cx, geometry.cy, geometry.outer + LABEL_OFFSET, mid);
|
|
105
104
|
const text = labels.formatter ? labels.formatter({
|
|
@@ -116,10 +115,10 @@ function Labels({
|
|
|
116
115
|
y: at.y,
|
|
117
116
|
textAnchor: Math.cos(mid) < 0 ? 'end' : 'start',
|
|
118
117
|
dominantBaseline: "middle",
|
|
119
|
-
fill: labels.color ??
|
|
120
|
-
fontFamily:
|
|
121
|
-
fontSize:
|
|
122
|
-
fontWeight:
|
|
118
|
+
fill: labels.color ?? labels.style?.color,
|
|
119
|
+
fontFamily: labels.style?.fontFamily,
|
|
120
|
+
fontSize: labels.style?.fontSize ?? '11px',
|
|
121
|
+
fontWeight: labels.style?.fontWeight,
|
|
123
122
|
children: text
|
|
124
123
|
}, `label-${slice.point.index}`);
|
|
125
124
|
});
|
|
@@ -134,7 +133,6 @@ function Labels({
|
|
|
134
133
|
function Slices({
|
|
135
134
|
ctx
|
|
136
135
|
}) {
|
|
137
|
-
var _ctx$hovered;
|
|
138
136
|
const slices = toSlices(ctx);
|
|
139
137
|
const alive = ctx.points.some(point => !ctx.hiddenPoints.has(point.index));
|
|
140
138
|
const targets = [...slices.flatMap(slice => [slice.from, slice.to]), ctx.hidden || !alive ? 0 : 1];
|
|
@@ -144,7 +142,7 @@ function Slices({
|
|
|
144
142
|
}
|
|
145
143
|
const radiusScale = tweened[tweened.length - 1] ?? 1;
|
|
146
144
|
const geometry = geometryOf(ctx, radiusScale);
|
|
147
|
-
const hovered =
|
|
145
|
+
const hovered = ctx.hovered?.pointIndex;
|
|
148
146
|
const drawn = slices.map((slice, index) => ({
|
|
149
147
|
...slice,
|
|
150
148
|
from: tweened[index * 2] ?? slice.from,
|
|
@@ -13,11 +13,11 @@ export const labelAnchor = align => {
|
|
|
13
13
|
return 'middle';
|
|
14
14
|
};
|
|
15
15
|
const textProps = style => ({
|
|
16
|
-
fontFamily: style
|
|
17
|
-
fontSize: style
|
|
18
|
-
fontWeight: style
|
|
19
|
-
fontStyle: style
|
|
20
|
-
paintOrder:
|
|
16
|
+
fontFamily: style?.fontFamily,
|
|
17
|
+
fontSize: style?.fontSize,
|
|
18
|
+
fontWeight: style?.fontWeight,
|
|
19
|
+
fontStyle: style?.fontStyle,
|
|
20
|
+
paintOrder: style?.textOutline === 'none' ? undefined : 'stroke'
|
|
21
21
|
});
|
|
22
22
|
/** No formatter and no format string: the chart's own number rule prints it. */
|
|
23
23
|
const defaultLabel = (ctx, format, y) => {
|
|
@@ -44,7 +44,7 @@ export function DataLabels({
|
|
|
44
44
|
positionOf
|
|
45
45
|
}) {
|
|
46
46
|
const options = ctx.series.dataLabels;
|
|
47
|
-
if (!
|
|
47
|
+
if (!options?.enabled) {
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
const turbo = ctx.series.turboThreshold;
|
|
@@ -110,12 +110,12 @@ export function DataLabels({
|
|
|
110
110
|
x: at.x,
|
|
111
111
|
y: y,
|
|
112
112
|
textAnchor: anchor,
|
|
113
|
-
fill: options.color ??
|
|
113
|
+
fill: options.color ?? style?.color ?? DEFAULT_TEXT_COLOR,
|
|
114
114
|
...textProps(style),
|
|
115
115
|
children: lines && lines.length > 1 ? lines.map((text, index) => _jsx("tspan", {
|
|
116
116
|
x: at.x,
|
|
117
117
|
dy: index === 0 ? 0 : textHeight(secondary ?? style),
|
|
118
|
-
fill: index === 0 ? undefined : secondary
|
|
118
|
+
fill: index === 0 ? undefined : secondary?.color,
|
|
119
119
|
...(index === 0 ? {} : textProps(secondary)),
|
|
120
120
|
children: text
|
|
121
121
|
}, `line-${index}`)) : content
|