@pushwoosh/kit-charts 0.0.4 → 0.0.6
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 +22 -0
- package/core/Chart/Chart.js +44 -13
- package/core/Chart/Chart.styles.d.ts +2 -0
- package/core/Chart/Chart.styles.js +3 -2
- package/core/format/template.d.ts +12 -0
- package/core/format/template.js +32 -0
- package/core/format/time.js +5 -4
- package/core/format/tokens.d.ts +3 -0
- package/core/format/tokens.js +5 -0
- package/core/types/options.d.ts +10 -0
- package/package.json +1 -1
- package/series/column/column.js +2 -3
- package/series/heatmap/heatmap.js +2 -3
- package/series/shared/color.d.ts +6 -0
- package/series/shared/color.js +9 -0
package/ai-documentation.md
CHANGED
|
@@ -339,6 +339,16 @@ the value wrapped in `valuePrefix`/`valueSuffix`. `formatter` replaces the body
|
|
|
339
339
|
returns a `ReactNode`; `ctx.points[].prev`/`.next` carry the neighbouring points, so
|
|
340
340
|
a delta needs no extra lookup. `shared: true` collects every series at the hovered x.
|
|
341
341
|
|
|
342
|
+
`headerFormat`, `pointFormat` and `pointFormatter` are taken under their Highcharts
|
|
343
|
+
names, so a config that already carries them needs no rewrite: `headerFormat: ''`
|
|
344
|
+
drops the header and `pointFormat` writes the row after the swatch. Both templates
|
|
345
|
+
fill `{point.key}`, `{point.x}`, `{point.y}`, `{point.value}` and `{series.name}`,
|
|
346
|
+
date an x through `{point.x:%b %d}` (a padded pattern is trimmed) and pass numbers
|
|
347
|
+
through the chart's `numberFormat`; markup in them is dropped, because the chrome
|
|
348
|
+
around a tooltip is the library's. `pointFormatter` is handed the point and returns
|
|
349
|
+
a `ReactNode`, and outranks `pointFormat`; `formatter` and `table` outrank all
|
|
350
|
+
three. Golden cases: `area/header-format`, `line/point-format-template`.
|
|
351
|
+
|
|
342
352
|
`ctx.points[].pointMeta` is the hovered point's own `meta` — the third type
|
|
343
353
|
parameter of `ChartOptions<TChartMeta, TSeriesMeta, TPointMeta>`, the same object
|
|
344
354
|
`onPointClick` reports. `meta` beside it stays the series'. A bar whose height is a
|
|
@@ -367,10 +377,22 @@ With `fillOpacity` on the body and `states.hover.fillOpacity` to deepen it, that
|
|
|
367
377
|
is the funnel look: the series keeps its real colour, so the legend swatch and
|
|
368
378
|
the tooltip dot stay readable, and only the bar is washed out.
|
|
369
379
|
|
|
380
|
+
`states.hover.color` swaps the fill on hover instead of lightening it, for a body
|
|
381
|
+
too pale for `brightness` to show on; it also wins over a per-point colour, since
|
|
382
|
+
one colour on hover is the point of asking. `column` and `heatmap` read it.
|
|
383
|
+
Golden case: `column/hover-color`.
|
|
384
|
+
|
|
370
385
|
A data label whose text carries `\n` is drawn as stacked lines, styled by
|
|
371
386
|
`style` for the first and `secondaryStyle` for the rest. It is one label: it
|
|
372
387
|
moves, measures and hides as one.
|
|
373
388
|
|
|
389
|
+
## Series opacity
|
|
390
|
+
|
|
391
|
+
`series.opacity` fades a whole series layer — body, border and data labels — where
|
|
392
|
+
`fillOpacity` reaches only the fill a renderer paints. A hover's
|
|
393
|
+
`states.inactive.opacity` overrides it while it lasts. Golden case:
|
|
394
|
+
`column/series-opacity`.
|
|
395
|
+
|
|
374
396
|
## Areas
|
|
375
397
|
|
|
376
398
|
`fillColor` paints the body under the line, where the fill is a token of its own
|
package/core/Chart/Chart.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Grid, XAxis, YAxis } from './Axes';
|
|
4
4
|
import { Canvas, Centred, Container, PinnedAxes, ResetZoom, ScrollBox, SeriesLayer, TooltipCell, TooltipDivider, TooltipHeadCell, TooltipName, TooltipRow, TooltipSeriesCell, TooltipSeriesInner, TooltipSwatch, TooltipTable, TooltipTitle, TooltipValue, ZoomBand } from './Chart.styles';
|
|
@@ -10,6 +10,7 @@ import { Titles } from './Titles';
|
|
|
10
10
|
import { Tooltip } from './Tooltip';
|
|
11
11
|
import { useSize } from './useSize';
|
|
12
12
|
import { MOTION_MS } from '../animation/motion';
|
|
13
|
+
import { applyTooltipTemplate } from '../format/template';
|
|
13
14
|
import { formatTime } from '../format/time';
|
|
14
15
|
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from '../layout/measure-text';
|
|
15
16
|
import { buildChartModel, hiddenKey, isContinuous } from '../model/build-model';
|
|
@@ -32,18 +33,47 @@ const toCssSize = (value, fallback) => {
|
|
|
32
33
|
}
|
|
33
34
|
return typeof value === 'number' ? `${value}px` : value;
|
|
34
35
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const templateContext = (point, x) => ({
|
|
37
|
+
x,
|
|
38
|
+
y: point?.y ?? null,
|
|
39
|
+
value: point?.value ?? null,
|
|
40
|
+
seriesName: point?.seriesName ?? ''
|
|
41
|
+
});
|
|
42
|
+
function tooltipHeader(ctx, headerFormat, formatX, numberFormat) {
|
|
43
|
+
if (headerFormat === undefined) {
|
|
44
|
+
return formatX(ctx.x);
|
|
45
|
+
}
|
|
46
|
+
return applyTooltipTemplate(headerFormat, templateContext(ctx.points[0], ctx.x), numberFormat);
|
|
47
|
+
}
|
|
48
|
+
function pointRow(point, tooltip, formatValue, numberFormat) {
|
|
49
|
+
const pointFormatter = tooltip?.pointFormatter;
|
|
50
|
+
if (pointFormatter) {
|
|
51
|
+
return pointFormatter(point);
|
|
52
|
+
}
|
|
53
|
+
if (tooltip?.pointFormat !== undefined) {
|
|
54
|
+
return _jsxs(_Fragment, {
|
|
40
55
|
children: [_jsx(TooltipSwatch, {
|
|
41
56
|
"$color": point.color
|
|
42
|
-
}),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
}), applyTooltipTemplate(tooltip.pointFormat, templateContext(point, point.x), numberFormat)]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return _jsxs(_Fragment, {
|
|
61
|
+
children: [_jsx(TooltipSwatch, {
|
|
62
|
+
"$color": point.color
|
|
63
|
+
}), _jsx(TooltipName, {
|
|
64
|
+
children: point.seriesName
|
|
65
|
+
}), _jsx(TooltipValue, {
|
|
66
|
+
children: formatValue(point.value ?? point.y)
|
|
67
|
+
})]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function defaultTooltip(ctx, tooltip, formatX, formatValue, numberFormat) {
|
|
71
|
+
const header = tooltipHeader(ctx, tooltip?.headerFormat, formatX, numberFormat);
|
|
72
|
+
return _jsxs("div", {
|
|
73
|
+
children: [header ? _jsx(TooltipTitle, {
|
|
74
|
+
children: header
|
|
75
|
+
}) : null, ctx.points.map(point => _jsx(TooltipRow, {
|
|
76
|
+
children: pointRow(point, tooltip, formatValue, numberFormat)
|
|
47
77
|
}, `${point.seriesIndex}-${point.index}`))]
|
|
48
78
|
});
|
|
49
79
|
}
|
|
@@ -435,8 +465,8 @@ function ChartBody({
|
|
|
435
465
|
return formatter(ctx);
|
|
436
466
|
}
|
|
437
467
|
const table = options.tooltip?.table;
|
|
438
|
-
return table ? tableTooltip(ctx, table, formatX) : defaultTooltip(ctx, formatX, formatValue);
|
|
439
|
-
}, [hover, model, options.title?.text, options.meta, options.tooltip
|
|
468
|
+
return table ? tableTooltip(ctx, table, formatX) : defaultTooltip(ctx, options.tooltip, formatX, formatValue, model.numberFormat);
|
|
469
|
+
}, [hover, model, options.title?.text, options.meta, options.tooltip, formatX, formatValue]);
|
|
440
470
|
const hasData = model?.series.some(series => series.points.some(point => point.y !== null)) ?? false;
|
|
441
471
|
const plotBorderWidth = options.chart?.plotBorderWidth ?? 0;
|
|
442
472
|
const scrolls = canvas.width > size.width || canvas.height > size.height;
|
|
@@ -537,6 +567,7 @@ function ChartBody({
|
|
|
537
567
|
"$collapse": series.renderer.exit === 'collapse',
|
|
538
568
|
"$selfAnimated": series.renderer.animatesGeometry === true,
|
|
539
569
|
"$inactive": inactiveOpacity(series),
|
|
570
|
+
"$opacity": series.options.opacity,
|
|
540
571
|
"$motionMs": motionOf(series),
|
|
541
572
|
"$cursor": series.options.cursor ?? 'default',
|
|
542
573
|
"aria-hidden": series.hidden,
|
|
@@ -33,6 +33,8 @@ export declare const SeriesLayer: import("styled-components").StyledComponent<"g
|
|
|
33
33
|
$selfAnimated: boolean;
|
|
34
34
|
/** Opacity while another series is hovered, from `states.inactive`. */
|
|
35
35
|
$inactive?: number;
|
|
36
|
+
/** `series.opacity`: the whole series faded, border and labels included. */
|
|
37
|
+
$opacity?: number;
|
|
36
38
|
/** `series.animation`; the chart clock unless this series asked for its own. */
|
|
37
39
|
$motionMs: number;
|
|
38
40
|
$cursor: string;
|
|
@@ -56,12 +56,13 @@ export const SeriesLayer = styled.g.withConfig({
|
|
|
56
56
|
componentId: "sc-1pit1b9-4"
|
|
57
57
|
})(["opacity:", ";pointer-events:", ";cursor:", ";transition:opacity ", "ms ", ";", " @media (prefers-reduced-motion:reduce){transition:none;path,rect,circle,text{transition:none;}}"], ({
|
|
58
58
|
$hidden,
|
|
59
|
-
$inactive
|
|
59
|
+
$inactive,
|
|
60
|
+
$opacity
|
|
60
61
|
}) => {
|
|
61
62
|
if ($hidden) {
|
|
62
63
|
return 0;
|
|
63
64
|
}
|
|
64
|
-
return $inactive ?? 1;
|
|
65
|
+
return $inactive ?? $opacity ?? 1;
|
|
65
66
|
}, ({
|
|
66
67
|
$hidden
|
|
67
68
|
}) => $hidden ? 'none' : 'auto', ({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type NumberFormatter } from '../types/renderer';
|
|
2
|
+
export interface TemplateContext {
|
|
3
|
+
x: number | string;
|
|
4
|
+
y: number | null;
|
|
5
|
+
value: number | null;
|
|
6
|
+
seriesName: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Fills a Highcharts header or point template. Markup in it is dropped rather than
|
|
10
|
+
* rendered: the chrome around a tooltip is the library's, and a `<span>` is styling.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyTooltipTemplate(format: string, ctx: TemplateContext, numberFormat?: NumberFormatter): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { formatTime } from './time';
|
|
2
|
+
import { fillTokens } from './tokens';
|
|
3
|
+
const substitute = (name, ctx, numberFormat) => {
|
|
4
|
+
switch (name) {
|
|
5
|
+
case 'point.key':
|
|
6
|
+
case 'point.x':
|
|
7
|
+
return String(ctx.x);
|
|
8
|
+
case 'point.y':
|
|
9
|
+
return ctx.y === null ? '' : numberFormat(ctx.y);
|
|
10
|
+
case 'point.value':
|
|
11
|
+
return ctx.value === null ? '' : numberFormat(ctx.value);
|
|
12
|
+
case 'series.name':
|
|
13
|
+
return ctx.seriesName;
|
|
14
|
+
default:
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Fills a Highcharts header or point template. Markup in it is dropped rather than
|
|
20
|
+
* rendered: the chrome around a tooltip is the library's, and a `<span>` is styling.
|
|
21
|
+
*/
|
|
22
|
+
export function applyTooltipTemplate(format, ctx, numberFormat = String) {
|
|
23
|
+
const stripped = format.replace(/<br\s*\/?>/gi, ' ').replace(/<[^>]*>/g, '');
|
|
24
|
+
return fillTokens(stripped, (name, pattern) => {
|
|
25
|
+
const value = substitute(name, ctx, numberFormat);
|
|
26
|
+
if (value === undefined) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const dates = name === 'point.key' || name === 'point.x';
|
|
30
|
+
return pattern && dates && typeof ctx.x === 'number' ? formatTime(pattern, ctx.x) : value;
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
package/core/format/time.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fillTokens } from './tokens';
|
|
1
2
|
const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
2
3
|
const MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
3
4
|
const DAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
@@ -29,10 +30,10 @@ export function formatTime(pattern, timestamp) {
|
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
export function applyValueFormat(format, value) {
|
|
32
|
-
return format
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
33
|
+
return fillTokens(format, (name, pattern) => {
|
|
34
|
+
if (name !== 'value') {
|
|
35
|
+
return undefined;
|
|
35
36
|
}
|
|
36
|
-
return String(value);
|
|
37
|
+
return pattern && typeof value === 'number' ? formatTime(pattern, value) : String(value);
|
|
37
38
|
});
|
|
38
39
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const TOKEN = /\{([a-z][a-z.]*)(?::([^}]*))?\}/gi;
|
|
2
|
+
/** Fills `{name}` and `{name:pattern}`; an unresolved token is left as it stands. */
|
|
3
|
+
export function fillTokens(format, resolve) {
|
|
4
|
+
return format.replace(TOKEN, (match, name, pattern) => resolve(name, pattern?.trim() || undefined) ?? match);
|
|
5
|
+
}
|
package/core/types/options.d.ts
CHANGED
|
@@ -205,6 +205,12 @@ export interface TooltipOptions<TChartMeta = unknown, TSeriesMeta = unknown, TPo
|
|
|
205
205
|
shape?: 'callout' | 'box';
|
|
206
206
|
followPointer?: boolean;
|
|
207
207
|
formatter?: TooltipFormatter<TChartMeta, TSeriesMeta, TPointMeta>;
|
|
208
|
+
/** Highcharts' header template: `''` drops the header, `{point.key}` and friends fill. */
|
|
209
|
+
headerFormat?: string;
|
|
210
|
+
/** Highcharts' per-point template, drawn after the swatch; `pointFormatter` wins. */
|
|
211
|
+
pointFormat?: string;
|
|
212
|
+
/** One row per point, in place of the default swatch, name and value. */
|
|
213
|
+
pointFormatter?: TooltipCellFormatter<TSeriesMeta, TPointMeta>;
|
|
208
214
|
dateFormat?: string;
|
|
209
215
|
valuePrefix?: string;
|
|
210
216
|
valueSuffix?: string;
|
|
@@ -255,6 +261,8 @@ export interface SeriesStatesOptions {
|
|
|
255
261
|
brightness?: number;
|
|
256
262
|
/** Deepens a translucent body on hover, where brightening a pale fill does nothing. */
|
|
257
263
|
fillOpacity?: number;
|
|
264
|
+
/** The colour to swap to, for a hover that is not a brighter shade of the same one. */
|
|
265
|
+
color?: string;
|
|
258
266
|
};
|
|
259
267
|
inactive?: {
|
|
260
268
|
opacity?: number;
|
|
@@ -300,6 +308,8 @@ export interface SeriesOptionsBase<TSeriesMeta = unknown, TPointMeta = unknown>
|
|
|
300
308
|
fillOpacity?: number;
|
|
301
309
|
/** The body's own colour, where the fill is not a wash of the series colour. */
|
|
302
310
|
fillColor?: string;
|
|
311
|
+
/** Fades the whole series — body, border and labels — unlike `fillOpacity`. */
|
|
312
|
+
opacity?: number;
|
|
303
313
|
pointPadding?: number;
|
|
304
314
|
groupPadding?: number;
|
|
305
315
|
minPointLength?: number;
|
package/package.json
CHANGED
package/series/column/column.js
CHANGED
|
@@ -2,11 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { pointKey } from '../../core/data/points';
|
|
3
3
|
import { drawable, projectSeries } from '../../core/geometry/path';
|
|
4
4
|
import { TEXT_BASELINE_SHIFT, VALUE_LABEL_GAP } from '../../core/layout/metrics';
|
|
5
|
-
import {
|
|
5
|
+
import { resolveHoverFill } from '../shared/color';
|
|
6
6
|
import { DataLabels } from '../shared/DataLabels';
|
|
7
7
|
import { includeZero } from '../shared/domain';
|
|
8
8
|
import { seriesLegend } from '../shared/legend';
|
|
9
|
-
const DEFAULT_HOVER_BRIGHTNESS = 0.1;
|
|
10
9
|
const SLOT_FALLBACK = {
|
|
11
10
|
width: 10,
|
|
12
11
|
offset: 0
|
|
@@ -46,7 +45,7 @@ const fillOf = (ctx, bar, hovered) => {
|
|
|
46
45
|
if (!hovered) {
|
|
47
46
|
return base;
|
|
48
47
|
}
|
|
49
|
-
return
|
|
48
|
+
return resolveHoverFill(base, ctx.series.states);
|
|
50
49
|
};
|
|
51
50
|
/**
|
|
52
51
|
* A translucent body under a solid rule is the funnel look: the series keeps its
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { pointKey } from '../../core/data/points';
|
|
3
3
|
import { TEXT_BASELINE_SHIFT, VALUE_LABEL_GAP } from '../../core/layout/metrics';
|
|
4
|
-
import {
|
|
4
|
+
import { resolveHoverFill } from '../shared/color';
|
|
5
5
|
import { DataLabels } from '../shared/DataLabels';
|
|
6
|
-
const DEFAULT_HOVER_BRIGHTNESS = 0.1;
|
|
7
6
|
/** Width of one column: a unit step on the x axis, or the whole plot if there is none. */
|
|
8
7
|
function columnWidth(ctx) {
|
|
9
8
|
const scale = ctx.scales.x;
|
|
@@ -79,7 +78,7 @@ const fillOf = (ctx, cell, hovered) => {
|
|
|
79
78
|
if (!hovered) {
|
|
80
79
|
return base;
|
|
81
80
|
}
|
|
82
|
-
return
|
|
81
|
+
return resolveHoverFill(base, ctx.series.states);
|
|
83
82
|
};
|
|
84
83
|
const labelX = (ctx, cell) => {
|
|
85
84
|
const padding = ctx.series.dataLabels?.padding ?? VALUE_LABEL_GAP;
|
package/series/shared/color.d.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
+
import { type SeriesStatesOptions } from '../../core/types/options';
|
|
1
2
|
export declare function brighten(color: string, amount: number): string;
|
|
3
|
+
/**
|
|
4
|
+
* An explicit hover colour wins over brightening, and over a per-point colour:
|
|
5
|
+
* asking for one colour on hover means one colour, not one per point.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveHoverFill(base: string, states: SeriesStatesOptions | undefined): string;
|
package/series/shared/color.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const DEFAULT_HOVER_BRIGHTNESS = 0.1;
|
|
1
2
|
const clamp = value => Math.min(255, Math.max(0, Math.round(value)));
|
|
2
3
|
const parseHex = color => {
|
|
3
4
|
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
|
|
@@ -16,4 +17,12 @@ export function brighten(color, amount) {
|
|
|
16
17
|
const ratio = Math.abs(amount);
|
|
17
18
|
const [r, g, b] = rgb.map(channel => clamp(channel + (target - channel) * ratio));
|
|
18
19
|
return `#${[r, g, b].map(channel => channel.toString(16).padStart(2, '0')).join('')}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An explicit hover colour wins over brightening, and over a per-point colour:
|
|
23
|
+
* asking for one colour on hover means one colour, not one per point.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveHoverFill(base, states) {
|
|
26
|
+
const hover = states?.hover;
|
|
27
|
+
return hover?.color ?? brighten(base, hover?.brightness ?? DEFAULT_HOVER_BRIGHTNESS);
|
|
19
28
|
}
|