@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,131 @@
|
|
|
1
|
+
import type { ChartPaint, ChartScene, SceneNode } from "#/core/types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A scene part of the way from one scene to the next, keyed by node: what an
|
|
5
|
+
* animated update draws on each frame. It lives in the core so the SVG and
|
|
6
|
+
* Canvas renderers animate identically, and never feeds hit-testing or
|
|
7
|
+
* focus: those always read the scene being animated towards.
|
|
8
|
+
*
|
|
9
|
+
* A node present on both sides moves; a new one fades in; one that left is
|
|
10
|
+
* gone at once (fading out a bar the data no longer has would show a value
|
|
11
|
+
* that is not true any more). A path tweens point by point when both shapes
|
|
12
|
+
* share their commands, and swaps at the end when they do not.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const NUMBER = /-?\d*\.?\d+(?:e[-+]?\d+)?/gi;
|
|
16
|
+
|
|
17
|
+
function mix(from: number, to: number, t: number): number {
|
|
18
|
+
return from + (to - from) * t;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Two paths can tween when only their numbers differ. */
|
|
22
|
+
function tweenPath(from: string, to: string, t: number): string {
|
|
23
|
+
const fromNumbers = from.match(NUMBER);
|
|
24
|
+
const toNumbers = to.match(NUMBER);
|
|
25
|
+
if (
|
|
26
|
+
fromNumbers === null ||
|
|
27
|
+
toNumbers === null ||
|
|
28
|
+
fromNumbers.length !== toNumbers.length ||
|
|
29
|
+
from.replace(NUMBER, "#") !== to.replace(NUMBER, "#")
|
|
30
|
+
) {
|
|
31
|
+
return t < 1 ? from : to;
|
|
32
|
+
}
|
|
33
|
+
let index = 0;
|
|
34
|
+
return to.replace(NUMBER, () => {
|
|
35
|
+
const value = mix(Number(fromNumbers[index]), Number(toNumbers[index]), t);
|
|
36
|
+
index += 1;
|
|
37
|
+
return String(Math.round(value * 100) / 100);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fadeIn<TPaint extends ChartPaint>(paint: TPaint, t: number): TPaint {
|
|
42
|
+
return { ...paint, opacity: (paint.opacity ?? 1) * t };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tweenNode(
|
|
46
|
+
from: SceneNode | undefined,
|
|
47
|
+
to: SceneNode,
|
|
48
|
+
t: number,
|
|
49
|
+
): SceneNode {
|
|
50
|
+
if (from === undefined || from.kind !== to.kind) {
|
|
51
|
+
return to.kind === "group"
|
|
52
|
+
? {
|
|
53
|
+
...to,
|
|
54
|
+
children: to.children.map((child) => tweenNode(undefined, child, t)),
|
|
55
|
+
}
|
|
56
|
+
: { ...to, paint: fadeIn(to.paint, t) };
|
|
57
|
+
}
|
|
58
|
+
switch (to.kind) {
|
|
59
|
+
case "group": {
|
|
60
|
+
const before = new Map(
|
|
61
|
+
(from as typeof to).children.map((child) => [child.key, child]),
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
...to,
|
|
65
|
+
children: to.children.map((child) =>
|
|
66
|
+
tweenNode(before.get(child.key), child, t),
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case "rect": {
|
|
71
|
+
const previous = from as typeof to;
|
|
72
|
+
return {
|
|
73
|
+
...to,
|
|
74
|
+
x: mix(previous.x, to.x, t),
|
|
75
|
+
y: mix(previous.y, to.y, t),
|
|
76
|
+
width: mix(previous.width, to.width, t),
|
|
77
|
+
height: mix(previous.height, to.height, t),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case "circle": {
|
|
81
|
+
const previous = from as typeof to;
|
|
82
|
+
return {
|
|
83
|
+
...to,
|
|
84
|
+
cx: mix(previous.cx, to.cx, t),
|
|
85
|
+
cy: mix(previous.cy, to.cy, t),
|
|
86
|
+
r: mix(previous.r, to.r, t),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case "line": {
|
|
90
|
+
const previous = from as typeof to;
|
|
91
|
+
return {
|
|
92
|
+
...to,
|
|
93
|
+
x1: mix(previous.x1, to.x1, t),
|
|
94
|
+
y1: mix(previous.y1, to.y1, t),
|
|
95
|
+
x2: mix(previous.x2, to.x2, t),
|
|
96
|
+
y2: mix(previous.y2, to.y2, t),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
case "text": {
|
|
100
|
+
const previous = from as typeof to;
|
|
101
|
+
return {
|
|
102
|
+
...to,
|
|
103
|
+
x: mix(previous.x, to.x, t),
|
|
104
|
+
y: mix(previous.y, to.y, t),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
case "path":
|
|
108
|
+
return { ...to, d: tweenPath((from as typeof to).d, to.d, t) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `to`, `t` of the way from `from` (0 is `from`, 1 is `to`). */
|
|
113
|
+
export function tweenScene(
|
|
114
|
+
from: ChartScene,
|
|
115
|
+
to: ChartScene,
|
|
116
|
+
t: number,
|
|
117
|
+
): ChartScene {
|
|
118
|
+
if (t >= 1) {
|
|
119
|
+
return to;
|
|
120
|
+
}
|
|
121
|
+
const before = new Map(from.nodes.map((node) => [node.key, node]));
|
|
122
|
+
return {
|
|
123
|
+
...to,
|
|
124
|
+
nodes: to.nodes.map((node) => tweenNode(before.get(node.key), node, t)),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Fast out, slow in: the motion settles rather than stops. */
|
|
129
|
+
export function easeOutCubic(t: number): number {
|
|
130
|
+
return 1 - (1 - t) ** 3;
|
|
131
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { categoryKey } from "#/core/scales/discrete.ts";
|
|
2
|
+
import type {
|
|
3
|
+
ChartFocusOrder,
|
|
4
|
+
ChartHitShape,
|
|
5
|
+
ChartPoint,
|
|
6
|
+
ChartScene,
|
|
7
|
+
ChartValue,
|
|
8
|
+
} from "#/core/types.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Focus and hit-testing, on the scene only, so SVG and Canvas behave the same.
|
|
12
|
+
* A stop is what one arrow-key press moves to: one point, or every point that
|
|
13
|
+
* shares a position (the column of a multi-series chart).
|
|
14
|
+
*/
|
|
15
|
+
export interface ChartFocusStop {
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly points: ReadonlyArray<ChartPoint>;
|
|
18
|
+
/** Where the stop sits along its axis, in chart pixels. */
|
|
19
|
+
readonly at: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** How far from a dot the pointer may be and still hover it, in pixels. */
|
|
23
|
+
const POINT_REACH = 24;
|
|
24
|
+
|
|
25
|
+
function positionOf(
|
|
26
|
+
point: ChartPoint,
|
|
27
|
+
order: ChartFocusOrder,
|
|
28
|
+
): ChartValue | undefined {
|
|
29
|
+
return order === "y" ? point.yValue : point.xValue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function focusStops(scene: ChartScene): ReadonlyArray<ChartFocusStop> {
|
|
33
|
+
const order = scene.focusOrder;
|
|
34
|
+
if (order === "point") {
|
|
35
|
+
return scene.points.map((point) => ({
|
|
36
|
+
key: point.key,
|
|
37
|
+
points: [point],
|
|
38
|
+
at: point.x,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
const stops = new Map<
|
|
42
|
+
string,
|
|
43
|
+
{ key: string; points: ChartPoint[]; at: number }
|
|
44
|
+
>();
|
|
45
|
+
for (const point of scene.points) {
|
|
46
|
+
const position = positionOf(point, order);
|
|
47
|
+
// On a faceted chart a column is a column of one cell, never across cells.
|
|
48
|
+
const within = point.facet === undefined ? "" : `${point.facet}|`;
|
|
49
|
+
const key = `${within}${
|
|
50
|
+
position === undefined
|
|
51
|
+
? `px:${Math.round(order === "y" ? point.y : point.x)}`
|
|
52
|
+
: categoryKey(position)
|
|
53
|
+
}`;
|
|
54
|
+
const stop = stops.get(key);
|
|
55
|
+
if (stop === undefined) {
|
|
56
|
+
stops.set(key, {
|
|
57
|
+
key,
|
|
58
|
+
points: [point],
|
|
59
|
+
at: order === "y" ? point.y : point.x,
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
stop.points.push(point);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const cellOrder = new Map(
|
|
66
|
+
(scene.cells ?? []).map((cell, index) => [cell.key, index]),
|
|
67
|
+
);
|
|
68
|
+
const cellOf = (stop: { points: ChartPoint[] }) =>
|
|
69
|
+
cellOrder.get(stop.points[0].facet ?? "") ?? 0;
|
|
70
|
+
return [...stops.values()].sort(
|
|
71
|
+
(left, right) => cellOf(left) - cellOf(right) || left.at - right.at,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Degrees clockwise from twelve o'clock, in `[0, 360)`. */
|
|
76
|
+
function bearing(dx: number, dy: number): number {
|
|
77
|
+
const degrees = (Math.atan2(dx, -dy) * 180) / Math.PI;
|
|
78
|
+
return degrees < 0 ? degrees + 360 : degrees;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function contains(hit: ChartHitShape, x: number, y: number): boolean {
|
|
82
|
+
if (hit.kind === "rect") {
|
|
83
|
+
const { rect } = hit;
|
|
84
|
+
return (
|
|
85
|
+
x >= rect.x &&
|
|
86
|
+
x <= rect.x + rect.width &&
|
|
87
|
+
y >= rect.y &&
|
|
88
|
+
y <= rect.y + rect.height
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const dx = x - hit.cx;
|
|
92
|
+
const dy = y - hit.cy;
|
|
93
|
+
const radius = Math.hypot(dx, dy);
|
|
94
|
+
if (radius < hit.innerRadius || radius > hit.outerRadius) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const angle = bearing(dx, dy);
|
|
98
|
+
const start = ((hit.startAngle % 360) + 360) % 360;
|
|
99
|
+
const sweep = hit.endAngle - hit.startAngle;
|
|
100
|
+
const offset = (angle - start + 360) % 360;
|
|
101
|
+
return sweep >= 360 || offset <= sweep;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hitArea(hit: ChartHitShape): number {
|
|
105
|
+
if (hit.kind === "rect") {
|
|
106
|
+
return hit.rect.width * hit.rect.height;
|
|
107
|
+
}
|
|
108
|
+
const sweep = Math.min(360, hit.endAngle - hit.startAngle) / 360;
|
|
109
|
+
return Math.PI * (hit.outerRadius ** 2 - hit.innerRadius ** 2) * sweep;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ChartHit {
|
|
113
|
+
readonly stop: number;
|
|
114
|
+
readonly point: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pickInStop(
|
|
118
|
+
stop: ChartFocusStop,
|
|
119
|
+
x: number,
|
|
120
|
+
y: number,
|
|
121
|
+
order: ChartFocusOrder,
|
|
122
|
+
): number {
|
|
123
|
+
const inside = stop.points.findIndex(
|
|
124
|
+
(point) => point.hit !== undefined && contains(point.hit, x, y),
|
|
125
|
+
);
|
|
126
|
+
if (inside !== -1) {
|
|
127
|
+
return inside;
|
|
128
|
+
}
|
|
129
|
+
let best = 0;
|
|
130
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
131
|
+
for (const [index, point] of stop.points.entries()) {
|
|
132
|
+
const distance =
|
|
133
|
+
order === "y" ? Math.abs(point.x - x) : Math.abs(point.y - y);
|
|
134
|
+
if (distance < bestDistance) {
|
|
135
|
+
best = index;
|
|
136
|
+
bestDistance = distance;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return best;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The stop and point under the pointer. Along a position axis the nearest
|
|
144
|
+
* column wins wherever the pointer is in the plot, the way a reader scrubs a
|
|
145
|
+
* time series; a scatter, pie or map needs the pointer on or near the shape.
|
|
146
|
+
*/
|
|
147
|
+
export function findNearest(
|
|
148
|
+
scene: ChartScene,
|
|
149
|
+
stops: ReadonlyArray<ChartFocusStop>,
|
|
150
|
+
x: number,
|
|
151
|
+
y: number,
|
|
152
|
+
): ChartHit | null {
|
|
153
|
+
if (stops.length === 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const order = scene.focusOrder;
|
|
157
|
+
if (order === "point") {
|
|
158
|
+
// Shapes can overlap (a dot over a region, regions' bounding boxes):
|
|
159
|
+
// the smallest one under the pointer is the one meant.
|
|
160
|
+
let inside = -1;
|
|
161
|
+
let insideArea = Number.POSITIVE_INFINITY;
|
|
162
|
+
for (const [index, stop] of stops.entries()) {
|
|
163
|
+
const hit = stop.points[0].hit;
|
|
164
|
+
if (hit !== undefined && contains(hit, x, y)) {
|
|
165
|
+
const area = hitArea(hit);
|
|
166
|
+
if (area < insideArea) {
|
|
167
|
+
inside = index;
|
|
168
|
+
insideArea = area;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (inside !== -1) {
|
|
173
|
+
return { stop: inside, point: 0 };
|
|
174
|
+
}
|
|
175
|
+
let best = -1;
|
|
176
|
+
let bestDistance = POINT_REACH;
|
|
177
|
+
for (const [index, stop] of stops.entries()) {
|
|
178
|
+
const point = stop.points[0];
|
|
179
|
+
if (point.hit?.kind === "arc") {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const distance = Math.hypot(point.x - x, point.y - y);
|
|
183
|
+
if (distance < bestDistance) {
|
|
184
|
+
best = index;
|
|
185
|
+
bestDistance = distance;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return best === -1 ? null : { stop: best, point: 0 };
|
|
189
|
+
}
|
|
190
|
+
const cell = scene.cells?.find(
|
|
191
|
+
(candidate) =>
|
|
192
|
+
x >= candidate.plot.x &&
|
|
193
|
+
x <= candidate.plot.x + candidate.plot.width &&
|
|
194
|
+
y >= candidate.plot.y &&
|
|
195
|
+
y <= candidate.plot.y + candidate.plot.height,
|
|
196
|
+
);
|
|
197
|
+
if (scene.cells !== undefined && cell === undefined) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const plot = cell?.plot ?? scene.plot;
|
|
201
|
+
if (
|
|
202
|
+
x < plot.x ||
|
|
203
|
+
x > plot.x + plot.width ||
|
|
204
|
+
y < plot.y ||
|
|
205
|
+
y > plot.y + plot.height
|
|
206
|
+
) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const along = order === "y" ? y : x;
|
|
210
|
+
let best = 0;
|
|
211
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
212
|
+
for (const [index, stop] of stops.entries()) {
|
|
213
|
+
if (cell !== undefined && stop.points[0].facet !== cell.key) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const distance = Math.abs(stop.at - along);
|
|
217
|
+
if (distance < bestDistance) {
|
|
218
|
+
best = index;
|
|
219
|
+
bestDistance = distance;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { stop: best, point: pickInStop(stops[best], x, y, order) };
|
|
223
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { ChartCorners, ChartCurve } from "#/core/types.ts";
|
|
2
|
+
|
|
3
|
+
/** A point in chart pixels. */
|
|
4
|
+
export interface ChartXY {
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Path builders. Everything is SVG path data, which both renderers read: SVG
|
|
11
|
+
* sets it as `d`, Canvas hands it to `Path2D`. Everything It is rounded to `PATH_PRECISION` decimals so the
|
|
12
|
+
* emitted `d` attributes stay short, stable across runs and pleasant to assert
|
|
13
|
+
* on in tests.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const PATH_PRECISION = 2;
|
|
17
|
+
/** Fritsch-Carlson clamps the tangent when the slope ratio exceeds this. */
|
|
18
|
+
const MONOTONE_SLOPE_LIMIT = 9;
|
|
19
|
+
|
|
20
|
+
function round(value: number): number {
|
|
21
|
+
const factor = 10 ** PATH_PRECISION;
|
|
22
|
+
return Math.round(value * factor) / factor;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function point(x: number, y: number): string {
|
|
26
|
+
return `${round(x)},${round(y)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Per-point tangents for a monotone cubic spline (Fritsch-Carlson): the curve
|
|
31
|
+
* bends through the data without the overshoot a plain cubic gives you, so a
|
|
32
|
+
* series that never goes negative never dips below the axis.
|
|
33
|
+
*/
|
|
34
|
+
function monotoneTangents(points: ReadonlyArray<ChartXY>): number[] {
|
|
35
|
+
const count = points.length;
|
|
36
|
+
const secants: number[] = [];
|
|
37
|
+
for (let index = 0; index < count - 1; index += 1) {
|
|
38
|
+
const run = points[index + 1].x - points[index].x;
|
|
39
|
+
secants.push(run === 0 ? 0 : (points[index + 1].y - points[index].y) / run);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tangents: number[] = [secants[0] ?? 0];
|
|
43
|
+
for (let index = 1; index < count - 1; index += 1) {
|
|
44
|
+
tangents.push((secants[index - 1] + secants[index]) / 2);
|
|
45
|
+
}
|
|
46
|
+
tangents.push(secants[count - 2] ?? 0);
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < secants.length; index += 1) {
|
|
49
|
+
const secant = secants[index];
|
|
50
|
+
if (secant === 0) {
|
|
51
|
+
tangents[index] = 0;
|
|
52
|
+
tangents[index + 1] = 0;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const before = tangents[index] / secant;
|
|
56
|
+
const after = tangents[index + 1] / secant;
|
|
57
|
+
const magnitude = before * before + after * after;
|
|
58
|
+
if (magnitude > MONOTONE_SLOPE_LIMIT) {
|
|
59
|
+
const scale = 3 / Math.sqrt(magnitude);
|
|
60
|
+
tangents[index] = scale * before * secant;
|
|
61
|
+
tangents[index + 1] = scale * after * secant;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return tangents;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function monotoneSegments(points: ReadonlyArray<ChartXY>): string {
|
|
68
|
+
const tangents = monotoneTangents(points);
|
|
69
|
+
const segments: string[] = [];
|
|
70
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
71
|
+
const from = points[index];
|
|
72
|
+
const to = points[index + 1];
|
|
73
|
+
const third = (to.x - from.x) / 3;
|
|
74
|
+
segments.push(
|
|
75
|
+
`C${point(from.x + third, from.y + tangents[index] * third)} ${point(
|
|
76
|
+
to.x - third,
|
|
77
|
+
to.y - tangents[index + 1] * third,
|
|
78
|
+
)} ${point(to.x, to.y)}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return segments.join("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Midpoint step: hold the value, jump halfway between the two categories. */
|
|
85
|
+
function stepSegments(points: ReadonlyArray<ChartXY>): string {
|
|
86
|
+
const segments: string[] = [];
|
|
87
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
88
|
+
const from = points[index];
|
|
89
|
+
const to = points[index + 1];
|
|
90
|
+
const middle = (from.x + to.x) / 2;
|
|
91
|
+
segments.push(
|
|
92
|
+
`L${point(middle, from.y)}L${point(middle, to.y)}L${point(to.x, to.y)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return segments.join("");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function linearSegments(points: ReadonlyArray<ChartXY>): string {
|
|
99
|
+
return points
|
|
100
|
+
.slice(1)
|
|
101
|
+
.map((current) => `L${point(current.x, current.y)}`)
|
|
102
|
+
.join("");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const segmentBuilders: Record<
|
|
106
|
+
ChartCurve,
|
|
107
|
+
(points: ReadonlyArray<ChartXY>) => string
|
|
108
|
+
> = {
|
|
109
|
+
linear: linearSegments,
|
|
110
|
+
monotone: monotoneSegments,
|
|
111
|
+
step: stepSegments,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* An open path through `points`. Returns `""` for fewer than two points, which
|
|
116
|
+
* renders as nothing rather than as a stray dot.
|
|
117
|
+
*/
|
|
118
|
+
export function linePath(
|
|
119
|
+
points: ReadonlyArray<ChartXY>,
|
|
120
|
+
curve: ChartCurve = "linear",
|
|
121
|
+
): string {
|
|
122
|
+
if (points.length < 2) {
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
return `M${point(points[0].x, points[0].y)}${segmentBuilders[curve](points)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A closed band between a top edge and a bottom edge, both drawn with the same
|
|
130
|
+
* curve, so an area and its outline never disagree. The bottom edge runs under
|
|
131
|
+
* the same x positions as the top: a flat baseline, or the series below it in
|
|
132
|
+
* a stack.
|
|
133
|
+
*/
|
|
134
|
+
export function areaPath(
|
|
135
|
+
top: ReadonlyArray<ChartXY>,
|
|
136
|
+
bottom: ReadonlyArray<ChartXY>,
|
|
137
|
+
curve: ChartCurve = "linear",
|
|
138
|
+
): string {
|
|
139
|
+
const upper = linePath(top, curve);
|
|
140
|
+
if (upper === "" || bottom.length === 0) {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
const lower = [...bottom].reverse();
|
|
144
|
+
return `${upper}L${point(lower[0].x, lower[0].y)}${segmentBuilders[curve](lower)}Z`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A closed polygon — the radar mark's outline. */
|
|
148
|
+
export function polygonPath(points: ReadonlyArray<ChartXY>): string {
|
|
149
|
+
if (points.length === 0) {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
const [head, ...rest] = points;
|
|
153
|
+
const tail = rest
|
|
154
|
+
.map((current) => `L${point(current.x, current.y)}`)
|
|
155
|
+
.join("");
|
|
156
|
+
return `M${point(head.x, head.y)}${tail}Z`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cornerRadii(
|
|
160
|
+
radius: number | ChartCorners,
|
|
161
|
+
width: number,
|
|
162
|
+
height: number,
|
|
163
|
+
): readonly [number, number, number, number] {
|
|
164
|
+
const raw: readonly [number, number, number, number] =
|
|
165
|
+
typeof radius === "number" ? [radius, radius, radius, radius] : radius;
|
|
166
|
+
const limit = Math.min(Math.abs(width), Math.abs(height)) / 2;
|
|
167
|
+
return [
|
|
168
|
+
Math.max(0, Math.min(raw[0], limit)),
|
|
169
|
+
Math.max(0, Math.min(raw[1], limit)),
|
|
170
|
+
Math.max(0, Math.min(raw[2], limit)),
|
|
171
|
+
Math.max(0, Math.min(raw[3], limit)),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface RoundedBarOptions {
|
|
176
|
+
readonly x: number;
|
|
177
|
+
readonly y: number;
|
|
178
|
+
readonly width: number;
|
|
179
|
+
readonly height: number;
|
|
180
|
+
/** One radius, or `[topLeft, topRight, bottomRight, bottomLeft]`. */
|
|
181
|
+
readonly radius?: number | ChartCorners;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A rectangle with independently rounded corners — stacked bars round only the
|
|
186
|
+
* outer end of the stack, so the inner joins stay square.
|
|
187
|
+
*/
|
|
188
|
+
export function roundedBarPath(options: RoundedBarOptions): string {
|
|
189
|
+
const { x, y, width, height, radius = 0 } = options;
|
|
190
|
+
if (width <= 0 || height <= 0) {
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = cornerRadii(
|
|
194
|
+
radius,
|
|
195
|
+
width,
|
|
196
|
+
height,
|
|
197
|
+
);
|
|
198
|
+
const right = x + width;
|
|
199
|
+
const bottom = y + height;
|
|
200
|
+
return [
|
|
201
|
+
`M${point(x + topLeft, y)}`,
|
|
202
|
+
`H${round(right - topRight)}`,
|
|
203
|
+
topRight > 0
|
|
204
|
+
? `A${round(topRight)},${round(topRight)} 0 0 1 ${point(right, y + topRight)}`
|
|
205
|
+
: "",
|
|
206
|
+
`V${round(bottom - bottomRight)}`,
|
|
207
|
+
bottomRight > 0
|
|
208
|
+
? `A${round(bottomRight)},${round(bottomRight)} 0 0 1 ${point(right - bottomRight, bottom)}`
|
|
209
|
+
: "",
|
|
210
|
+
`H${round(x + bottomLeft)}`,
|
|
211
|
+
bottomLeft > 0
|
|
212
|
+
? `A${round(bottomLeft)},${round(bottomLeft)} 0 0 1 ${point(x, bottom - bottomLeft)}`
|
|
213
|
+
: "",
|
|
214
|
+
`V${round(y + topLeft)}`,
|
|
215
|
+
topLeft > 0
|
|
216
|
+
? `A${round(topLeft)},${round(topLeft)} 0 0 1 ${point(x + topLeft, y)}`
|
|
217
|
+
: "",
|
|
218
|
+
"Z",
|
|
219
|
+
].join("");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const DEGREES_TO_RADIANS = Math.PI / 180;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Angles are degrees, clockwise, zero at twelve o'clock: the way a pie chart
|
|
226
|
+
* is described out loud.
|
|
227
|
+
*/
|
|
228
|
+
export function polarToCartesian(
|
|
229
|
+
cx: number,
|
|
230
|
+
cy: number,
|
|
231
|
+
radius: number,
|
|
232
|
+
angle: number,
|
|
233
|
+
): ChartXY {
|
|
234
|
+
const radians = angle * DEGREES_TO_RADIANS;
|
|
235
|
+
return {
|
|
236
|
+
x: cx + radius * Math.sin(radians),
|
|
237
|
+
y: cy - radius * Math.cos(radians),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface ArcOptions {
|
|
242
|
+
/** Centre of the circle the arc is cut from, in plot coordinates. */
|
|
243
|
+
readonly cx: number;
|
|
244
|
+
/** Centre of the circle the arc is cut from, in plot coordinates. */
|
|
245
|
+
readonly cy: number;
|
|
246
|
+
/** Inner edge. `0` makes the arc a pie wedge rather than a donut segment. */
|
|
247
|
+
readonly innerRadius: number;
|
|
248
|
+
/** Outer edge, in pixels from the centre. */
|
|
249
|
+
readonly outerRadius: number;
|
|
250
|
+
/** Degrees clockwise from twelve o'clock. */
|
|
251
|
+
readonly startAngle: number;
|
|
252
|
+
/** Degrees clockwise from twelve o'clock. A 360-degree sweep draws a full ring. */
|
|
253
|
+
readonly endAngle: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A pie wedge (`innerRadius` 0) or a donut segment. A sweep of 360 degrees or
|
|
258
|
+
* more is drawn as two half arcs, because a single arc back to its own start
|
|
259
|
+
* point renders as nothing at all.
|
|
260
|
+
*/
|
|
261
|
+
export function arcPath(options: ArcOptions): string {
|
|
262
|
+
const { cx, cy, innerRadius, outerRadius, startAngle, endAngle } = options;
|
|
263
|
+
const sweep = endAngle - startAngle;
|
|
264
|
+
if (sweep <= 0 || outerRadius <= 0) {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
if (sweep >= 360) {
|
|
268
|
+
const half = startAngle + 180;
|
|
269
|
+
return `${arcPath({ ...options, endAngle: half })}${arcPath({
|
|
270
|
+
...options,
|
|
271
|
+
startAngle: half,
|
|
272
|
+
endAngle: startAngle + 359.99,
|
|
273
|
+
})}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const largeArc = sweep > 180 ? 1 : 0;
|
|
277
|
+
const outerStart = polarToCartesian(cx, cy, outerRadius, startAngle);
|
|
278
|
+
const outerEnd = polarToCartesian(cx, cy, outerRadius, endAngle);
|
|
279
|
+
|
|
280
|
+
if (innerRadius <= 0) {
|
|
281
|
+
return [
|
|
282
|
+
`M${point(cx, cy)}`,
|
|
283
|
+
`L${point(outerStart.x, outerStart.y)}`,
|
|
284
|
+
`A${round(outerRadius)},${round(outerRadius)} 0 ${largeArc} 1 ${point(outerEnd.x, outerEnd.y)}`,
|
|
285
|
+
"Z",
|
|
286
|
+
].join("");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const innerEnd = polarToCartesian(cx, cy, innerRadius, endAngle);
|
|
290
|
+
const innerStart = polarToCartesian(cx, cy, innerRadius, startAngle);
|
|
291
|
+
return [
|
|
292
|
+
`M${point(outerStart.x, outerStart.y)}`,
|
|
293
|
+
`A${round(outerRadius)},${round(outerRadius)} 0 ${largeArc} 1 ${point(outerEnd.x, outerEnd.y)}`,
|
|
294
|
+
`L${point(innerEnd.x, innerEnd.y)}`,
|
|
295
|
+
`A${round(innerRadius)},${round(innerRadius)} 0 ${largeArc} 0 ${point(innerStart.x, innerStart.y)}`,
|
|
296
|
+
"Z",
|
|
297
|
+
].join("");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Path data for GeoJSON polygon rings already in chart pixels (what
|
|
302
|
+
* d3-contour returns): every ring closed, holes drawn with the even-odd rule
|
|
303
|
+
* the renderers default to.
|
|
304
|
+
*/
|
|
305
|
+
export function multiPolygonPath(
|
|
306
|
+
polygons: ReadonlyArray<
|
|
307
|
+
ReadonlyArray<
|
|
308
|
+
ReadonlyArray<readonly [number, number] | ReadonlyArray<number>>
|
|
309
|
+
>
|
|
310
|
+
>,
|
|
311
|
+
offsetX = 0,
|
|
312
|
+
offsetY = 0,
|
|
313
|
+
): string {
|
|
314
|
+
return polygons
|
|
315
|
+
.flatMap((rings) =>
|
|
316
|
+
rings.map((ring) =>
|
|
317
|
+
ring.length === 0
|
|
318
|
+
? ""
|
|
319
|
+
: `M${ring.map(([x, y]) => point(x + offsetX, y + offsetY)).join("L")}Z`,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
.join("");
|
|
323
|
+
}
|