@voila.dev/chart 4.0.0
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/README.md +85 -0
- package/dist/brush/brush-layer.d.ts +16 -0
- package/dist/brush/brush-layer.d.ts.map +1 -0
- package/dist/brush/brush-layer.js +137 -0
- package/dist/brush/brush-layer.js.map +7 -0
- package/dist/brush/index.d.ts +10 -0
- package/dist/brush/index.d.ts.map +1 -0
- package/dist/brush/index.js +8 -0
- package/dist/brush/index.js.map +7 -0
- package/dist/canvas/canvas-renderer.d.ts +11 -0
- package/dist/canvas/canvas-renderer.d.ts.map +1 -0
- package/dist/canvas/canvas-renderer.js +49 -0
- package/dist/canvas/canvas-renderer.js.map +7 -0
- package/dist/canvas/index.d.ts +2 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/index.js +5 -0
- package/dist/canvas/index.js.map +7 -0
- package/dist/canvas/paint-scene.d.ts +4 -0
- package/dist/canvas/paint-scene.d.ts.map +1 -0
- package/dist/canvas/paint-scene.js +130 -0
- package/dist/canvas/paint-scene.js.map +7 -0
- package/dist/canvas/resolve-color.d.ts +18 -0
- package/dist/canvas/resolve-color.d.ts.map +1 -0
- package/dist/canvas/resolve-color.js +52 -0
- package/dist/canvas/resolve-color.js.map +7 -0
- package/dist/contour/density.d.ts +20 -0
- package/dist/contour/density.d.ts.map +1 -0
- package/dist/contour/density.js +59 -0
- package/dist/contour/density.js.map +7 -0
- package/dist/contour/index.d.ts +2 -0
- package/dist/contour/index.d.ts.map +1 -0
- package/dist/contour/index.js +5 -0
- package/dist/contour/index.js.map +7 -0
- package/dist/core/channel.d.ts +11 -0
- package/dist/core/channel.d.ts.map +1 -0
- package/dist/core/channel.js +44 -0
- package/dist/core/channel.js.map +7 -0
- package/dist/core/compile.d.ts +22 -0
- package/dist/core/compile.d.ts.map +1 -0
- package/dist/core/compile.js +295 -0
- package/dist/core/compile.js.map +7 -0
- package/dist/core/define-chart.d.ts +96 -0
- package/dist/core/define-chart.d.ts.map +1 -0
- package/dist/core/define-chart.js +7 -0
- package/dist/core/define-chart.js.map +7 -0
- package/dist/core/facet.d.ts +7 -0
- package/dist/core/facet.d.ts.map +1 -0
- package/dist/core/facet.js +134 -0
- package/dist/core/facet.js.map +7 -0
- package/dist/core/format.d.ts +37 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +108 -0
- package/dist/core/format.js.map +7 -0
- package/dist/core/guides/axes.d.ts +25 -0
- package/dist/core/guides/axes.d.ts.map +1 -0
- package/dist/core/guides/axes.js +204 -0
- package/dist/core/guides/axes.js.map +7 -0
- package/dist/core/marks/area.d.ts +23 -0
- package/dist/core/marks/area.d.ts.map +1 -0
- package/dist/core/marks/area.js +187 -0
- package/dist/core/marks/area.js.map +7 -0
- package/dist/core/marks/bar.d.ts +26 -0
- package/dist/core/marks/bar.d.ts.map +1 -0
- package/dist/core/marks/bar.js +200 -0
- package/dist/core/marks/bar.js.map +7 -0
- package/dist/core/marks/cell.d.ts +16 -0
- package/dist/core/marks/cell.d.ts.map +1 -0
- package/dist/core/marks/cell.js +98 -0
- package/dist/core/marks/cell.js.map +7 -0
- package/dist/core/marks/dot.d.ts +17 -0
- package/dist/core/marks/dot.d.ts.map +1 -0
- package/dist/core/marks/dot.js +125 -0
- package/dist/core/marks/dot.js.map +7 -0
- package/dist/core/marks/funnel-layout.d.ts +37 -0
- package/dist/core/marks/funnel-layout.d.ts.map +1 -0
- package/dist/core/marks/funnel-layout.js +61 -0
- package/dist/core/marks/funnel-layout.js.map +7 -0
- package/dist/core/marks/funnel.d.ts +19 -0
- package/dist/core/marks/funnel.d.ts.map +1 -0
- package/dist/core/marks/funnel.js +143 -0
- package/dist/core/marks/funnel.js.map +7 -0
- package/dist/core/marks/line.d.ts +17 -0
- package/dist/core/marks/line.d.ts.map +1 -0
- package/dist/core/marks/line.js +150 -0
- package/dist/core/marks/line.js.map +7 -0
- package/dist/core/marks/polar.d.ts +61 -0
- package/dist/core/marks/polar.d.ts.map +1 -0
- package/dist/core/marks/polar.js +436 -0
- package/dist/core/marks/polar.js.map +7 -0
- package/dist/core/marks/rect.d.ts +22 -0
- package/dist/core/marks/rect.d.ts.map +1 -0
- package/dist/core/marks/rect.js +119 -0
- package/dist/core/marks/rect.js.map +7 -0
- package/dist/core/marks/rule.d.ts +17 -0
- package/dist/core/marks/rule.d.ts.map +1 -0
- package/dist/core/marks/rule.js +73 -0
- package/dist/core/marks/rule.js.map +7 -0
- package/dist/core/marks/shared.d.ts +41 -0
- package/dist/core/marks/shared.d.ts.map +1 -0
- package/dist/core/marks/shared.js +74 -0
- package/dist/core/marks/shared.js.map +7 -0
- package/dist/core/marks/stack.d.ts +13 -0
- package/dist/core/marks/stack.d.ts.map +1 -0
- package/dist/core/marks/stack.js +28 -0
- package/dist/core/marks/stack.js.map +7 -0
- package/dist/core/marks/stats/difference.d.ts +20 -0
- package/dist/core/marks/stats/difference.d.ts.map +1 -0
- package/dist/core/marks/stats/difference.js +168 -0
- package/dist/core/marks/stats/difference.js.map +7 -0
- package/dist/core/marks/stats/distribution.d.ts +41 -0
- package/dist/core/marks/stats/distribution.d.ts.map +1 -0
- package/dist/core/marks/stats/distribution.js +354 -0
- package/dist/core/marks/stats/distribution.js.map +7 -0
- package/dist/core/marks/stats/dodge.d.ts +16 -0
- package/dist/core/marks/stats/dodge.d.ts.map +1 -0
- package/dist/core/marks/stats/dodge.js +104 -0
- package/dist/core/marks/stats/dodge.js.map +7 -0
- package/dist/core/marks/stats/hexbin.d.ts +26 -0
- package/dist/core/marks/stats/hexbin.d.ts.map +1 -0
- package/dist/core/marks/stats/hexbin.js +113 -0
- package/dist/core/marks/stats/hexbin.js.map +7 -0
- package/dist/core/marks/stats/histogram.d.ts +27 -0
- package/dist/core/marks/stats/histogram.d.ts.map +1 -0
- package/dist/core/marks/stats/histogram.js +70 -0
- package/dist/core/marks/stats/histogram.js.map +7 -0
- package/dist/core/marks/stats/regression.d.ts +13 -0
- package/dist/core/marks/stats/regression.d.ts.map +1 -0
- package/dist/core/marks/stats/regression.js +80 -0
- package/dist/core/marks/stats/regression.js.map +7 -0
- package/dist/core/marks/stats/statistics.d.ts +34 -0
- package/dist/core/marks/stats/statistics.d.ts.map +1 -0
- package/dist/core/marks/stats/statistics.js +111 -0
- package/dist/core/marks/stats/statistics.js.map +7 -0
- package/dist/core/marks/stats/waffle.d.ts +18 -0
- package/dist/core/marks/stats/waffle.d.ts.map +1 -0
- package/dist/core/marks/stats/waffle.js +117 -0
- package/dist/core/marks/stats/waffle.js.map +7 -0
- package/dist/core/marks/text.d.ts +19 -0
- package/dist/core/marks/text.d.ts.map +1 -0
- package/dist/core/marks/text.js +60 -0
- package/dist/core/marks/text.js.map +7 -0
- package/dist/core/motion/tween.d.ts +6 -0
- package/dist/core/motion/tween.d.ts.map +1 -0
- package/dist/core/motion/tween.js +98 -0
- package/dist/core/motion/tween.js.map +7 -0
- package/dist/core/nearest.d.ts +25 -0
- package/dist/core/nearest.d.ts.map +1 -0
- package/dist/core/nearest.js +151 -0
- package/dist/core/nearest.js.map +7 -0
- package/dist/core/paths.d.ts +65 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +192 -0
- package/dist/core/paths.js.map +7 -0
- package/dist/core/scales/color.d.ts +11 -0
- package/dist/core/scales/color.d.ts.map +1 -0
- package/dist/core/scales/color.js +35 -0
- package/dist/core/scales/color.js.map +7 -0
- package/dist/core/scales/continuous.d.ts +18 -0
- package/dist/core/scales/continuous.d.ts.map +1 -0
- package/dist/core/scales/continuous.js +82 -0
- package/dist/core/scales/continuous.js.map +7 -0
- package/dist/core/scales/discrete.d.ts +22 -0
- package/dist/core/scales/discrete.d.ts.map +1 -0
- package/dist/core/scales/discrete.js +103 -0
- package/dist/core/scales/discrete.js.map +7 -0
- package/dist/core/scales/resolve.d.ts +18 -0
- package/dist/core/scales/resolve.d.ts.map +1 -0
- package/dist/core/scales/resolve.js +112 -0
- package/dist/core/scales/resolve.js.map +7 -0
- package/dist/core/scales/time.d.ts +4 -0
- package/dist/core/scales/time.d.ts.map +1 -0
- package/dist/core/scales/time.js +140 -0
- package/dist/core/scales/time.js.map +7 -0
- package/dist/core/text.d.ts +3 -0
- package/dist/core/text.d.ts.map +1 -0
- package/dist/core/text.js +29 -0
- package/dist/core/text.js.map +7 -0
- package/dist/core/theme.d.ts +8 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +21 -0
- package/dist/core/theme.js.map +7 -0
- package/dist/core/ticks.d.ts +27 -0
- package/dist/core/ticks.d.ts.map +1 -0
- package/dist/core/ticks.js +76 -0
- package/dist/core/ticks.js.map +7 -0
- package/dist/core/types.d.ts +305 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +7 -0
- package/dist/force/force.d.ts +30 -0
- package/dist/force/force.d.ts.map +1 -0
- package/dist/force/force.js +143 -0
- package/dist/force/force.js.map +7 -0
- package/dist/force/index.d.ts +2 -0
- package/dist/force/index.d.ts.map +1 -0
- package/dist/force/index.js +7 -0
- package/dist/force/index.js.map +7 -0
- package/dist/geo/geo.d.ts +59 -0
- package/dist/geo/geo.d.ts.map +1 -0
- package/dist/geo/geo.js +231 -0
- package/dist/geo/geo.js.map +7 -0
- package/dist/geo/index.d.ts +2 -0
- package/dist/geo/index.d.ts.map +1 -0
- package/dist/geo/index.js +13 -0
- package/dist/geo/index.js.map +7 -0
- package/dist/hierarchy/hierarchy.d.ts +24 -0
- package/dist/hierarchy/hierarchy.d.ts.map +1 -0
- package/dist/hierarchy/hierarchy.js +279 -0
- package/dist/hierarchy/hierarchy.js.map +7 -0
- package/dist/hierarchy/index.d.ts +2 -0
- package/dist/hierarchy/index.d.ts.map +1 -0
- package/dist/hierarchy/index.js +11 -0
- package/dist/hierarchy/index.js.map +7 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +7 -0
- package/dist/react/brush-contract.d.ts +23 -0
- package/dist/react/brush-contract.d.ts.map +1 -0
- package/dist/react/brush-contract.js +7 -0
- package/dist/react/brush-contract.js.map +7 -0
- package/dist/react/chart-data-table.d.ts +18 -0
- package/dist/react/chart-data-table.d.ts.map +1 -0
- package/dist/react/chart-data-table.js +55 -0
- package/dist/react/chart-data-table.js.map +7 -0
- package/dist/react/chart-legend.d.ts +17 -0
- package/dist/react/chart-legend.d.ts.map +1 -0
- package/dist/react/chart-legend.js +126 -0
- package/dist/react/chart-legend.js.map +7 -0
- package/dist/react/chart-tooltip.d.ts +18 -0
- package/dist/react/chart-tooltip.d.ts.map +1 -0
- package/dist/react/chart-tooltip.js +95 -0
- package/dist/react/chart-tooltip.js.map +7 -0
- package/dist/react/chart.d.ts +45 -0
- package/dist/react/chart.d.ts.map +1 -0
- package/dist/react/chart.js +383 -0
- package/dist/react/chart.js.map +7 -0
- package/dist/react/focus-overlay.d.ts +14 -0
- package/dist/react/focus-overlay.d.ts.map +1 -0
- package/dist/react/focus-overlay.js +147 -0
- package/dist/react/focus-overlay.js.map +7 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +11 -0
- package/dist/react/index.js.map +7 -0
- package/dist/react/messages.d.ts +16 -0
- package/dist/react/messages.d.ts.map +1 -0
- package/dist/react/messages.js +33 -0
- package/dist/react/messages.js.map +7 -0
- package/dist/react/renderer.d.ts +14 -0
- package/dist/react/renderer.d.ts.map +1 -0
- package/dist/react/renderer.js +1 -0
- package/dist/react/renderer.js.map +7 -0
- package/dist/react/sr-only.d.ts +4 -0
- package/dist/react/sr-only.d.ts.map +1 -0
- package/dist/react/sr-only.js +15 -0
- package/dist/react/sr-only.js.map +7 -0
- package/dist/react/svg-renderer.d.ts +4 -0
- package/dist/react/svg-renderer.d.ts.map +1 -0
- package/dist/react/svg-renderer.js +20 -0
- package/dist/react/svg-renderer.js.map +7 -0
- package/dist/react/svg-scene.d.ts +8 -0
- package/dist/react/svg-scene.d.ts.map +1 -0
- package/dist/react/svg-scene.js +121 -0
- package/dist/react/svg-scene.js.map +7 -0
- package/dist/react/use-animated-scene.d.ts +10 -0
- package/dist/react/use-animated-scene.d.ts.map +1 -0
- package/dist/react/use-animated-scene.js +41 -0
- package/dist/react/use-animated-scene.js.map +7 -0
- package/dist/react/use-chart-focus.d.ts +29 -0
- package/dist/react/use-chart-focus.d.ts.map +1 -0
- package/dist/react/use-chart-focus.js +180 -0
- package/dist/react/use-chart-focus.js.map +7 -0
- package/dist/react/use-chart-width.d.ts +12 -0
- package/dist/react/use-chart-width.d.ts.map +1 -0
- package/dist/react/use-chart-width.js +23 -0
- package/dist/react/use-chart-width.js.map +7 -0
- package/dist/react/use-chart-zoom.d.ts +13 -0
- package/dist/react/use-chart-zoom.d.ts.map +1 -0
- package/dist/react/use-chart-zoom.js +52 -0
- package/dist/react/use-chart-zoom.js.map +7 -0
- package/dist/react/use-text-measurer.d.ts +8 -0
- package/dist/react/use-text-measurer.d.ts.map +1 -0
- package/dist/react/use-text-measurer.js +44 -0
- package/dist/react/use-text-measurer.js.map +7 -0
- package/dist/sankey/index.d.ts +2 -0
- package/dist/sankey/index.d.ts.map +1 -0
- package/dist/sankey/index.js +7 -0
- package/dist/sankey/index.js.map +7 -0
- package/dist/sankey/sankey.d.ts +29 -0
- package/dist/sankey/sankey.d.ts.map +1 -0
- package/dist/sankey/sankey.js +127 -0
- package/dist/sankey/sankey.js.map +7 -0
- package/dist/voronoi/index.d.ts +2 -0
- package/dist/voronoi/index.d.ts.map +1 -0
- package/dist/voronoi/index.js +5 -0
- package/dist/voronoi/index.js.map +7 -0
- package/dist/voronoi/voronoi.d.ts +16 -0
- package/dist/voronoi/voronoi.d.ts.map +1 -0
- package/dist/voronoi/voronoi.js +62 -0
- package/dist/voronoi/voronoi.js.map +7 -0
- package/package.json +74 -0
- package/src/brush/brush-layer.tsx +174 -0
- package/src/brush/index.ts +14 -0
- package/src/canvas/canvas-renderer.tsx +56 -0
- package/src/canvas/index.ts +1 -0
- package/src/canvas/paint-scene.ts +152 -0
- package/src/canvas/resolve-color.ts +65 -0
- package/src/contour/density.ts +90 -0
- package/src/contour/index.ts +1 -0
- package/src/core/channel.ts +53 -0
- package/src/core/compile.ts +436 -0
- package/src/core/define-chart.ts +128 -0
- package/src/core/facet.ts +189 -0
- package/src/core/format.ts +163 -0
- package/src/core/guides/axes.ts +283 -0
- package/src/core/marks/area.ts +238 -0
- package/src/core/marks/bar.ts +278 -0
- package/src/core/marks/cell.ts +123 -0
- package/src/core/marks/dot.ts +157 -0
- package/src/core/marks/funnel-layout.ts +113 -0
- package/src/core/marks/funnel.ts +172 -0
- package/src/core/marks/line.ts +206 -0
- package/src/core/marks/polar.ts +568 -0
- package/src/core/marks/rect.ts +156 -0
- package/src/core/marks/rule.ts +104 -0
- package/src/core/marks/shared.ts +134 -0
- package/src/core/marks/stack.ts +42 -0
- package/src/core/marks/stats/difference.ts +205 -0
- package/src/core/marks/stats/distribution.ts +425 -0
- package/src/core/marks/stats/dodge.ts +136 -0
- package/src/core/marks/stats/hexbin.ts +152 -0
- package/src/core/marks/stats/histogram.ts +108 -0
- package/src/core/marks/stats/regression.ts +108 -0
- package/src/core/marks/stats/statistics.ts +153 -0
- package/src/core/marks/stats/waffle.ts +141 -0
- package/src/core/marks/text.ts +97 -0
- package/src/core/motion/tween.ts +131 -0
- package/src/core/nearest.ts +223 -0
- package/src/core/paths.ts +323 -0
- package/src/core/scales/color.ts +51 -0
- package/src/core/scales/continuous.ts +111 -0
- package/src/core/scales/discrete.ts +156 -0
- package/src/core/scales/resolve.ts +158 -0
- package/src/core/scales/time.ts +180 -0
- package/src/core/text.ts +42 -0
- package/src/core/theme.ts +26 -0
- package/src/core/ticks.ts +118 -0
- package/src/core/types.ts +358 -0
- package/src/force/force.ts +200 -0
- package/src/force/index.ts +6 -0
- package/src/geo/geo.ts +346 -0
- package/src/geo/index.ts +10 -0
- package/src/hierarchy/hierarchy.ts +377 -0
- package/src/hierarchy/index.ts +6 -0
- package/src/index.ts +70 -0
- package/src/react/brush-contract.ts +35 -0
- package/src/react/chart-data-table.tsx +98 -0
- package/src/react/chart-legend.tsx +130 -0
- package/src/react/chart-tooltip.tsx +111 -0
- package/src/react/chart.tsx +489 -0
- package/src/react/focus-overlay.tsx +164 -0
- package/src/react/index.ts +9 -0
- package/src/react/messages.ts +49 -0
- package/src/react/renderer.ts +15 -0
- package/src/react/sr-only.ts +14 -0
- package/src/react/svg-renderer.tsx +18 -0
- package/src/react/svg-scene.tsx +146 -0
- package/src/react/use-animated-scene.ts +57 -0
- package/src/react/use-chart-focus.ts +245 -0
- package/src/react/use-chart-width.ts +27 -0
- package/src/react/use-chart-zoom.ts +67 -0
- package/src/react/use-text-measurer.ts +49 -0
- package/src/sankey/index.ts +6 -0
- package/src/sankey/sankey.ts +179 -0
- package/src/voronoi/index.ts +1 -0
- package/src/voronoi/voronoi.ts +84 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { contourDensity } from "d3-contour";
|
|
2
|
+
import { isPlaceable, readChannel } from "#/core/channel.ts";
|
|
3
|
+
import { markId, valueOn } from "#/core/marks/shared.ts";
|
|
4
|
+
import { multiPolygonPath } from "#/core/paths.ts";
|
|
5
|
+
import type {
|
|
6
|
+
ChartAccessor,
|
|
7
|
+
ChartMark,
|
|
8
|
+
ChartValue,
|
|
9
|
+
SceneNode,
|
|
10
|
+
} from "#/core/types.ts";
|
|
11
|
+
|
|
12
|
+
export interface Density2dOptions<TDatum> {
|
|
13
|
+
readonly id?: string;
|
|
14
|
+
readonly x: ChartAccessor<TDatum, ChartValue>;
|
|
15
|
+
readonly y: ChartAccessor<TDatum, ChartValue>;
|
|
16
|
+
/** Smoothing, in pixels. */
|
|
17
|
+
readonly bandwidth?: number;
|
|
18
|
+
/** How many density levels to draw. */
|
|
19
|
+
readonly thresholds?: number;
|
|
20
|
+
readonly fill?: string;
|
|
21
|
+
/** Outline the levels instead of filling them. */
|
|
22
|
+
readonly stroke?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Where points crowd, as nested contours: the shape of a scatter too dense
|
|
27
|
+
* to read. Decorative on its own (the levels are not values a reader can
|
|
28
|
+
* visit), so draw the points too, or give the chart a description.
|
|
29
|
+
*/
|
|
30
|
+
export function density2d<TDatum>(
|
|
31
|
+
data: ReadonlyArray<TDatum>,
|
|
32
|
+
options: Density2dOptions<TDatum>,
|
|
33
|
+
): ChartMark {
|
|
34
|
+
const xs = readChannel(data, options.x);
|
|
35
|
+
const ys = readChannel(data, options.y);
|
|
36
|
+
return {
|
|
37
|
+
id: options.id,
|
|
38
|
+
coordinate: "cartesian",
|
|
39
|
+
channels: {
|
|
40
|
+
x: { values: xs.filter(isPlaceable) },
|
|
41
|
+
y: { values: ys.filter(isPlaceable) },
|
|
42
|
+
},
|
|
43
|
+
render(context) {
|
|
44
|
+
const id = markId(options.id, "density2d", context);
|
|
45
|
+
const { x: xScale, y: yScale } = context.scales;
|
|
46
|
+
if (xScale === undefined || yScale === undefined) {
|
|
47
|
+
return { nodes: [] };
|
|
48
|
+
}
|
|
49
|
+
const { plot } = context;
|
|
50
|
+
const pixels: Array<[number, number]> = [];
|
|
51
|
+
for (const [index, raw] of xs.entries()) {
|
|
52
|
+
const x = valueOn(xScale, raw);
|
|
53
|
+
const y = valueOn(yScale, ys[index]);
|
|
54
|
+
if (x !== undefined && y !== undefined) {
|
|
55
|
+
pixels.push([xScale.center(x) - plot.x, yScale.center(y) - plot.y]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const levels = contourDensity()
|
|
59
|
+
.x((pixel) => pixel[0])
|
|
60
|
+
.y((pixel) => pixel[1])
|
|
61
|
+
.size([
|
|
62
|
+
Math.max(1, Math.round(plot.width)),
|
|
63
|
+
Math.max(1, Math.round(plot.height)),
|
|
64
|
+
])
|
|
65
|
+
.bandwidth(options.bandwidth ?? 20)
|
|
66
|
+
.thresholds(options.thresholds ?? 8)(pixels);
|
|
67
|
+
const color = options.fill ?? context.paletteColor(context.markIndex);
|
|
68
|
+
const nodes: SceneNode[] = levels.map((level, index) => {
|
|
69
|
+
const share = levels.length <= 1 ? 1 : (index + 1) / levels.length;
|
|
70
|
+
return {
|
|
71
|
+
kind: "path",
|
|
72
|
+
key: `${id}:${index}`,
|
|
73
|
+
role: "mark",
|
|
74
|
+
d: multiPolygonPath(level.coordinates as never, plot.x, plot.y),
|
|
75
|
+
paint: options.stroke
|
|
76
|
+
? {
|
|
77
|
+
fill: "none",
|
|
78
|
+
stroke: color,
|
|
79
|
+
strokeWidth: 1,
|
|
80
|
+
strokeOpacity: 0.3 + 0.7 * share,
|
|
81
|
+
}
|
|
82
|
+
: { fill: color, fillOpacity: 0.12 },
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
nodes: nodes.filter((node) => node.kind !== "path" || node.d !== ""),
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type Density2dOptions, density2d } from "#/contour/density.ts";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChartAccessor, ChartValue } from "#/core/types.ts";
|
|
2
|
+
|
|
3
|
+
/** Reads one channel off every datum. A missing field reads as `undefined`, never a throw. */
|
|
4
|
+
export function readChannel<TDatum, TValue>(
|
|
5
|
+
data: ReadonlyArray<TDatum>,
|
|
6
|
+
accessor: ChartAccessor<TDatum, TValue> | undefined,
|
|
7
|
+
): ReadonlyArray<TValue | undefined> {
|
|
8
|
+
if (accessor === undefined) {
|
|
9
|
+
return data.map(() => undefined);
|
|
10
|
+
}
|
|
11
|
+
if (typeof accessor === "function") {
|
|
12
|
+
return data.map((datum, index) => accessor(datum, index));
|
|
13
|
+
}
|
|
14
|
+
return data.map(
|
|
15
|
+
(datum) => (datum as Record<string, unknown>)[accessor] as TValue,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The field name an accessor reads, for an axis or column heading. */
|
|
20
|
+
export function channelLabel<TDatum, TValue>(
|
|
21
|
+
accessor: ChartAccessor<TDatum, TValue> | undefined,
|
|
22
|
+
): string | undefined {
|
|
23
|
+
return typeof accessor === "string" ? accessor : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A value a scale can place: finite numbers, valid dates and strings. */
|
|
27
|
+
export function isPlaceable(value: unknown): value is ChartValue {
|
|
28
|
+
if (typeof value === "number") {
|
|
29
|
+
return Number.isFinite(value);
|
|
30
|
+
}
|
|
31
|
+
if (value instanceof Date) {
|
|
32
|
+
return !Number.isNaN(value.getTime());
|
|
33
|
+
}
|
|
34
|
+
return typeof value === "string";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A number off a channel, or `undefined` for a gap. Numeric strings count. */
|
|
38
|
+
export function numericValue(value: unknown): number | undefined {
|
|
39
|
+
if (typeof value === "number") {
|
|
40
|
+
return Number.isFinite(value) ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function placeableValues(
|
|
50
|
+
values: ReadonlyArray<unknown>,
|
|
51
|
+
): ReadonlyArray<ChartValue> {
|
|
52
|
+
return values.filter(isPlaceable);
|
|
53
|
+
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChartDefinition,
|
|
3
|
+
ChartPositionScaleOptions,
|
|
4
|
+
ChartSpec,
|
|
5
|
+
} from "#/core/define-chart.ts";
|
|
6
|
+
import { dateValueFormatter, formatValue } from "#/core/format.ts";
|
|
7
|
+
import {
|
|
8
|
+
type AxisMeasureInput,
|
|
9
|
+
bottomAxis,
|
|
10
|
+
bottomMargin,
|
|
11
|
+
leftAxis,
|
|
12
|
+
leftMargin,
|
|
13
|
+
rightMargin,
|
|
14
|
+
} from "#/core/guides/axes.ts";
|
|
15
|
+
import {
|
|
16
|
+
ordinalColorScale,
|
|
17
|
+
sequentialColorScale,
|
|
18
|
+
} from "#/core/scales/color.ts";
|
|
19
|
+
import { toNumber } from "#/core/scales/continuous.ts";
|
|
20
|
+
import { categoryKey } from "#/core/scales/discrete.ts";
|
|
21
|
+
import {
|
|
22
|
+
buildScale,
|
|
23
|
+
planScale,
|
|
24
|
+
type ScalePlan,
|
|
25
|
+
} from "#/core/scales/resolve.ts";
|
|
26
|
+
import { estimateTextWidth } from "#/core/text.ts";
|
|
27
|
+
import { DEFAULT_THEME } from "#/core/theme.ts";
|
|
28
|
+
import type {
|
|
29
|
+
ChartChannel,
|
|
30
|
+
ChartColorScale,
|
|
31
|
+
ChartFocusOrder,
|
|
32
|
+
ChartLegendItem,
|
|
33
|
+
ChartMargin,
|
|
34
|
+
ChartMark,
|
|
35
|
+
ChartPoint,
|
|
36
|
+
ChartPositionScale,
|
|
37
|
+
ChartRect,
|
|
38
|
+
ChartScene,
|
|
39
|
+
ChartTextMeasurer,
|
|
40
|
+
ChartTheme,
|
|
41
|
+
ChartValue,
|
|
42
|
+
SceneNode,
|
|
43
|
+
} from "#/core/types.ts";
|
|
44
|
+
|
|
45
|
+
const DEFAULT_LOCALE = "fr-FR";
|
|
46
|
+
const EDGE = 8;
|
|
47
|
+
/** Pixels per tick an axis aims for: about one label every 80 px across, 48 px down. */
|
|
48
|
+
const X_TICK_SPACING = 80;
|
|
49
|
+
const Y_TICK_SPACING = 48;
|
|
50
|
+
const MIN_TICKS = 2;
|
|
51
|
+
|
|
52
|
+
export interface CompileOptions {
|
|
53
|
+
readonly width: number;
|
|
54
|
+
readonly height: number;
|
|
55
|
+
readonly measureText?: ChartTextMeasurer;
|
|
56
|
+
/** Series the legend has switched off. The scales keep their domain, so the axes hold still. */
|
|
57
|
+
readonly hiddenSeries?: ReadonlySet<string>;
|
|
58
|
+
/** The part of a continuous x on show, when zoomed. */
|
|
59
|
+
readonly xDomain?: readonly [number, number];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function distinctBy<T>(
|
|
63
|
+
items: ReadonlyArray<T>,
|
|
64
|
+
keyOf: (item: T) => string,
|
|
65
|
+
): T[] {
|
|
66
|
+
const seen = new Set<string>();
|
|
67
|
+
return items.filter((item) => {
|
|
68
|
+
const key = keyOf(item);
|
|
69
|
+
if (seen.has(key)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
seen.add(key);
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const RAMP_STOPS = 5;
|
|
78
|
+
|
|
79
|
+
function rampOf(
|
|
80
|
+
color: ChartColorScale,
|
|
81
|
+
format: (value: ChartValue) => string,
|
|
82
|
+
): ChartScene["colorRamp"] {
|
|
83
|
+
const [low, high] = color.domain.map(toNumber);
|
|
84
|
+
return {
|
|
85
|
+
stops: Array.from({ length: RAMP_STOPS }, (_unused, index) =>
|
|
86
|
+
color.map(low + ((high - low) * index) / (RAMP_STOPS - 1)),
|
|
87
|
+
),
|
|
88
|
+
low: format(low),
|
|
89
|
+
high: format(high),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isMark(
|
|
94
|
+
mark: ChartMark | false | null | undefined,
|
|
95
|
+
): mark is ChartMark {
|
|
96
|
+
return Boolean(mark);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function channelsOf(
|
|
100
|
+
marks: ReadonlyArray<ChartMark>,
|
|
101
|
+
name: "x" | "y" | "color",
|
|
102
|
+
): ReadonlyArray<ChartChannel> {
|
|
103
|
+
return marks.flatMap((mark) => {
|
|
104
|
+
const channel = mark.channels[name];
|
|
105
|
+
return channel === undefined ? [] : [channel];
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function tickCount(length: number, spacing: number): number {
|
|
110
|
+
return Math.max(MIN_TICKS, Math.floor(length / spacing));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function colorScaleOf(
|
|
114
|
+
spec: ChartSpec,
|
|
115
|
+
channels: ReadonlyArray<ChartChannel>,
|
|
116
|
+
theme: ChartTheme,
|
|
117
|
+
): ChartColorScale | undefined {
|
|
118
|
+
if (channels.length === 0) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const options = spec.color;
|
|
122
|
+
const values = channels.flatMap((channel) => channel.values);
|
|
123
|
+
const sequential =
|
|
124
|
+
options?.type === "sequential" ||
|
|
125
|
+
(options?.type === undefined &&
|
|
126
|
+
values.length > 0 &&
|
|
127
|
+
values.every((value) => typeof value === "number"));
|
|
128
|
+
if (sequential) {
|
|
129
|
+
const numbers = (options?.domain ?? values).map(toNumber);
|
|
130
|
+
const [from = theme.background, to = theme.palette[0]] = options?.range ?? [
|
|
131
|
+
`color-mix(in oklab, ${theme.palette[0]} 12%, ${theme.background})`,
|
|
132
|
+
theme.palette[0],
|
|
133
|
+
];
|
|
134
|
+
return sequentialColorScale(
|
|
135
|
+
[Math.min(...numbers), Math.max(...numbers)],
|
|
136
|
+
[from, to],
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const domain = distinctBy(options?.domain ?? values, categoryKey);
|
|
140
|
+
return ordinalColorScale(domain, options?.range ?? theme.palette);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withoutHidden(
|
|
144
|
+
nodes: ReadonlyArray<SceneNode>,
|
|
145
|
+
hidden: ReadonlySet<string>,
|
|
146
|
+
): ReadonlyArray<SceneNode> {
|
|
147
|
+
if (hidden.size === 0) {
|
|
148
|
+
return nodes;
|
|
149
|
+
}
|
|
150
|
+
return nodes
|
|
151
|
+
.filter((node) => node.series === undefined || !hidden.has(node.series))
|
|
152
|
+
.map((node) =>
|
|
153
|
+
node.kind === "group"
|
|
154
|
+
? { ...node, children: withoutHidden(node.children, hidden) }
|
|
155
|
+
: node,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function focusOrderOf(
|
|
160
|
+
spec: ChartSpec,
|
|
161
|
+
marks: ReadonlyArray<ChartMark>,
|
|
162
|
+
x: ChartPositionScale | undefined,
|
|
163
|
+
y: ChartPositionScale | undefined,
|
|
164
|
+
): ChartFocusOrder {
|
|
165
|
+
if (spec.focus !== undefined) {
|
|
166
|
+
return spec.focus;
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
marks.some(
|
|
170
|
+
(mark) => mark.coordinate === "frame" || mark.focusOrder === "point",
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
return "point";
|
|
174
|
+
}
|
|
175
|
+
const yDiscrete = y?.kind === "band" || y?.kind === "point";
|
|
176
|
+
const xDiscrete = x?.kind === "band" || x?.kind === "point";
|
|
177
|
+
return yDiscrete && !xDiscrete ? "y" : "x";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function marginFor(
|
|
181
|
+
spec: ChartSpec,
|
|
182
|
+
width: number,
|
|
183
|
+
xInput: AxisMeasureInput | undefined,
|
|
184
|
+
yInput: AxisMeasureInput | undefined,
|
|
185
|
+
fontSize: number,
|
|
186
|
+
): ChartMargin {
|
|
187
|
+
const computed: ChartMargin = {
|
|
188
|
+
top: yInput === undefined ? EDGE : Math.max(EDGE, fontSize / 2 + 2),
|
|
189
|
+
right: xInput === undefined ? EDGE : rightMargin(xInput),
|
|
190
|
+
bottom: xInput === undefined ? EDGE : bottomMargin(xInput),
|
|
191
|
+
left: yInput === undefined ? EDGE : leftMargin(yInput, width),
|
|
192
|
+
};
|
|
193
|
+
return { ...computed, ...spec.margin };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Definition and size in, scene out. Pure: the same inputs give the same
|
|
198
|
+
* scene, on the server and in the browser, which is what lets the SVG render
|
|
199
|
+
* on the server hydrate without a mismatch.
|
|
200
|
+
*/
|
|
201
|
+
export function compileChart(
|
|
202
|
+
definition: ChartDefinition,
|
|
203
|
+
options: CompileOptions,
|
|
204
|
+
): ChartScene {
|
|
205
|
+
const spec = definition.build({
|
|
206
|
+
width: options.width,
|
|
207
|
+
height: options.height,
|
|
208
|
+
});
|
|
209
|
+
return spec.facet === undefined
|
|
210
|
+
? compileSpec(spec, options)
|
|
211
|
+
: spec.facet.compile(spec, { ...options, compileCell: compileSpec });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** One chart's scene from its spec: what `compileChart` does once per facet cell. */
|
|
215
|
+
export function compileSpec(
|
|
216
|
+
spec: ChartSpec,
|
|
217
|
+
options: CompileOptions,
|
|
218
|
+
): ChartScene {
|
|
219
|
+
const { width, height } = options;
|
|
220
|
+
const measureText = options.measureText ?? estimateTextWidth;
|
|
221
|
+
const hidden = options.hiddenSeries ?? new Set<string>();
|
|
222
|
+
const locale = spec.locale ?? DEFAULT_LOCALE;
|
|
223
|
+
const theme: ChartTheme = { ...DEFAULT_THEME, ...spec.theme };
|
|
224
|
+
const marks = (spec.marks ?? []).filter(isMark);
|
|
225
|
+
const cartesian = marks.filter((mark) => mark.coordinate === "cartesian");
|
|
226
|
+
|
|
227
|
+
const plannedX = planScale(channelsOf(cartesian, "x"), spec.x, "x");
|
|
228
|
+
// A zoomed chart shows part of a continuous x: the viewport replaces the
|
|
229
|
+
// domain, and the marks are clipped to the plot.
|
|
230
|
+
const zoomed =
|
|
231
|
+
options.xDomain !== undefined &&
|
|
232
|
+
plannedX !== undefined &&
|
|
233
|
+
plannedX.kind !== "band" &&
|
|
234
|
+
plannedX.kind !== "point";
|
|
235
|
+
const xPlan =
|
|
236
|
+
zoomed && plannedX && options.xDomain
|
|
237
|
+
? { ...plannedX, domain: options.xDomain }
|
|
238
|
+
: plannedX;
|
|
239
|
+
const yPlan = planScale(channelsOf(cartesian, "y"), spec.y, "y");
|
|
240
|
+
|
|
241
|
+
// First pass: scales over the whole chart, only to measure tick labels.
|
|
242
|
+
function provisional(
|
|
243
|
+
plan: ScalePlan | undefined,
|
|
244
|
+
axisOptions: ChartPositionScaleOptions | undefined,
|
|
245
|
+
range: readonly [number, number],
|
|
246
|
+
count: number,
|
|
247
|
+
): AxisMeasureInput | undefined {
|
|
248
|
+
if (plan === undefined) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
scale: buildScale(plan, axisOptions, range, locale, count),
|
|
253
|
+
options: axisOptions,
|
|
254
|
+
title: axisOptions?.label,
|
|
255
|
+
theme,
|
|
256
|
+
measureText,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const yDiscrete = yPlan?.kind === "band" || yPlan?.kind === "point";
|
|
260
|
+
const yRange = (top: number, bottom: number): readonly [number, number] =>
|
|
261
|
+
yDiscrete ? [top, bottom] : [bottom, top];
|
|
262
|
+
|
|
263
|
+
const margin = marginFor(
|
|
264
|
+
spec,
|
|
265
|
+
width,
|
|
266
|
+
provisional(xPlan, spec.x, [0, width], tickCount(width, X_TICK_SPACING)),
|
|
267
|
+
provisional(
|
|
268
|
+
yPlan,
|
|
269
|
+
spec.y,
|
|
270
|
+
yRange(0, height),
|
|
271
|
+
tickCount(height, Y_TICK_SPACING),
|
|
272
|
+
),
|
|
273
|
+
theme.fontSize,
|
|
274
|
+
);
|
|
275
|
+
const plot: ChartRect = {
|
|
276
|
+
x: margin.left,
|
|
277
|
+
y: margin.top,
|
|
278
|
+
width: Math.max(0, width - margin.left - margin.right),
|
|
279
|
+
height: Math.max(0, height - margin.top - margin.bottom),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
function insetOf(name: "x" | "y"): number {
|
|
283
|
+
return Math.max(
|
|
284
|
+
0,
|
|
285
|
+
...channelsOf(cartesian, name).map((channel) => channel.inset ?? 0),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const xInset = insetOf("x");
|
|
289
|
+
const yInset = insetOf("y");
|
|
290
|
+
const x =
|
|
291
|
+
xPlan &&
|
|
292
|
+
buildScale(
|
|
293
|
+
xPlan,
|
|
294
|
+
spec.x,
|
|
295
|
+
[plot.x + xInset, plot.x + plot.width - xInset],
|
|
296
|
+
locale,
|
|
297
|
+
tickCount(plot.width, X_TICK_SPACING),
|
|
298
|
+
);
|
|
299
|
+
const y =
|
|
300
|
+
yPlan &&
|
|
301
|
+
buildScale(
|
|
302
|
+
yPlan,
|
|
303
|
+
spec.y,
|
|
304
|
+
yRange(plot.y + yInset, plot.y + plot.height - yInset),
|
|
305
|
+
locale,
|
|
306
|
+
tickCount(plot.height, Y_TICK_SPACING),
|
|
307
|
+
);
|
|
308
|
+
const color = colorScaleOf(spec, channelsOf(marks, "color"), theme);
|
|
309
|
+
const scales = { x, y, color };
|
|
310
|
+
|
|
311
|
+
function channelFormatter(
|
|
312
|
+
channels: ReadonlyArray<ChartChannel>,
|
|
313
|
+
custom: ((value: ChartValue) => string) | undefined,
|
|
314
|
+
): (value: ChartValue) => string {
|
|
315
|
+
if (custom !== undefined) {
|
|
316
|
+
return custom;
|
|
317
|
+
}
|
|
318
|
+
const dates = dateValueFormatter(
|
|
319
|
+
channels.flatMap((channel) => channel.values),
|
|
320
|
+
locale,
|
|
321
|
+
);
|
|
322
|
+
return (value) =>
|
|
323
|
+
dates !== undefined && value instanceof Date
|
|
324
|
+
? dates(value)
|
|
325
|
+
: formatValue(value, locale);
|
|
326
|
+
}
|
|
327
|
+
const formatX = channelFormatter(channelsOf(cartesian, "x"), spec.x?.format);
|
|
328
|
+
const formatY = channelFormatter(channelsOf(cartesian, "y"), spec.y?.format);
|
|
329
|
+
const seriesLabel = (value: ChartValue) =>
|
|
330
|
+
spec.color?.labels?.[categoryKey(value)] ?? formatValue(value, locale);
|
|
331
|
+
|
|
332
|
+
const grid: SceneNode[] = [];
|
|
333
|
+
const guides: SceneNode[] = [];
|
|
334
|
+
const xContinuous =
|
|
335
|
+
x !== undefined && x.kind !== "band" && x.kind !== "point";
|
|
336
|
+
const yContinuous =
|
|
337
|
+
y !== undefined && y.kind !== "band" && y.kind !== "point";
|
|
338
|
+
if (x !== undefined) {
|
|
339
|
+
const axis = bottomAxis(
|
|
340
|
+
{ scale: x, options: spec.x, title: spec.x?.label, theme, measureText },
|
|
341
|
+
plot,
|
|
342
|
+
spec.x?.grid ?? (xContinuous && !yContinuous),
|
|
343
|
+
);
|
|
344
|
+
grid.push(...axis.grid);
|
|
345
|
+
guides.push(...axis.labels);
|
|
346
|
+
}
|
|
347
|
+
if (y !== undefined) {
|
|
348
|
+
const axis = leftAxis(
|
|
349
|
+
{ scale: y, options: spec.y, title: spec.y?.label, theme, measureText },
|
|
350
|
+
plot,
|
|
351
|
+
spec.y?.grid ?? yContinuous,
|
|
352
|
+
width,
|
|
353
|
+
);
|
|
354
|
+
grid.push(...axis.grid);
|
|
355
|
+
guides.push(...axis.labels);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const markNodes: SceneNode[] = [];
|
|
359
|
+
const points: ChartPoint[] = [];
|
|
360
|
+
const legend: ChartLegendItem[] = [];
|
|
361
|
+
const paletteColor = (index: number) =>
|
|
362
|
+
theme.palette[index % theme.palette.length];
|
|
363
|
+
const projection = spec.projection?.fit(plot);
|
|
364
|
+
for (const [markIndex, mark] of marks.entries()) {
|
|
365
|
+
const rendered = mark.render({
|
|
366
|
+
scales,
|
|
367
|
+
plot,
|
|
368
|
+
theme,
|
|
369
|
+
measureText,
|
|
370
|
+
locale,
|
|
371
|
+
paletteColor,
|
|
372
|
+
colorOf: (value) => color?.map(value) ?? paletteColor(markIndex),
|
|
373
|
+
seriesLabel,
|
|
374
|
+
formatX,
|
|
375
|
+
formatY,
|
|
376
|
+
markIndex,
|
|
377
|
+
projection,
|
|
378
|
+
});
|
|
379
|
+
markNodes.push(...rendered.nodes);
|
|
380
|
+
points.push(
|
|
381
|
+
...(rendered.points ?? []).filter(
|
|
382
|
+
(point) => point.series === undefined || !hidden.has(point.series),
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
legend.push(...(rendered.legend ?? []));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const colored = marks.filter((mark) => mark.channels.color !== undefined);
|
|
389
|
+
const legendWanted =
|
|
390
|
+
spec.color?.legend ?? colored.some((mark) => mark.colorLegend !== false);
|
|
391
|
+
if (color?.kind === "ordinal" && legendWanted) {
|
|
392
|
+
for (const value of color.domain) {
|
|
393
|
+
legend.push({
|
|
394
|
+
key: categoryKey(value),
|
|
395
|
+
label: seriesLabel(value),
|
|
396
|
+
color: color.map(value),
|
|
397
|
+
shape: "square",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
width,
|
|
404
|
+
height,
|
|
405
|
+
plot,
|
|
406
|
+
nodes: [
|
|
407
|
+
{ kind: "group", key: "grid", role: "grid", children: grid },
|
|
408
|
+
{
|
|
409
|
+
kind: "group",
|
|
410
|
+
key: "marks",
|
|
411
|
+
role: "marks",
|
|
412
|
+
clip: zoomed ? plot : undefined,
|
|
413
|
+
children: withoutHidden(markNodes, hidden),
|
|
414
|
+
},
|
|
415
|
+
{ kind: "group", key: "axes", role: "axes", children: guides },
|
|
416
|
+
],
|
|
417
|
+
points,
|
|
418
|
+
legend: distinctBy(legend, (item) => item.key),
|
|
419
|
+
focusOrder: focusOrderOf(spec, marks, x, y),
|
|
420
|
+
scales,
|
|
421
|
+
xLabel:
|
|
422
|
+
xPlan?.label ??
|
|
423
|
+
marks.find((mark) => mark.titles?.category)?.titles?.category ??
|
|
424
|
+
"x",
|
|
425
|
+
yLabel:
|
|
426
|
+
yPlan?.label ??
|
|
427
|
+
marks.find((mark) => mark.titles?.value)?.titles?.value ??
|
|
428
|
+
"y",
|
|
429
|
+
theme,
|
|
430
|
+
locale,
|
|
431
|
+
colorRamp:
|
|
432
|
+
color?.kind === "sequential" && spec.color?.legend !== false
|
|
433
|
+
? rampOf(color, (value) => formatValue(value, locale))
|
|
434
|
+
: undefined,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChartFocusOrder,
|
|
3
|
+
ChartMargin,
|
|
4
|
+
ChartMark,
|
|
5
|
+
ChartPositionScaleKind,
|
|
6
|
+
ChartProjection,
|
|
7
|
+
ChartScene,
|
|
8
|
+
ChartTextMeasurer,
|
|
9
|
+
ChartTheme,
|
|
10
|
+
ChartValue,
|
|
11
|
+
} from "#/core/types.ts";
|
|
12
|
+
|
|
13
|
+
export interface ChartAxisOptions {
|
|
14
|
+
/** Axis title, data-table column heading and tooltip heading. */
|
|
15
|
+
readonly label?: string;
|
|
16
|
+
/** `false` hides the axis; the scale still places the marks. */
|
|
17
|
+
readonly axis?: boolean;
|
|
18
|
+
readonly grid?: boolean;
|
|
19
|
+
/** A tick count to aim for, or the exact values to tick. */
|
|
20
|
+
readonly ticks?: number | ReadonlyArray<ChartValue>;
|
|
21
|
+
readonly tickFormat?: (value: ChartValue) => string;
|
|
22
|
+
/** How the tooltip, live region and data table write a value of this channel. */
|
|
23
|
+
readonly format?: (value: ChartValue) => string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChartPositionScaleOptions extends ChartAxisOptions {
|
|
27
|
+
readonly type?: ChartPositionScaleKind;
|
|
28
|
+
readonly domain?: ReadonlyArray<ChartValue>;
|
|
29
|
+
/** Widen a continuous domain to round ticks. On by default for y. */
|
|
30
|
+
readonly nice?: boolean;
|
|
31
|
+
/** Keep zero on a continuous domain even when no mark asks for it. */
|
|
32
|
+
readonly zero?: boolean;
|
|
33
|
+
readonly reverse?: boolean;
|
|
34
|
+
readonly paddingInner?: number;
|
|
35
|
+
readonly paddingOuter?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ChartColorOptions {
|
|
39
|
+
readonly type?: "ordinal" | "sequential";
|
|
40
|
+
readonly domain?: ReadonlyArray<ChartValue>;
|
|
41
|
+
/** Palette for an ordinal scale, `[from, to]` for a sequential one. */
|
|
42
|
+
readonly range?: ReadonlyArray<string>;
|
|
43
|
+
readonly legend?: boolean;
|
|
44
|
+
/** Display names of the series, by value. */
|
|
45
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Small multiples: the same chart once per value, on a grid, every cell on
|
|
50
|
+
* the same scales so the cells compare at a glance.
|
|
51
|
+
*/
|
|
52
|
+
export interface ChartFacetOptions {
|
|
53
|
+
readonly values: ReadonlyArray<ChartValue>;
|
|
54
|
+
/** The marks of one cell. */
|
|
55
|
+
readonly marks: (
|
|
56
|
+
value: ChartValue,
|
|
57
|
+
index: number,
|
|
58
|
+
) => ReadonlyArray<ChartMark | false | null | undefined>;
|
|
59
|
+
readonly label?: (value: ChartValue) => string;
|
|
60
|
+
/** Cells per row. Defaults to three, or fewer when there are fewer values. */
|
|
61
|
+
readonly columns?: number;
|
|
62
|
+
/** Pixels between two cells. */
|
|
63
|
+
readonly gap?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What `facet(…)` returns: the options, and the compiler that lays the cells
|
|
68
|
+
* out. Carried by the spec rather than imported by the core, so a chart
|
|
69
|
+
* without facets never ships the facet code.
|
|
70
|
+
*/
|
|
71
|
+
export interface ChartFacet {
|
|
72
|
+
readonly options: ChartFacetOptions;
|
|
73
|
+
readonly compile: (
|
|
74
|
+
spec: ChartSpec,
|
|
75
|
+
options: ChartFacetCompileOptions,
|
|
76
|
+
) => ChartScene;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The compile options a facet compiler is handed, plus the per-cell compiler. */
|
|
80
|
+
export interface ChartFacetCompileOptions {
|
|
81
|
+
readonly width: number;
|
|
82
|
+
readonly height: number;
|
|
83
|
+
readonly measureText?: ChartTextMeasurer;
|
|
84
|
+
readonly hiddenSeries?: ReadonlySet<string>;
|
|
85
|
+
readonly xDomain?: readonly [number, number];
|
|
86
|
+
readonly compileCell: (
|
|
87
|
+
spec: ChartSpec,
|
|
88
|
+
options: Omit<ChartFacetCompileOptions, "compileCell">,
|
|
89
|
+
) => ChartScene;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ChartSpec {
|
|
93
|
+
/** Falsy entries are skipped, so a mark can be switched on with `&&`. Unused with `facet`. */
|
|
94
|
+
readonly marks?: ReadonlyArray<ChartMark | false | null | undefined>;
|
|
95
|
+
readonly x?: ChartPositionScaleOptions;
|
|
96
|
+
readonly y?: ChartPositionScaleOptions;
|
|
97
|
+
readonly color?: ChartColorOptions;
|
|
98
|
+
/** Overrides the margins the axes would reserve, side by side. */
|
|
99
|
+
readonly margin?: Partial<ChartMargin>;
|
|
100
|
+
readonly focus?: ChartFocusOrder;
|
|
101
|
+
/** Small multiples, from `facet(…)`. With it, `marks` is ignored: each cell takes its own. */
|
|
102
|
+
readonly facet?: ChartFacet;
|
|
103
|
+
/** For maps: how longitude and latitude become pixels. */
|
|
104
|
+
readonly projection?: ChartProjection;
|
|
105
|
+
readonly locale?: string;
|
|
106
|
+
readonly theme?: Partial<ChartTheme>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ChartBuildContext {
|
|
110
|
+
readonly width: number;
|
|
111
|
+
readonly height: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ChartDefinition {
|
|
115
|
+
readonly build: (context: ChartBuildContext) => ChartSpec;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A chart as data. Pass the spec when it does not depend on the size, or a
|
|
120
|
+
* builder when it does (fewer ticks on a phone, a different layout below some
|
|
121
|
+
* width). Build it in `useMemo`: the definition's identity is what tells the
|
|
122
|
+
* renderer the data changed.
|
|
123
|
+
*/
|
|
124
|
+
export function defineChart(
|
|
125
|
+
spec: ChartSpec | ((context: ChartBuildContext) => ChartSpec),
|
|
126
|
+
): ChartDefinition {
|
|
127
|
+
return { build: typeof spec === "function" ? spec : () => spec };
|
|
128
|
+
}
|