@pushwoosh/kit-charts 0.0.4 → 0.0.5
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/Chart.js +30 -14
- package/core/Chart/Chart.styles.d.ts +2 -0
- package/core/Chart/Chart.styles.js +3 -2
- package/core/format/template.d.ts +10 -0
- package/core/format/template.js +30 -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 +8 -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,13 @@ 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` and `pointFormatter` are taken under their Highcharts names, so a
|
|
343
|
+
config that already carries them needs no rewrite: `headerFormat: ''` drops the
|
|
344
|
+
header, a template fills `{point.key}`, `{point.x}`, `{point.y}` and
|
|
345
|
+
`{series.name}` (markup in it is dropped — the chrome is the library's), and
|
|
346
|
+
`pointFormatter` is handed the point and returns a `ReactNode`. `formatter` and
|
|
347
|
+
`table` both outrank them. Golden case: `area/header-format`.
|
|
348
|
+
|
|
342
349
|
`ctx.points[].pointMeta` is the hovered point's own `meta` — the third type
|
|
343
350
|
parameter of `ChartOptions<TChartMeta, TSeriesMeta, TPointMeta>`, the same object
|
|
344
351
|
`onPointClick` reports. `meta` beside it stays the series'. A bar whose height is a
|
|
@@ -367,10 +374,22 @@ With `fillOpacity` on the body and `states.hover.fillOpacity` to deepen it, that
|
|
|
367
374
|
is the funnel look: the series keeps its real colour, so the legend swatch and
|
|
368
375
|
the tooltip dot stay readable, and only the bar is washed out.
|
|
369
376
|
|
|
377
|
+
`states.hover.color` swaps the fill on hover instead of lightening it, for a body
|
|
378
|
+
too pale for `brightness` to show on; it also wins over a per-point colour, since
|
|
379
|
+
one colour on hover is the point of asking. `column` and `heatmap` read it.
|
|
380
|
+
Golden case: `column/hover-color`.
|
|
381
|
+
|
|
370
382
|
A data label whose text carries `\n` is drawn as stacked lines, styled by
|
|
371
383
|
`style` for the first and `secondaryStyle` for the rest. It is one label: it
|
|
372
384
|
moves, measures and hides as one.
|
|
373
385
|
|
|
386
|
+
## Series opacity
|
|
387
|
+
|
|
388
|
+
`series.opacity` fades a whole series layer — body, border and data labels — where
|
|
389
|
+
`fillOpacity` reaches only the fill a renderer paints. A hover's
|
|
390
|
+
`states.inactive.opacity` overrides it while it lasts. Golden case:
|
|
391
|
+
`column/series-opacity`.
|
|
392
|
+
|
|
374
393
|
## Areas
|
|
375
394
|
|
|
376
395
|
`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,32 @@ const toCssSize = (value, fallback) => {
|
|
|
32
33
|
}
|
|
33
34
|
return typeof value === 'number' ? `${value}px` : value;
|
|
34
35
|
};
|
|
35
|
-
function
|
|
36
|
+
function tooltipHeader(ctx, headerFormat, formatX) {
|
|
37
|
+
if (headerFormat === undefined) {
|
|
38
|
+
return formatX(ctx.x);
|
|
39
|
+
}
|
|
40
|
+
return applyTooltipTemplate(headerFormat, {
|
|
41
|
+
x: ctx.x,
|
|
42
|
+
y: ctx.points[0]?.y ?? null,
|
|
43
|
+
seriesName: ctx.points[0]?.seriesName ?? ''
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function defaultTooltip(ctx, tooltip, formatX, formatValue) {
|
|
47
|
+
const header = tooltipHeader(ctx, tooltip?.headerFormat, formatX);
|
|
48
|
+
const pointFormatter = tooltip?.pointFormatter;
|
|
36
49
|
return _jsxs("div", {
|
|
37
|
-
children: [_jsx(TooltipTitle, {
|
|
38
|
-
children:
|
|
39
|
-
}), ctx.points.map(point =>
|
|
40
|
-
children:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
children: [header ? _jsx(TooltipTitle, {
|
|
51
|
+
children: header
|
|
52
|
+
}) : null, ctx.points.map(point => _jsx(TooltipRow, {
|
|
53
|
+
children: pointFormatter ? pointFormatter(point) : _jsxs(_Fragment, {
|
|
54
|
+
children: [_jsx(TooltipSwatch, {
|
|
55
|
+
"$color": point.color
|
|
56
|
+
}), _jsx(TooltipName, {
|
|
57
|
+
children: point.seriesName
|
|
58
|
+
}), _jsx(TooltipValue, {
|
|
59
|
+
children: formatValue(point.value ?? point.y)
|
|
60
|
+
})]
|
|
61
|
+
})
|
|
47
62
|
}, `${point.seriesIndex}-${point.index}`))]
|
|
48
63
|
});
|
|
49
64
|
}
|
|
@@ -435,8 +450,8 @@ function ChartBody({
|
|
|
435
450
|
return formatter(ctx);
|
|
436
451
|
}
|
|
437
452
|
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
|
|
453
|
+
return table ? tableTooltip(ctx, table, formatX) : defaultTooltip(ctx, options.tooltip, formatX, formatValue);
|
|
454
|
+
}, [hover, model, options.title?.text, options.meta, options.tooltip, formatX, formatValue]);
|
|
440
455
|
const hasData = model?.series.some(series => series.points.some(point => point.y !== null)) ?? false;
|
|
441
456
|
const plotBorderWidth = options.chart?.plotBorderWidth ?? 0;
|
|
442
457
|
const scrolls = canvas.width > size.width || canvas.height > size.height;
|
|
@@ -537,6 +552,7 @@ function ChartBody({
|
|
|
537
552
|
"$collapse": series.renderer.exit === 'collapse',
|
|
538
553
|
"$selfAnimated": series.renderer.animatesGeometry === true,
|
|
539
554
|
"$inactive": inactiveOpacity(series),
|
|
555
|
+
"$opacity": series.options.opacity,
|
|
540
556
|
"$motionMs": motionOf(series),
|
|
541
557
|
"$cursor": series.options.cursor ?? 'default',
|
|
542
558
|
"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,10 @@
|
|
|
1
|
+
export interface TemplateContext {
|
|
2
|
+
x: number | string;
|
|
3
|
+
y: number | null;
|
|
4
|
+
seriesName: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Fills a Highcharts header template. Markup in it is dropped rather than rendered:
|
|
8
|
+
* the chrome around a tooltip is the library's, and a `<span>` in a config is styling.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyTooltipTemplate(format: string, ctx: TemplateContext): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatTime } from './time';
|
|
2
|
+
import { fillTokens } from './tokens';
|
|
3
|
+
const substitute = (name, ctx) => {
|
|
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 ? '' : String(ctx.y);
|
|
10
|
+
case 'series.name':
|
|
11
|
+
return ctx.seriesName;
|
|
12
|
+
default:
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Fills a Highcharts header template. Markup in it is dropped rather than rendered:
|
|
18
|
+
* the chrome around a tooltip is the library's, and a `<span>` in a config is styling.
|
|
19
|
+
*/
|
|
20
|
+
export function applyTooltipTemplate(format, ctx) {
|
|
21
|
+
const stripped = format.replace(/<br\s*\/?>/gi, ' ').replace(/<[^>]*>/g, '');
|
|
22
|
+
return fillTokens(stripped, (name, pattern) => {
|
|
23
|
+
const value = substitute(name, ctx);
|
|
24
|
+
if (value === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const dates = name === 'point.key' || name === 'point.x';
|
|
28
|
+
return pattern && dates && typeof ctx.x === 'number' ? formatTime(pattern, ctx.x) : value;
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
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,3 @@
|
|
|
1
|
+
export type TokenResolver = (name: string, pattern: string | undefined) => string | undefined;
|
|
2
|
+
/** Fills `{name}` and `{name:pattern}` placeholders; an unresolved one is left as it stands. */
|
|
3
|
+
export declare function fillTokens(format: string, resolve: TokenResolver): string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const TOKEN = /\{([a-z][a-z.]*)(?::([^}]*))?\}/gi;
|
|
2
|
+
/** Fills `{name}` and `{name:pattern}` placeholders; an unresolved one is left as it stands. */
|
|
3
|
+
export function fillTokens(format, resolve) {
|
|
4
|
+
return format.replace(TOKEN, (match, name, pattern) => resolve(name, pattern) ?? match);
|
|
5
|
+
}
|
package/core/types/options.d.ts
CHANGED
|
@@ -205,6 +205,10 @@ 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
|
+
/** One row per point, in place of the default swatch, name and value. */
|
|
211
|
+
pointFormatter?: TooltipCellFormatter<TSeriesMeta, TPointMeta>;
|
|
208
212
|
dateFormat?: string;
|
|
209
213
|
valuePrefix?: string;
|
|
210
214
|
valueSuffix?: string;
|
|
@@ -255,6 +259,8 @@ export interface SeriesStatesOptions {
|
|
|
255
259
|
brightness?: number;
|
|
256
260
|
/** Deepens a translucent body on hover, where brightening a pale fill does nothing. */
|
|
257
261
|
fillOpacity?: number;
|
|
262
|
+
/** The colour to swap to, for a hover that is not a brighter shade of the same one. */
|
|
263
|
+
color?: string;
|
|
258
264
|
};
|
|
259
265
|
inactive?: {
|
|
260
266
|
opacity?: number;
|
|
@@ -300,6 +306,8 @@ export interface SeriesOptionsBase<TSeriesMeta = unknown, TPointMeta = unknown>
|
|
|
300
306
|
fillOpacity?: number;
|
|
301
307
|
/** The body's own colour, where the fill is not a wash of the series colour. */
|
|
302
308
|
fillColor?: string;
|
|
309
|
+
/** Fades the whole series — body, border and labels — unlike `fillOpacity`. */
|
|
310
|
+
opacity?: number;
|
|
303
311
|
pointPadding?: number;
|
|
304
312
|
groupPadding?: number;
|
|
305
313
|
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
|
}
|