@voila.dev/chart 4.0.107 → 4.0.108
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/canvas/paint-scene.d.ts.map +1 -1
- package/dist/canvas/paint-scene.js +31 -1
- package/dist/canvas/paint-scene.js.map +2 -2
- package/dist/core/marks/area.d.ts.map +1 -1
- package/dist/core/marks/area.js +3 -2
- package/dist/core/marks/area.js.map +2 -2
- package/dist/core/marks/bar.d.ts +5 -0
- package/dist/core/marks/bar.d.ts.map +1 -1
- package/dist/core/marks/bar.js +19 -9
- package/dist/core/marks/bar.js.map +2 -2
- package/dist/core/marks/cell.d.ts.map +1 -1
- package/dist/core/marks/cell.js +2 -1
- package/dist/core/marks/cell.js.map +2 -2
- package/dist/core/marks/dot.d.ts.map +1 -1
- package/dist/core/marks/dot.js +2 -1
- package/dist/core/marks/dot.js.map +2 -2
- package/dist/core/marks/funnel.d.ts.map +1 -1
- package/dist/core/marks/funnel.js +3 -2
- package/dist/core/marks/funnel.js.map +2 -2
- package/dist/core/marks/line.d.ts.map +1 -1
- package/dist/core/marks/line.js +2 -1
- package/dist/core/marks/line.js.map +2 -2
- package/dist/core/marks/polar.d.ts.map +1 -1
- package/dist/core/marks/polar.js +5 -4
- package/dist/core/marks/polar.js.map +2 -2
- package/dist/core/marks/rect.d.ts.map +1 -1
- package/dist/core/marks/rect.js +2 -1
- package/dist/core/marks/rect.js.map +2 -2
- package/dist/core/marks/shared.d.ts +8 -2
- package/dist/core/marks/shared.d.ts.map +1 -1
- package/dist/core/marks/shared.js +4 -0
- package/dist/core/marks/shared.js.map +2 -2
- package/dist/core/marks/stats/dodge.d.ts.map +1 -1
- package/dist/core/marks/stats/dodge.js +2 -1
- package/dist/core/marks/stats/dodge.js.map +2 -2
- package/dist/core/marks/stats/histogram.d.ts.map +1 -1
- package/dist/core/marks/stats/histogram.js +2 -1
- package/dist/core/marks/stats/histogram.js.map +2 -2
- package/dist/core/marks/stats/waffle.d.ts.map +1 -1
- package/dist/core/marks/stats/waffle.js +3 -2
- package/dist/core/marks/stats/waffle.js.map +2 -2
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/chart-placeholders.d.ts +13 -0
- package/dist/react/chart-placeholders.d.ts.map +1 -0
- package/dist/react/chart-placeholders.js +77 -0
- package/dist/react/chart-placeholders.js.map +7 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +2 -2
- package/dist/react/svg-scene.d.ts +1 -1
- package/dist/react/svg-scene.d.ts.map +1 -1
- package/dist/react/svg-scene.js +53 -10
- package/dist/react/svg-scene.js.map +2 -2
- package/package.json +1 -1
- package/src/canvas/paint-scene.ts +45 -1
- package/src/core/marks/area.ts +2 -1
- package/src/core/marks/bar.ts +25 -8
- package/src/core/marks/cell.ts +2 -1
- package/src/core/marks/dot.ts +2 -1
- package/src/core/marks/funnel.ts +2 -1
- package/src/core/marks/line.ts +2 -1
- package/src/core/marks/polar.ts +4 -3
- package/src/core/marks/rect.ts +2 -1
- package/src/core/marks/shared.ts +15 -2
- package/src/core/marks/stats/dodge.ts +2 -1
- package/src/core/marks/stats/histogram.ts +2 -1
- package/src/core/marks/stats/waffle.ts +2 -1
- package/src/core/types.ts +2 -0
- package/src/react/chart-placeholders.tsx +93 -0
- package/src/react/index.ts +1 -0
- package/src/react/svg-scene.tsx +72 -11
package/src/core/marks/polar.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
markId,
|
|
11
11
|
paint,
|
|
12
12
|
seriesResolver,
|
|
13
|
+
tips,
|
|
13
14
|
} from "#/core/marks/shared.ts";
|
|
14
15
|
import {
|
|
15
16
|
arcPath,
|
|
@@ -187,7 +188,7 @@ export function arc<TDatum>(
|
|
|
187
188
|
opacity: options.opacity,
|
|
188
189
|
}),
|
|
189
190
|
});
|
|
190
|
-
if (options.tip
|
|
191
|
+
if (!tips(options.tip, data[index], index)) {
|
|
191
192
|
continue;
|
|
192
193
|
}
|
|
193
194
|
const middle = (angle.startAngle + angle.endAngle) / 2;
|
|
@@ -307,7 +308,7 @@ export function radialBar<TDatum>(
|
|
|
307
308
|
d: arcPath(shape),
|
|
308
309
|
paint: paint({ fill: series.color, opacity: options.opacity }),
|
|
309
310
|
});
|
|
310
|
-
if (options.tip
|
|
311
|
+
if (!tips(options.tip, data[index], index)) {
|
|
311
312
|
continue;
|
|
312
313
|
}
|
|
313
314
|
const anchor = polarToCartesian(
|
|
@@ -512,7 +513,7 @@ export function radar<TDatum>(
|
|
|
512
513
|
};
|
|
513
514
|
polygon.vertices[spoke] = vertex;
|
|
514
515
|
polygons.set(series.key, polygon);
|
|
515
|
-
if (options.tip
|
|
516
|
+
if (tips(options.tip, data[index], index)) {
|
|
516
517
|
points.push({
|
|
517
518
|
key: `${id}:${index}`,
|
|
518
519
|
markId: id,
|
package/src/core/marks/rect.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
markId,
|
|
11
11
|
paint,
|
|
12
12
|
seriesResolver,
|
|
13
|
+
tips,
|
|
13
14
|
valueOn,
|
|
14
15
|
} from "#/core/marks/shared.ts";
|
|
15
16
|
import { stackSegments } from "#/core/marks/stack.ts";
|
|
@@ -131,7 +132,7 @@ export function rectY<TDatum>(
|
|
|
131
132
|
opacity: options.opacity,
|
|
132
133
|
}),
|
|
133
134
|
});
|
|
134
|
-
if (options.tip
|
|
135
|
+
if (tips(options.tip, data[index], index)) {
|
|
135
136
|
points.push({
|
|
136
137
|
key: `${id}:${index}`,
|
|
137
138
|
markId: id,
|
package/src/core/marks/shared.ts
CHANGED
|
@@ -22,8 +22,12 @@ export interface ChartMarkOptions<TDatum> {
|
|
|
22
22
|
readonly label?: string;
|
|
23
23
|
/** A field or function whose distinct values split the mark into coloured series. */
|
|
24
24
|
readonly color?: ChartAccessor<TDatum, ChartValue>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* `false` keeps the mark out of focus, tooltips and the data table; a
|
|
27
|
+
* function decides per datum (the point where a recorded line hands over
|
|
28
|
+
* to its dashed projection belongs to one of the two, not both).
|
|
29
|
+
*/
|
|
30
|
+
readonly tip?: boolean | ((datum: TDatum, index: number) => boolean);
|
|
27
31
|
readonly opacity?: number;
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -132,3 +136,12 @@ export function valueOn(
|
|
|
132
136
|
}
|
|
133
137
|
return numericValue(raw);
|
|
134
138
|
}
|
|
139
|
+
|
|
140
|
+
/** Whether a datum takes part in focus, tooltips and the data table. */
|
|
141
|
+
export function tips<TDatum>(
|
|
142
|
+
tip: ChartMarkOptions<TDatum>["tip"],
|
|
143
|
+
datum: TDatum,
|
|
144
|
+
index: number,
|
|
145
|
+
): boolean {
|
|
146
|
+
return typeof tip === "function" ? tip(datum, index) : tip !== false;
|
|
147
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
markId,
|
|
6
6
|
paint,
|
|
7
7
|
seriesResolver,
|
|
8
|
+
tips,
|
|
8
9
|
valueOn,
|
|
9
10
|
} from "#/core/marks/shared.ts";
|
|
10
11
|
import type {
|
|
@@ -109,7 +110,7 @@ export function dodgeY<TDatum>(
|
|
|
109
110
|
r,
|
|
110
111
|
paint: paint({ fill: series.color, opacity: options.opacity }),
|
|
111
112
|
});
|
|
112
|
-
if (options.tip
|
|
113
|
+
if (tips(options.tip, data[entry.index], entry.index)) {
|
|
113
114
|
points.push({
|
|
114
115
|
key: `${id}:${entry.index}`,
|
|
115
116
|
markId: id,
|
|
@@ -95,7 +95,8 @@ export function histogram<TDatum>(
|
|
|
95
95
|
return rectY(bins, {
|
|
96
96
|
id: options.id,
|
|
97
97
|
label: options.label,
|
|
98
|
-
|
|
98
|
+
// Bins are not the caller's data: only a plain on/off carries over.
|
|
99
|
+
tip: typeof options.tip === "function" ? undefined : options.tip,
|
|
99
100
|
opacity: options.opacity,
|
|
100
101
|
fill: options.fill,
|
|
101
102
|
x1: (bin) => bin.x1,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
markId,
|
|
11
11
|
paint,
|
|
12
12
|
seriesResolver,
|
|
13
|
+
tips,
|
|
13
14
|
} from "#/core/marks/shared.ts";
|
|
14
15
|
import { categoryKey } from "#/core/scales/discrete.ts";
|
|
15
16
|
import type {
|
|
@@ -117,7 +118,7 @@ export function waffleY<TDatum>(
|
|
|
117
118
|
paint: paint({ fill: series.color, opacity: options.opacity }),
|
|
118
119
|
});
|
|
119
120
|
}
|
|
120
|
-
if (options.tip
|
|
121
|
+
if (tips(options.tip, data[index], index)) {
|
|
121
122
|
points.push({
|
|
122
123
|
key: `${id}:${index}`,
|
|
123
124
|
markId: id,
|
package/src/core/types.ts
CHANGED
|
@@ -90,6 +90,8 @@ export interface ChartPaint {
|
|
|
90
90
|
readonly opacity?: number;
|
|
91
91
|
readonly fillOpacity?: number;
|
|
92
92
|
readonly strokeOpacity?: number;
|
|
93
|
+
/** Fill with diagonal hatching in the fill colour: a value pencilled in, not yet true. */
|
|
94
|
+
readonly hatch?: boolean;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export interface ChartTextPaint extends ChartPaint {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stand-ins for a chart: no data yet, or none at all. Sized like the chart
|
|
5
|
+
* they replace (pass the same height), so the page does not jump when the
|
|
6
|
+
* data lands. Styled with the kit's tokens when the page has them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BOX: React.CSSProperties = {
|
|
10
|
+
width: "100%",
|
|
11
|
+
aspectRatio: "16 / 9",
|
|
12
|
+
borderRadius: 8,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
interface EmptyProps extends React.ComponentProps<"div"> {}
|
|
16
|
+
|
|
17
|
+
/** In place of a chart with nothing to show. Pass the localized message as children. */
|
|
18
|
+
export function ChartEmpty({ style, ...props }: EmptyProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="chart-empty"
|
|
22
|
+
style={{
|
|
23
|
+
...BOX,
|
|
24
|
+
display: "flex",
|
|
25
|
+
flexDirection: "column",
|
|
26
|
+
alignItems: "center",
|
|
27
|
+
justifyContent: "center",
|
|
28
|
+
gap: 4,
|
|
29
|
+
border:
|
|
30
|
+
"1px dashed var(--border, color-mix(in oklab, currentColor 25%, transparent))",
|
|
31
|
+
color: "var(--muted-foreground, inherit)",
|
|
32
|
+
fontSize: 14,
|
|
33
|
+
textAlign: "center",
|
|
34
|
+
textWrap: "balance",
|
|
35
|
+
...style,
|
|
36
|
+
}}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Fixed, not random, so the server and the browser render the same bars. */
|
|
43
|
+
const BAR_HEIGHTS = [40, 70, 55, 90, 65, 80, 50] as const;
|
|
44
|
+
|
|
45
|
+
const SKELETON_CSS = `
|
|
46
|
+
@keyframes voila-chart-pulse{50%{opacity:.5}}
|
|
47
|
+
@media (prefers-reduced-motion:no-preference){[data-slot="chart-skeleton-bar"]{animation:voila-chart-pulse 2s cubic-bezier(.4,0,.6,1) infinite}}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
interface SkeletonProps extends React.ComponentProps<"div"> {
|
|
51
|
+
/** What a screen reader hears while the chart loads. */
|
|
52
|
+
readonly label?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A loading placeholder shaped like a bar chart, announced as a status. */
|
|
56
|
+
export function ChartSkeleton({
|
|
57
|
+
label = "Chargement",
|
|
58
|
+
style,
|
|
59
|
+
...props
|
|
60
|
+
}: SkeletonProps) {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
data-slot="chart-skeleton"
|
|
64
|
+
role="status"
|
|
65
|
+
aria-label={label}
|
|
66
|
+
style={{
|
|
67
|
+
...BOX,
|
|
68
|
+
display: "flex",
|
|
69
|
+
alignItems: "flex-end",
|
|
70
|
+
gap: 8,
|
|
71
|
+
...style,
|
|
72
|
+
}}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
<style href="voila-chart-skeleton" precedence="default">
|
|
76
|
+
{SKELETON_CSS}
|
|
77
|
+
</style>
|
|
78
|
+
{BAR_HEIGHTS.map((height) => (
|
|
79
|
+
<div
|
|
80
|
+
key={height}
|
|
81
|
+
data-slot="chart-skeleton-bar"
|
|
82
|
+
style={{
|
|
83
|
+
width: "100%",
|
|
84
|
+
height: `${height}%`,
|
|
85
|
+
borderRadius: 6,
|
|
86
|
+
background:
|
|
87
|
+
"var(--muted, color-mix(in oklab, currentColor 10%, transparent))",
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
package/src/react/index.ts
CHANGED
package/src/react/svg-scene.tsx
CHANGED
|
@@ -17,9 +17,40 @@ const BASELINE: Record<
|
|
|
17
17
|
alphabetic: "alphabetic",
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const HATCH_STEP = 6;
|
|
21
|
+
|
|
22
|
+
/** A stable id per colour (djb2), the same on the server and in the browser. */
|
|
23
|
+
function hatchId(prefix: string, color: string): string {
|
|
24
|
+
let hash = 5381;
|
|
25
|
+
for (const character of color) {
|
|
26
|
+
hash = ((hash << 5) + hash + character.charCodeAt(0)) | 0;
|
|
27
|
+
}
|
|
28
|
+
return `${prefix}-hatch-${(hash >>> 0).toString(36)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hatchColors(
|
|
32
|
+
nodes: ReadonlyArray<SceneNode>,
|
|
33
|
+
into = new Set<string>(),
|
|
34
|
+
): Set<string> {
|
|
35
|
+
for (const node of nodes) {
|
|
36
|
+
if (node.kind === "group") {
|
|
37
|
+
hatchColors(node.children, into);
|
|
38
|
+
} else if (node.paint.hatch && node.paint.fill) {
|
|
39
|
+
into.add(node.paint.fill);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return into;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function paintProps(
|
|
46
|
+
paint: ChartPaint,
|
|
47
|
+
prefix: string,
|
|
48
|
+
): React.SVGAttributes<SVGElement> {
|
|
21
49
|
return {
|
|
22
|
-
fill:
|
|
50
|
+
fill:
|
|
51
|
+
paint.hatch && paint.fill
|
|
52
|
+
? `url(#${hatchId(prefix, paint.fill)})`
|
|
53
|
+
: (paint.fill ?? "none"),
|
|
23
54
|
stroke: paint.stroke,
|
|
24
55
|
strokeWidth: paint.strokeWidth,
|
|
25
56
|
strokeDasharray: paint.strokeDasharray,
|
|
@@ -72,7 +103,7 @@ function SceneElement({
|
|
|
72
103
|
<path
|
|
73
104
|
{...data}
|
|
74
105
|
d={roundedBarPath({ ...node, radius: node.corners })}
|
|
75
|
-
{...paintProps(node.paint)}
|
|
106
|
+
{...paintProps(node.paint, clipPrefix)}
|
|
76
107
|
/>
|
|
77
108
|
) : (
|
|
78
109
|
<rect
|
|
@@ -81,11 +112,13 @@ function SceneElement({
|
|
|
81
112
|
y={node.y}
|
|
82
113
|
width={node.width}
|
|
83
114
|
height={node.height}
|
|
84
|
-
{...paintProps(node.paint)}
|
|
115
|
+
{...paintProps(node.paint, clipPrefix)}
|
|
85
116
|
/>
|
|
86
117
|
);
|
|
87
118
|
case "path":
|
|
88
|
-
return
|
|
119
|
+
return (
|
|
120
|
+
<path {...data} d={node.d} {...paintProps(node.paint, clipPrefix)} />
|
|
121
|
+
);
|
|
89
122
|
case "circle":
|
|
90
123
|
return (
|
|
91
124
|
<circle
|
|
@@ -93,7 +126,7 @@ function SceneElement({
|
|
|
93
126
|
cx={node.cx}
|
|
94
127
|
cy={node.cy}
|
|
95
128
|
r={node.r}
|
|
96
|
-
{...paintProps(node.paint)}
|
|
129
|
+
{...paintProps(node.paint, clipPrefix)}
|
|
97
130
|
/>
|
|
98
131
|
);
|
|
99
132
|
case "line":
|
|
@@ -104,7 +137,7 @@ function SceneElement({
|
|
|
104
137
|
y1={node.y1}
|
|
105
138
|
x2={node.x2}
|
|
106
139
|
y2={node.y2}
|
|
107
|
-
{...paintProps(node.paint)}
|
|
140
|
+
{...paintProps(node.paint, clipPrefix)}
|
|
108
141
|
/>
|
|
109
142
|
);
|
|
110
143
|
case "text":
|
|
@@ -124,7 +157,7 @@ function SceneElement({
|
|
|
124
157
|
dominantBaseline={
|
|
125
158
|
node.paint.baseline ? BASELINE[node.paint.baseline] : undefined
|
|
126
159
|
}
|
|
127
|
-
{...paintProps(node.paint)}
|
|
160
|
+
{...paintProps(node.paint, clipPrefix)}
|
|
128
161
|
>
|
|
129
162
|
{node.text}
|
|
130
163
|
</text>
|
|
@@ -140,7 +173,35 @@ export function SvgSceneNodes({
|
|
|
140
173
|
scene: ChartScene;
|
|
141
174
|
clipPrefix: string;
|
|
142
175
|
}) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
const hatches = [...hatchColors(scene.nodes)];
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
{hatches.length > 0 ? (
|
|
180
|
+
<defs>
|
|
181
|
+
{hatches.map((color) => (
|
|
182
|
+
<pattern
|
|
183
|
+
key={color}
|
|
184
|
+
id={hatchId(clipPrefix, color)}
|
|
185
|
+
width={HATCH_STEP}
|
|
186
|
+
height={HATCH_STEP}
|
|
187
|
+
patternUnits="userSpaceOnUse"
|
|
188
|
+
patternTransform="rotate(45)"
|
|
189
|
+
>
|
|
190
|
+
<line
|
|
191
|
+
x1={0}
|
|
192
|
+
y1={0}
|
|
193
|
+
x2={0}
|
|
194
|
+
y2={HATCH_STEP}
|
|
195
|
+
stroke={color}
|
|
196
|
+
strokeWidth={1.5}
|
|
197
|
+
/>
|
|
198
|
+
</pattern>
|
|
199
|
+
))}
|
|
200
|
+
</defs>
|
|
201
|
+
) : null}
|
|
202
|
+
{scene.nodes.map((node) => (
|
|
203
|
+
<SceneElement key={node.key} node={node} clipPrefix={clipPrefix} />
|
|
204
|
+
))}
|
|
205
|
+
</>
|
|
206
|
+
);
|
|
146
207
|
}
|