@pushwoosh/kit-charts 0.0.1 → 0.0.2
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/PlotDecorations.js +59 -32
- package/core/layout/measure-text.d.ts +5 -0
- package/core/layout/measure-text.js +5 -0
- package/core/layout/metrics.d.ts +7 -0
- package/core/layout/metrics.js +7 -0
- package/core/model/palette.d.ts +8 -0
- package/core/model/palette.js +31 -0
- package/core/types/options.d.ts +4 -0
- package/package.json +1 -1
package/ai-documentation.md
CHANGED
|
@@ -103,6 +103,25 @@ takes `value`; on a category axis both address a category by name and a band
|
|
|
103
103
|
covers the whole slot. Both accept a `label`, and a `zIndex` of 5 or more draws
|
|
104
104
|
them over the series instead of under it — the value Highcharts users reach for.
|
|
105
105
|
|
|
106
|
+
A label's `align` is horizontal and its `verticalAlign` vertical whichever way
|
|
107
|
+
the decoration runs, as in Highcharts: a line on a y axis takes its label to the
|
|
108
|
+
left edge of the plot (`align: 'left'`, the default there) and sits it on the
|
|
109
|
+
rule (`verticalAlign: 'middle'`). `x`/`y` shift it from there — a negative `x`
|
|
110
|
+
hangs it in the axis gutter.
|
|
111
|
+
|
|
112
|
+
`label.backgroundColor` draws the label on a pill instead of leaving it as loose
|
|
113
|
+
text, with `borderRadius` and `padding` to tune it. The text takes whichever of
|
|
114
|
+
white and `BRAND_BLACK` reads better on that fill — the greys a threshold is
|
|
115
|
+
painted in carry white at under 3:1 — and `style.color` overrides the choice.
|
|
116
|
+
That is what a threshold needs: a number sitting on a rule has no
|
|
117
|
+
ground of its own to read against. Highcharts has no such option — it takes
|
|
118
|
+
`useHTML` and a styled span, which this library has no equivalent of — so this is
|
|
119
|
+
an addition rather than a divergence: a Highcharts config still renders.
|
|
120
|
+
|
|
121
|
+
Whether a threshold is breached is the application's decision, not the chart's:
|
|
122
|
+
it passes the colour. Golden cases `area/threshold` and `area/threshold-breached`
|
|
123
|
+
are the same options with one colour changed.
|
|
124
|
+
|
|
106
125
|
## Interaction
|
|
107
126
|
|
|
108
127
|
`onPointClick(point, event)` reports the point nearest a click inside the plot,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { DEFAULT_FONT_SIZE, textHeight } from '../layout/measure-text';
|
|
3
|
-
import { crisp, PLOT_LABEL_GAP } from '../layout/metrics';
|
|
4
|
-
import { DEFAULT_AXIS_COLOR, DEFAULT_MUTED_TEXT_COLOR, DEFAULT_SURFACE_COLOR } from '../model/palette';
|
|
2
|
+
import { DEFAULT_FONT_SIZE, measureText, textAscent, textHeight } from '../layout/measure-text';
|
|
3
|
+
import { crisp, PLOT_BADGE_PADDING_X, PLOT_BADGE_PADDING_Y, PLOT_BADGE_RADIUS, PLOT_LABEL_GAP } from '../layout/metrics';
|
|
4
|
+
import { DEFAULT_AXIS_COLOR, DEFAULT_MUTED_TEXT_COLOR, DEFAULT_SURFACE_COLOR, textColorOn } from '../model/palette';
|
|
5
5
|
import { ABOVE_SERIES_Z_INDEX } from '../types/options';
|
|
6
6
|
const DASH = {
|
|
7
7
|
solid: undefined,
|
|
@@ -28,8 +28,8 @@ function project(scale, value, edge) {
|
|
|
28
28
|
const at = scale.map(numeric);
|
|
29
29
|
return typeof at === 'number' ? at : null;
|
|
30
30
|
}
|
|
31
|
-
const textProps = style => ({
|
|
32
|
-
fill: (style === null || style === void 0 ? void 0 : style.color) ?? DEFAULT_MUTED_TEXT_COLOR,
|
|
31
|
+
const textProps = (style, onFill) => ({
|
|
32
|
+
fill: (style === null || style === void 0 ? void 0 : style.color) ?? (onFill ? textColorOn(onFill) : DEFAULT_MUTED_TEXT_COLOR),
|
|
33
33
|
fontFamily: style === null || style === void 0 ? void 0 : style.fontFamily,
|
|
34
34
|
fontSize: (style === null || style === void 0 ? void 0 : style.fontSize) ?? DEFAULT_FONT_SIZE,
|
|
35
35
|
fontWeight: style === null || style === void 0 ? void 0 : style.fontWeight
|
|
@@ -39,40 +39,59 @@ const ANCHOR = {
|
|
|
39
39
|
center: 'middle',
|
|
40
40
|
right: 'end'
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* `align` is horizontal and `verticalAlign` vertical whichever way the
|
|
44
|
+
* decoration runs, as in Highcharts — a horizontal line takes its label to the
|
|
45
|
+
* left edge of the plot and sits it on the rule.
|
|
46
|
+
*/
|
|
42
47
|
function PlotLabel({
|
|
43
48
|
label,
|
|
44
|
-
|
|
45
|
-
span,
|
|
49
|
+
box,
|
|
46
50
|
along
|
|
47
51
|
}) {
|
|
48
52
|
if (!(label !== null && label !== void 0 && label.text)) {
|
|
49
53
|
return null;
|
|
50
54
|
}
|
|
51
|
-
const [low, high] = span[0] <= span[1] ? span : [span[1], span[0]];
|
|
52
55
|
const align = label.align ?? (along === 'x' ? 'center' : 'left');
|
|
53
56
|
const verticalAlign = label.verticalAlign ?? (along === 'x' ? 'top' : 'middle');
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
left:
|
|
58
|
-
center:
|
|
59
|
-
right:
|
|
60
|
-
}[align];
|
|
61
|
-
const
|
|
62
|
-
top:
|
|
63
|
-
middle:
|
|
64
|
-
bottom:
|
|
65
|
-
}[verticalAlign];
|
|
66
|
-
const x = (along === 'x' ? alongAt : acrossAt) + (label.x ?? 0);
|
|
67
|
-
const y = (along === 'x' ? acrossAt : alongAt) + (label.y ?? 0);
|
|
57
|
+
const line = textHeight(label.style);
|
|
58
|
+
const ascent = textAscent(label.style);
|
|
59
|
+
const x = {
|
|
60
|
+
left: box.x + PLOT_LABEL_GAP,
|
|
61
|
+
center: box.x + box.width / 2,
|
|
62
|
+
right: box.x + box.width - PLOT_LABEL_GAP
|
|
63
|
+
}[align] + (label.x ?? 0);
|
|
64
|
+
const y = {
|
|
65
|
+
top: box.y + PLOT_LABEL_GAP + ascent,
|
|
66
|
+
middle: box.y + box.height / 2 + ascent - line / 2,
|
|
67
|
+
bottom: box.y + box.height - PLOT_LABEL_GAP - (line - ascent)
|
|
68
|
+
}[verticalAlign] + (label.y ?? 0);
|
|
68
69
|
const rotation = label.rotation ?? 0;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const fill = label.backgroundColor;
|
|
71
|
+
const padX = label.padding ?? PLOT_BADGE_PADDING_X;
|
|
72
|
+
const padY = label.padding ?? PLOT_BADGE_PADDING_Y;
|
|
73
|
+
const width = measureText(label.text, label.style) + padX * 2;
|
|
74
|
+
const height = line + padY * 2;
|
|
75
|
+
return _jsxs("g", {
|
|
73
76
|
transform: rotation === 0 ? undefined : `rotate(${rotation} ${x} ${y})`,
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
children: [fill && _jsx("rect", {
|
|
78
|
+
x: {
|
|
79
|
+
left: x - padX,
|
|
80
|
+
center: x - width / 2,
|
|
81
|
+
right: x - width + padX
|
|
82
|
+
}[align],
|
|
83
|
+
y: y - ascent - padY,
|
|
84
|
+
width: width,
|
|
85
|
+
height: height,
|
|
86
|
+
rx: label.borderRadius ?? PLOT_BADGE_RADIUS,
|
|
87
|
+
fill: fill
|
|
88
|
+
}), _jsx("text", {
|
|
89
|
+
x: x,
|
|
90
|
+
y: y,
|
|
91
|
+
textAnchor: ANCHOR[align],
|
|
92
|
+
...textProps(label.style, fill),
|
|
93
|
+
children: label.text
|
|
94
|
+
})]
|
|
76
95
|
});
|
|
77
96
|
}
|
|
78
97
|
/**
|
|
@@ -124,8 +143,7 @@ export function PlotDecorations({
|
|
|
124
143
|
strokeWidth: options_.borderWidth ?? 0
|
|
125
144
|
}), _jsx(PlotLabel, {
|
|
126
145
|
label: options_.label,
|
|
127
|
-
|
|
128
|
-
span: [low, high],
|
|
146
|
+
box: box,
|
|
129
147
|
along: along
|
|
130
148
|
})]
|
|
131
149
|
}, `band-${index}`);
|
|
@@ -159,8 +177,17 @@ export function PlotDecorations({
|
|
|
159
177
|
strokeDasharray: DASH[options_.dashStyle ?? 'solid']
|
|
160
178
|
}), _jsx(PlotLabel, {
|
|
161
179
|
label: options_.label,
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
box: along === 'x' ? {
|
|
181
|
+
x: on,
|
|
182
|
+
y: plot.y,
|
|
183
|
+
width: 0,
|
|
184
|
+
height: plot.height
|
|
185
|
+
} : {
|
|
186
|
+
x: plot.x,
|
|
187
|
+
y: on,
|
|
188
|
+
width: plot.width,
|
|
189
|
+
height: 0
|
|
190
|
+
},
|
|
164
191
|
along: along
|
|
165
192
|
})]
|
|
166
193
|
}, `line-${index}`);
|
|
@@ -6,4 +6,9 @@ export declare const DEFAULT_SUBTITLE_FONT_SIZE: string;
|
|
|
6
6
|
export declare const toCssFont: (style?: TextStyle) => string;
|
|
7
7
|
export declare function measureText(text: string, style?: TextStyle): number;
|
|
8
8
|
export declare const textHeight: (style?: TextStyle) => number;
|
|
9
|
+
/**
|
|
10
|
+
* How much of a line box stands above the baseline. Drawing a box around text
|
|
11
|
+
* needs the split, not just the height: SVG places text by its baseline.
|
|
12
|
+
*/
|
|
13
|
+
export declare const textAscent: (style?: TextStyle) => number;
|
|
9
14
|
export declare function clearTextMeasurementCache(): void;
|
|
@@ -51,6 +51,11 @@ export function measureText(text, style) {
|
|
|
51
51
|
return width;
|
|
52
52
|
}
|
|
53
53
|
export const textHeight = style => Math.ceil(parseFontSize(toCssFont(style)) * 1.2);
|
|
54
|
+
/**
|
|
55
|
+
* How much of a line box stands above the baseline. Drawing a box around text
|
|
56
|
+
* needs the split, not just the height: SVG places text by its baseline.
|
|
57
|
+
*/
|
|
58
|
+
export const textAscent = style => Math.round(textHeight(style) * 0.8);
|
|
54
59
|
export function clearTextMeasurementCache() {
|
|
55
60
|
cache.clear();
|
|
56
61
|
context = undefined;
|
package/core/layout/metrics.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export declare const TOOLTIP_PADDING = 8;
|
|
|
48
48
|
export declare const TOOLTIP_RADIUS = 6;
|
|
49
49
|
/** Distance from a plot band or plot line to its own label. */
|
|
50
50
|
export declare const PLOT_LABEL_GAP = 6;
|
|
51
|
+
/**
|
|
52
|
+
* A plot label drawn on a fill: its padding and corner radius. Wider than it is
|
|
53
|
+
* tall, because a pill reads as a pill only when the text has room either side.
|
|
54
|
+
*/
|
|
55
|
+
export declare const PLOT_BADGE_PADDING_X = 6;
|
|
56
|
+
export declare const PLOT_BADGE_PADDING_Y = 2;
|
|
57
|
+
export declare const PLOT_BADGE_RADIUS = 4;
|
|
51
58
|
/**
|
|
52
59
|
* Puts a stroke where the device can draw it in one row of pixels: half a stroke
|
|
53
60
|
* either side of the position has to land on a pixel boundary, so an odd stroke
|
package/core/layout/metrics.js
CHANGED
|
@@ -48,6 +48,13 @@ export const TOOLTIP_PADDING = 8;
|
|
|
48
48
|
export const TOOLTIP_RADIUS = 6;
|
|
49
49
|
/** Distance from a plot band or plot line to its own label. */
|
|
50
50
|
export const PLOT_LABEL_GAP = 6;
|
|
51
|
+
/**
|
|
52
|
+
* A plot label drawn on a fill: its padding and corner radius. Wider than it is
|
|
53
|
+
* tall, because a pill reads as a pill only when the text has room either side.
|
|
54
|
+
*/
|
|
55
|
+
export const PLOT_BADGE_PADDING_X = 6;
|
|
56
|
+
export const PLOT_BADGE_PADDING_Y = 2;
|
|
57
|
+
export const PLOT_BADGE_RADIUS = 4;
|
|
51
58
|
/**
|
|
52
59
|
* Puts a stroke where the device can draw it in one row of pixels: half a stroke
|
|
53
60
|
* either side of the position has to land on a pixel boundary, so an odd stroke
|
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;
|
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 {
|