@fundar/data-chart-telling 0.0.14 → 0.0.15

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.
Files changed (141) hide show
  1. package/README.md +121 -17
  2. package/dist/charts/{Chart.svelte.d.ts → BaseChart.svelte.d.ts} +3 -3
  3. package/dist/charts/bar/Chart.svelte +5 -5
  4. package/dist/charts/bar/Chart.svelte.d.ts +1 -2
  5. package/dist/charts/heatmap/Chart.svelte +8 -7
  6. package/dist/charts/heatmap/Chart.svelte.d.ts +2 -3
  7. package/dist/charts/line/Chart.svelte +5 -5
  8. package/dist/charts/line/Chart.svelte.d.ts +1 -2
  9. package/dist/charts/pyramid/Chart.svelte +7 -6
  10. package/dist/charts/pyramid/Chart.svelte.d.ts +2 -3
  11. package/dist/configuration/config.svelte.js +1 -1
  12. package/dist/configuration/themes/index.d.ts +56 -40
  13. package/dist/index.d.ts +26 -12
  14. package/dist/index.js +12 -7
  15. package/dist/layout/facet/FacetLayout.svelte +1 -1
  16. package/dist/layout/facet/FacetLayout.svelte.d.ts +1 -1
  17. package/dist/layout/geo/GeoLayout.svelte +47 -0
  18. package/dist/layout/geo/GeoLayout.svelte.d.ts +22 -0
  19. package/dist/layout/geo/resolveProjection.d.ts +10 -0
  20. package/dist/layout/geo/resolveProjection.js +48 -0
  21. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  22. package/dist/layout/plot/AxisLayout.svelte +95 -0
  23. package/dist/layout/plot/AxisLayout.svelte.d.ts +39 -0
  24. package/dist/layout/plot/BasePlotLayout.svelte +302 -0
  25. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +95 -0
  26. package/dist/layout/plot/GridLayout.svelte +30 -0
  27. package/dist/layout/plot/GridLayout.svelte.d.ts +27 -0
  28. package/dist/layout/plot/RuleLayout.svelte +111 -0
  29. package/dist/layout/plot/RuleLayout.svelte.d.ts +40 -0
  30. package/dist/layout/plot/margins.d.ts +34 -0
  31. package/dist/layout/plot/margins.js +26 -0
  32. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  33. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  34. package/dist/layout/tooltip/Tooltip.svelte +15 -0
  35. package/dist/layout/tooltip/Tooltip.svelte.d.ts +7 -0
  36. package/dist/layout/tooltip/TooltipLayout.svelte +45 -0
  37. package/dist/layout/tooltip/TooltipLayout.svelte.d.ts +41 -0
  38. package/dist/layout/tooltip/controller.svelte.d.ts +4 -3
  39. package/dist/layout/tooltip/controller.svelte.js +5 -6
  40. package/dist/layout/tooltip/hover.svelte.d.ts +1 -1
  41. package/dist/layout/tooltip/hover.svelte.js +1 -1
  42. package/dist/layout/tooltip/utils.d.ts +2 -2
  43. package/dist/markers/ContourClipMarker.svelte +77 -0
  44. package/dist/markers/ContourClipMarker.svelte.d.ts +10 -0
  45. package/dist/{plots/markers → markers}/DeltaMarker.svelte +4 -4
  46. package/dist/{plots/markers → markers}/DeltaMarker.svelte.d.ts +3 -3
  47. package/dist/{plots/markers → markers}/DotMarker.svelte +3 -3
  48. package/dist/{plots/markers → markers}/DotMarker.svelte.d.ts +3 -3
  49. package/dist/markers/HoverMarker.svelte +136 -0
  50. package/dist/{plots/markers → markers}/HoverMarker.svelte.d.ts +4 -4
  51. package/dist/{plots/markers → markers}/PeakMarker.svelte +2 -2
  52. package/dist/{plots/markers → markers}/PeakMarker.svelte.d.ts +2 -2
  53. package/dist/{plots/markers → markers}/TextMarker.svelte +11 -4
  54. package/dist/{plots/markers → markers}/TextMarker.svelte.d.ts +3 -3
  55. package/dist/markers/inset/InsetMarker.svelte +170 -0
  56. package/dist/markers/inset/InsetMarker.svelte.d.ts +51 -0
  57. package/dist/markers/inset/insetLayout.d.ts +25 -0
  58. package/dist/markers/inset/insetLayout.js +39 -0
  59. package/dist/markers/inset/insetProjection.d.ts +8 -0
  60. package/dist/markers/inset/insetProjection.js +39 -0
  61. package/dist/plots/bar/BarSegments.svelte +66 -0
  62. package/dist/plots/bar/BarSegments.svelte.d.ts +36 -0
  63. package/dist/plots/bar/Plot.svelte +132 -76
  64. package/dist/plots/bar/Plot.svelte.d.ts +7 -3
  65. package/dist/plots/bar/ValueLabels.svelte +89 -0
  66. package/dist/plots/bar/ValueLabels.svelte.d.ts +39 -0
  67. package/dist/plots/bar/hoverPoints.d.ts +15 -0
  68. package/dist/plots/bar/hoverPoints.js +35 -0
  69. package/dist/plots/bar/layout.svelte.d.ts +37 -0
  70. package/dist/plots/bar/layout.svelte.js +132 -0
  71. package/dist/plots/geo/Plot.svelte +548 -0
  72. package/dist/plots/geo/Plot.svelte.d.ts +29 -0
  73. package/dist/plots/geo/TileLayer.svelte +77 -0
  74. package/dist/plots/geo/TileLayer.svelte.d.ts +11 -0
  75. package/dist/plots/geo/projections/argentina.d.ts +8 -0
  76. package/dist/plots/geo/projections/argentina.js +35 -0
  77. package/dist/plots/geo/projections/fit.d.ts +8 -0
  78. package/dist/plots/geo/projections/fit.js +14 -0
  79. package/dist/plots/geo/rotate.svelte.d.ts +26 -0
  80. package/dist/plots/geo/rotate.svelte.js +79 -0
  81. package/dist/plots/geo/zoom.svelte.d.ts +32 -0
  82. package/dist/plots/geo/zoom.svelte.js +50 -0
  83. package/dist/plots/heatmap/Plot.svelte +129 -47
  84. package/dist/plots/heatmap/Plot.svelte.d.ts +7 -3
  85. package/dist/plots/line/Plot.svelte +44 -16
  86. package/dist/plots/line/Plot.svelte.d.ts +7 -3
  87. package/dist/plots/pyramid/Plot.svelte +70 -25
  88. package/dist/plots/pyramid/Plot.svelte.d.ts +7 -3
  89. package/dist/plots/utils/delta.d.ts +1 -1
  90. package/dist/plots/utils/geoAccessors.d.ts +64 -0
  91. package/dist/plots/utils/geoAccessors.js +188 -0
  92. package/dist/plots/utils/geoSegments.d.ts +14 -0
  93. package/dist/plots/utils/geoSegments.js +17 -0
  94. package/dist/plots/utils/segments.d.ts +14 -5
  95. package/dist/plots/utils/segments.js +25 -13
  96. package/dist/plots/utils/tiles.d.ts +89 -0
  97. package/dist/plots/utils/tiles.js +159 -0
  98. package/dist/plots/utils/topojson.d.ts +9 -0
  99. package/dist/plots/utils/topojson.js +30 -0
  100. package/dist/types/charts/common.d.ts +2 -1
  101. package/dist/types/charts/legend.d.ts +1 -1
  102. package/dist/types/charts/props.d.ts +9 -6
  103. package/dist/types/configuration/styling.d.ts +11 -5
  104. package/dist/types/layout/tooltip.d.ts +14 -21
  105. package/dist/types/{plots/markers.d.ts → markers/common.d.ts} +2 -18
  106. package/dist/types/markers/geo.d.ts +237 -0
  107. package/dist/types/markers/props.d.ts +11 -0
  108. package/dist/types/plots/axis.d.ts +55 -0
  109. package/dist/types/plots/axis.js +1 -0
  110. package/dist/types/plots/constants.d.ts +30 -0
  111. package/dist/types/plots/constants.js +1 -0
  112. package/dist/types/plots/data/common.d.ts +40 -0
  113. package/dist/types/plots/data/common.js +1 -0
  114. package/dist/types/plots/data/geo.d.ts +74 -0
  115. package/dist/types/plots/data/geo.js +1 -0
  116. package/dist/types/plots/delta.d.ts +2 -2
  117. package/dist/types/plots/props.d.ts +105 -16
  118. package/dist/types/plots/scales/geo.d.ts +44 -0
  119. package/dist/types/plots/scales/geo.js +1 -0
  120. package/dist/types/plots/segments/common.d.ts +25 -0
  121. package/dist/types/plots/segments/common.js +1 -0
  122. package/dist/types/plots/segments/config.d.ts +51 -0
  123. package/dist/types/plots/segments/config.js +1 -0
  124. package/dist/types/plots/styles/common.d.ts +37 -0
  125. package/dist/types/plots/styles/common.js +1 -0
  126. package/dist/types/plots/styles/geo.d.ts +42 -0
  127. package/dist/types/plots/styles/geo.js +1 -0
  128. package/dist/types/plots/styling.d.ts +45 -0
  129. package/dist/types/plots/styling.js +1 -0
  130. package/dist/utils/grouping.d.ts +2 -1
  131. package/dist/utils/interpolate.d.ts +2 -1
  132. package/package.json +27 -13
  133. package/dist/layout/plot/PlotLayout.svelte +0 -294
  134. package/dist/layout/plot/PlotLayout.svelte.d.ts +0 -70
  135. package/dist/plots/markers/HoverMarker.svelte +0 -98
  136. package/dist/types/plots/common.d.ts +0 -97
  137. package/dist/types/plots/styles.d.ts +0 -187
  138. /package/dist/charts/{Chart.svelte → BaseChart.svelte} +0 -0
  139. /package/dist/types/{plots → markers}/common.js +0 -0
  140. /package/dist/types/{plots/markers.js → markers/geo.js} +0 -0
  141. /package/dist/types/{plots/styles.js → markers/props.js} +0 -0
@@ -0,0 +1,35 @@
1
+ import { geoAzimuthalEquidistant } from 'd3-geo';
2
+ import { pointsToFitFeature } from './fit';
3
+ // Representative extremity points — not real border geometry (which stays
4
+ // out of `src/lib`, see stories/plots/geo/data.ts), just enough for
5
+ // `fitSize` to frame Argentina's full territorial claim sensibly. These are
6
+ // plain real-world coordinates, not derived from any particular dataset, so
7
+ // they work the same way regardless of which Argentina GeoJSON actually
8
+ // gets drawn through this projection.
9
+ const MAINLAND_POINTS = [
10
+ [-65.3, -21.8], // La Quiaca (north)
11
+ [-68.3, -54.8], // Ushuaia / Tierra del Fuego (south)
12
+ [-53.7, -25.6], // Puerto Iguazú (east)
13
+ [-73.4, -50.3], // Los Glaciares, Santa Cruz (west)
14
+ [-71.8, -41.1], // Bariloche (west, mid-latitude bulge)
15
+ ];
16
+ const ANTARCTIC_POINTS = [
17
+ [-61.2, -51.0], // Malvinas (north tip)
18
+ [-57.8, -52.3], // Malvinas (east tip)
19
+ [-38.0, -53.9], // Georgias del Sur
20
+ [-26.5, -59.5], // Sandwich del Sur
21
+ [-74, -60], // Antarctic sector, western meridian (60°S treaty boundary)
22
+ [-25, -60], // Antarctic sector, eastern meridian (60°S treaty boundary)
23
+ [-49.5, -89.9], // South Pole, on the sector's central meridian
24
+ ];
25
+ /**
26
+ * Argentina's territory in one unbroken frame: the mainland and the
27
+ * Antarctic/South Atlantic sector both visible, related to each other at
28
+ * true scale (azimuthal-equidistant, rotated so the split falls to the
29
+ * sides rather than through the middle of either half).
30
+ */
31
+ export const argentinaBicontinental = ({ width, height }) => {
32
+ const projection = geoAzimuthalEquidistant().rotate([60, 70]);
33
+ projection.fitSize([width, height], pointsToFitFeature([...MAINLAND_POINTS, ...ANTARCTIC_POINTS]));
34
+ return projection;
35
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Wraps a handful of representative `[lon, lat]` points as a `MultiPoint`
3
+ * Feature, just so `projection.fitExtent`/`fitSize` has something to
4
+ * measure. Custom projections built on these live in `src/lib` (see
5
+ * `stories/plots/geo/data.ts` for why real border geometry doesn't), so a
6
+ * few hand-picked extremity points stand in for a region's true extent.
7
+ */
8
+ export declare function pointsToFitFeature(points: [number, number][]): GeoJSON.Feature;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Wraps a handful of representative `[lon, lat]` points as a `MultiPoint`
3
+ * Feature, just so `projection.fitExtent`/`fitSize` has something to
4
+ * measure. Custom projections built on these live in `src/lib` (see
5
+ * `stories/plots/geo/data.ts` for why real border geometry doesn't), so a
6
+ * few hand-picked extremity points stand in for a region's true extent.
7
+ */
8
+ export function pointsToFitFeature(points) {
9
+ return {
10
+ type: 'Feature',
11
+ properties: null,
12
+ geometry: { type: 'MultiPoint', coordinates: points },
13
+ };
14
+ }
@@ -0,0 +1,26 @@
1
+ export type GeoRotate = [number, number, number];
2
+ /**
3
+ * Wires drag-to-rotate onto a DOM node for orthographic-style projections,
4
+ * exposing the resulting `[lambda, phi, gamma]` as reactive Svelte state —
5
+ * the caller feeds it back into the projection's own `rotate` config (see
6
+ * `GeoPlot`'s own `Plot.svelte`). Plain pointer events rather than d3-drag: a
7
+ * single-pointer drag-delta-to-rotation-delta conversion doesn't need
8
+ * d3-drag's multi-touch gesture machinery, and this package doesn't
9
+ * otherwise depend on it.
10
+ *
11
+ * Sensitivity is a fixed degrees-per-pixel constant scaled by the current
12
+ * pan/zoom `scale` (so dragging feels consistent whether zoomed in or out)
13
+ * — not derived from the projection's own fitted scale, which isn't
14
+ * exposed back up to this layer. Known v1 simplification, in the same
15
+ * spirit as `createGeoZoom`'s own "v1 limitation" — good enough to spin
16
+ * the globe, not tuned to feel identical across wildly different
17
+ * width/height/domain combinations.
18
+ */
19
+ export declare function createGeoRotate(args: {
20
+ enabled: () => boolean;
21
+ baseRotate: () => GeoRotate;
22
+ scale: () => number;
23
+ node: () => HTMLElement | null;
24
+ }): {
25
+ readonly rotate: GeoRotate;
26
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Wires drag-to-rotate onto a DOM node for orthographic-style projections,
3
+ * exposing the resulting `[lambda, phi, gamma]` as reactive Svelte state —
4
+ * the caller feeds it back into the projection's own `rotate` config (see
5
+ * `GeoPlot`'s own `Plot.svelte`). Plain pointer events rather than d3-drag: a
6
+ * single-pointer drag-delta-to-rotation-delta conversion doesn't need
7
+ * d3-drag's multi-touch gesture machinery, and this package doesn't
8
+ * otherwise depend on it.
9
+ *
10
+ * Sensitivity is a fixed degrees-per-pixel constant scaled by the current
11
+ * pan/zoom `scale` (so dragging feels consistent whether zoomed in or out)
12
+ * — not derived from the projection's own fitted scale, which isn't
13
+ * exposed back up to this layer. Known v1 simplification, in the same
14
+ * spirit as `createGeoZoom`'s own "v1 limitation" — good enough to spin
15
+ * the globe, not tuned to feel identical across wildly different
16
+ * width/height/domain combinations.
17
+ */
18
+ export function createGeoRotate(args) {
19
+ let rotate = $state(args.baseRotate());
20
+ // Reset to the configured base rotation whenever it changes (e.g. a
21
+ // Storybook control edit) — but only while not actively enabled, so this
22
+ // doesn't fight the user's own in-progress drag.
23
+ $effect(() => {
24
+ if (!args.enabled())
25
+ rotate = args.baseRotate();
26
+ });
27
+ $effect(() => {
28
+ const el = args.node();
29
+ if (!args.enabled() || !el)
30
+ return;
31
+ // `dragging`/`start`/`startRotate` are deliberately plain closure
32
+ // variables, not `$state` — this effect must NOT read the reactive
33
+ // `rotate` value anywhere in its own synchronous body (only inside the
34
+ // event-handler closures below, which aren't part of dependency
35
+ // tracking). Reading it here would make every `rotate` write from
36
+ // `onPointerMove` re-trigger this very effect mid-drag, tearing down
37
+ // and rebuilding the listeners — and losing `dragging`'s `true` state
38
+ // — on every pixel of movement.
39
+ let dragging = false;
40
+ let start = { x: 0, y: 0 };
41
+ let startRotate = [0, 0, 0];
42
+ function onPointerDown(event) {
43
+ if (event.button !== 0)
44
+ return;
45
+ dragging = true;
46
+ start = { x: event.clientX, y: event.clientY };
47
+ startRotate = rotate;
48
+ el.setPointerCapture(event.pointerId);
49
+ }
50
+ function onPointerMove(event) {
51
+ if (!dragging)
52
+ return;
53
+ const degPerPixel = 0.3 / Math.max(args.scale(), 0.1);
54
+ const dLambda = (event.clientX - start.x) * degPerPixel;
55
+ const dPhi = (event.clientY - start.y) * degPerPixel;
56
+ const [lambda0, phi0, gamma0] = startRotate;
57
+ rotate = [lambda0 + dLambda, Math.max(-90, Math.min(90, phi0 - dPhi)), gamma0];
58
+ }
59
+ function onPointerUp(event) {
60
+ dragging = false;
61
+ el.releasePointerCapture(event.pointerId);
62
+ }
63
+ el.addEventListener('pointerdown', onPointerDown);
64
+ el.addEventListener('pointermove', onPointerMove);
65
+ el.addEventListener('pointerup', onPointerUp);
66
+ el.addEventListener('pointercancel', onPointerUp);
67
+ return () => {
68
+ el.removeEventListener('pointerdown', onPointerDown);
69
+ el.removeEventListener('pointermove', onPointerMove);
70
+ el.removeEventListener('pointerup', onPointerUp);
71
+ el.removeEventListener('pointercancel', onPointerUp);
72
+ };
73
+ });
74
+ return {
75
+ get rotate() {
76
+ return rotate;
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,32 @@
1
+ import type { GeoZoomConfig } from '../../types/plots/styles/geo';
2
+ export type GeoZoomTransform = {
3
+ x: number;
4
+ y: number;
5
+ k: number;
6
+ };
7
+ /**
8
+ * Wires drag-pan + scroll-zoom onto a DOM node via d3-zoom, exposing the
9
+ * resulting transform as reactive Svelte state. The caller applies it as an
10
+ * SVG `<g transform="translate(x,y) scale(k)">` wrapper — cheap, since the
11
+ * underlying `d3-geo` projection is computed once by svelteplot and never
12
+ * recomputed per zoom tick.
13
+ *
14
+ * Known v1 limitation: svelteplot's own `<Pointer>` hit-tests in the plot's
15
+ * pre-zoom projected coordinate space, so hover/tooltip matching does not
16
+ * compensate for an active pan/zoom transform — it still works, but targets
17
+ * a shape's un-zoomed position rather than its currently-visible one.
18
+ */
19
+ export declare function createGeoZoom(args: {
20
+ enabled: () => boolean;
21
+ config: () => GeoZoomConfig | undefined;
22
+ node: () => HTMLElement | null;
23
+ /**
24
+ * When true, drag/touch gestures are left for something else to handle
25
+ * (see `createGeoRotate`) — only wheel/pinch scaling stays wired to this
26
+ * behavior. Checked per-event, so toggling it doesn't need to tear down
27
+ * and recreate the underlying d3-zoom behavior.
28
+ */
29
+ disableDrag?: () => boolean;
30
+ }): {
31
+ readonly transform: GeoZoomTransform;
32
+ };
@@ -0,0 +1,50 @@
1
+ import { select } from 'd3-selection';
2
+ import { zoom as d3zoom, zoomIdentity } from 'd3-zoom';
3
+ /**
4
+ * Wires drag-pan + scroll-zoom onto a DOM node via d3-zoom, exposing the
5
+ * resulting transform as reactive Svelte state. The caller applies it as an
6
+ * SVG `<g transform="translate(x,y) scale(k)">` wrapper — cheap, since the
7
+ * underlying `d3-geo` projection is computed once by svelteplot and never
8
+ * recomputed per zoom tick.
9
+ *
10
+ * Known v1 limitation: svelteplot's own `<Pointer>` hit-tests in the plot's
11
+ * pre-zoom projected coordinate space, so hover/tooltip matching does not
12
+ * compensate for an active pan/zoom transform — it still works, but targets
13
+ * a shape's un-zoomed position rather than its currently-visible one.
14
+ */
15
+ export function createGeoZoom(args) {
16
+ let transform = $state({ x: 0, y: 0, k: 1 });
17
+ $effect(() => {
18
+ const el = args.node();
19
+ if (!args.enabled() || !el)
20
+ return;
21
+ const cfg = args.config();
22
+ const behavior = d3zoom()
23
+ .scaleExtent([cfg?.min ?? 1, cfg?.max ?? 8])
24
+ .filter((event) => {
25
+ if (args.disableDrag?.() && event.type !== 'wheel')
26
+ return false;
27
+ // d3-zoom's own default filter, minus the part this replaces.
28
+ return (!event.ctrlKey || event.type === 'wheel') && !event.button;
29
+ })
30
+ .on('zoom', (event) => {
31
+ transform = { x: event.transform.x, y: event.transform.y, k: event.transform.k };
32
+ });
33
+ const selection = select(el).call(behavior);
34
+ const initial = cfg?.initial;
35
+ if (initial) {
36
+ behavior.transform(selection, zoomIdentity.translate(initial.x ?? 0, initial.y ?? 0).scale(initial.k ?? 1));
37
+ }
38
+ else {
39
+ transform = { x: 0, y: 0, k: 1 };
40
+ }
41
+ return () => {
42
+ selection.on('.zoom', null);
43
+ };
44
+ });
45
+ return {
46
+ get transform() {
47
+ return transform;
48
+ },
49
+ };
50
+ }
@@ -1,13 +1,22 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
2
  import { Cell, Text } from 'svelteplot';
3
3
  import { resolveAccessor } from '../utils/accessors';
4
+ import { validateSegments } from '../utils/segments';
4
5
  import { buildSeries } from '../../utils/grouping';
5
6
  import { makeColorScale } from '../../utils/color';
6
7
  import { getConfiguration } from '../../configuration/config.svelte';
7
- import PlotLayout from '../../layout/plot/PlotLayout.svelte';
8
- import type { PlotProps } from '../../types/plots/props';
9
- import type { AxisValue } from '../../types/plots/common';
10
- import type { CellSegmentStyle } from '../../types/plots/styles';
8
+ import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
9
+ import AxisLayout from '../../layout/plot/AxisLayout.svelte';
10
+ import GridLayout from '../../layout/plot/GridLayout.svelte';
11
+ import RuleLayout from '../../layout/plot/RuleLayout.svelte';
12
+ import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
13
+ import type { BasePlotProps } from '../../types/plots/props';
14
+ import type { AxisValue } from '../../types/plots/axis';
15
+ import type { SeriesProps, DataProps } from '../../types/plots/data/common';
16
+ import type { AxisBasedScalesConfig } from '../../types/plots/props';
17
+ import type { BasePlotStyles } from '../../types/plots/styles/common';
18
+ import type { CellSegmentStyle } from '../../types/plots/segments/common';
19
+ import type { Marker } from '../../types/markers/common';
11
20
 
12
21
  /**
13
22
  * A categorical grid of value-coloured cells. Accepts either a pre-built
@@ -18,6 +27,15 @@
18
27
  * The colour domain is read from `scales.z.domain`; when absent it is derived
19
28
  * from the visible slice of data.
20
29
  */
30
+ type Props = BasePlotProps<
31
+ SeriesProps<TData> | DataProps<TData>,
32
+ TData,
33
+ BasePlotStyles,
34
+ CellSegmentStyle,
35
+ Marker,
36
+ AxisBasedScalesConfig<TData>
37
+ >;
38
+
21
39
  let {
22
40
  width,
23
41
  height,
@@ -32,9 +50,10 @@
32
50
  margins,
33
51
  markers = [],
34
52
  tooltip = undefined,
35
- }: PlotProps<TData, CellSegmentStyle> = $props();
53
+ }: Props = $props();
36
54
 
37
55
  $effect(() => {
56
+ validateSegments(segments);
38
57
  const scoped = markers.filter(
39
58
  (m) => (m.type === 'hover' || m.type === 'component') && m.series,
40
59
  );
@@ -43,12 +62,10 @@
43
62
  'HeatmapPlot: markers scoped to a `series` are ignored — heatmaps have no series concept.',
44
63
  );
45
64
  }
46
- const seriesKeys = segments.flatMap((g) =>
47
- Object.keys(g as Record<string, unknown>).filter((k) => k !== 'default'),
48
- );
65
+ const seriesKeys = Object.keys(segments).filter((k) => k !== 'default');
49
66
  if (seriesKeys.length > 0) {
50
67
  console.warn(
51
- `HeatmapPlot: segment groups have series-specific keys (${[...new Set(seriesKeys)].join(', ')}) — heatmaps have no named series. Use the 'default' key instead.`,
68
+ `HeatmapPlot: segment groups have series-specific keys (${seriesKeys.join(', ')}) — heatmaps have no named series. Use the 'default' key instead.`,
52
69
  );
53
70
  }
54
71
  });
@@ -58,6 +75,9 @@
58
75
  const maxColor = $derived(styles.colors?.max ?? cfg.continuous.max);
59
76
  const showVals = $derived(styles.values?.show ?? true);
60
77
  const fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
78
+ // 'outside' is a per-point escape hatch resolved by HoverMarker; heatmaps
79
+ // have no signed axis to resolve it against, so treat it as unset.
80
+ const resolvedTextAnchor = $derived(styles.values?.textAnchor === 'outside' ? undefined : styles.values?.textAnchor);
61
81
 
62
82
  const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, undefined));
63
83
  const flat = $derived(resolvedSeries.flatMap((s) => s.data));
@@ -90,29 +110,39 @@
90
110
  makeColorScale(valueDomain[0], valueDomain[1], minColor, maxColor),
91
111
  );
92
112
 
93
- const segList = $derived(segments.flatMap((g) => (g as Record<string, CellSegmentStyle[]>)['default'] ?? []));
113
+ const segList = $derived(segments.default ?? []);
94
114
 
115
+ /**
116
+ * A catch-all (no `areas`) always matches, so evaluating segments in plain
117
+ * array order and merging every match — as this used to do — makes a
118
+ * catch-all listed *after* an area-specific one (the documented, and only
119
+ * demonstrated, convention: baseline first, highlight second) overwrite it
120
+ * on every cell, since it's applied last unconditionally. Area-specific
121
+ * segments must always win over the catch-all regardless of list order —
122
+ * so the catch-all is applied once as the baseline, and only
123
+ * area-specific segments can override it on top, mirroring the same
124
+ * catch-all/area-specific priority every other plot kind's segments
125
+ * already follow (see `resolveSeriesSegments` in `utils/segments.ts`).
126
+ */
95
127
  const segStyleMap = $derived.by(() => {
96
128
  const map = new Map<string, CellSegmentStyle>();
97
129
  if (!segList.length) return map;
130
+ const catchAll = segList.find((s) => !s.areas || s.areas.length === 0);
131
+ const areaSegs = segList.filter((s) => s.areas && s.areas.length > 0);
98
132
  for (const d of flat) {
99
133
  const cx = getX(d) as AxisValue;
100
134
  const cy = getY(d) as AxisValue;
101
135
  const key = `${String(cx)}::${String(cy)}`;
102
136
  if (!map.has(key)) {
103
- let style: CellSegmentStyle = {};
104
- for (const seg of segList) {
105
- const areas = seg.areas;
106
- const matches =
107
- !areas || areas.length === 0
108
- ? true
109
- : areas.some((area) => {
110
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
- const xOk = !area.x || ((area.x.from === undefined || (cx as any) >= area.x.from) && (area.x.to === undefined || (cx as any) <= area.x.to));
112
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
- const yOk = !area.y || ((area.y.from === undefined || (cy as any) >= area.y.from) && (area.y.to === undefined || (cy as any) <= area.y.to));
114
- return xOk && yOk;
115
- });
137
+ let style: CellSegmentStyle = catchAll?.style ? { ...catchAll.style } : {};
138
+ for (const seg of areaSegs) {
139
+ const matches = seg.areas!.some((area) => {
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ const xOk = !area.x || ((area.x.from === undefined || (cx as any) >= area.x.from) && (area.x.to === undefined || (cx as any) <= area.x.to));
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ const yOk = !area.y || ((area.y.from === undefined || (cy as any) >= area.y.from) && (area.y.to === undefined || (cy as any) <= area.y.to));
144
+ return xOk && yOk;
145
+ });
116
146
  if (matches) style = { ...style, ...seg.style };
117
147
  }
118
148
  map.set(key, style);
@@ -124,37 +154,83 @@
124
154
  const getCellStyle = $derived(
125
155
  (d: TData) => segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`) ?? {},
126
156
  );
157
+
158
+ /**
159
+ * One `<Cell>` call per *distinct resolved style*, not one call for the
160
+ * whole dataset. svelteplot registers `fillOpacity`/`strokeWidth` under
161
+ * its shared "opacity"/numeric scale machinery — passed as a per-row
162
+ * accessor that genuinely varies within a single mark instance, that
163
+ * scale collapses every row to the same (wrong) value instead of the
164
+ * literal numbers set on each segment. A literal, per-call constant (one
165
+ * call per style bucket) bypasses that entirely, which is exactly how
166
+ * bar/line avoid the same trap (one mark per visual segment). `fill`
167
+ * stays a per-cell accessor within each group — unlike fillOpacity, it
168
+ * must keep varying continuously with the underlying value wherever a
169
+ * segment doesn't explicitly override it.
170
+ */
171
+ type StyleGroup = { style: CellSegmentStyle; data: TData[] };
172
+ const styleGroups = $derived.by((): StyleGroup[] => {
173
+ const groups = new Map<string, StyleGroup>();
174
+ for (const d of flat) {
175
+ const style = getCellStyle(d);
176
+ const key = JSON.stringify(style);
177
+ let group = groups.get(key);
178
+ if (!group) {
179
+ group = { style, data: [] };
180
+ groups.set(key, group);
181
+ }
182
+ group.data.push(d);
183
+ }
184
+ return [...groups.values()];
185
+ });
186
+
187
+ // The single, fully-resolved scales object BasePlotLayout reads for
188
+ // everything (ticks, grid, sort, and the real svelteplot scale) — avoids
189
+ // keeping a second, separately-defaulted copy of `scales.x`/`scales.y`
190
+ // (our own band-scale + domain defaults) out of sync with the one
191
+ // actually forwarded.
192
+ const resolvedScales = $derived<AxisBasedScalesConfig<TData>>({
193
+ ...scales,
194
+ x: { type: 'band', labelAnchor: 'right', domain: xDomain, ...(scales.x ?? {}) },
195
+ y: { type: 'band', domain: yDomain, ...(scales.y ?? {}) },
196
+ });
197
+
198
+ const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
199
+ const hoverStrategy = $derived(resolveHoverStrategy(tooltip?.strategy, markers, 'punctual'));
200
+ const hoverSync = $derived(resolveHoverSync(tooltip?.sync, markers));
127
201
  </script>
128
202
 
129
- <PlotLayout
203
+ <BasePlotLayout
130
204
  {width}
131
205
  {height}
132
- x={{ type: 'band', labelAnchor: 'right', domain: xDomain, ...(scales.x ?? {}) }}
133
- y={{ type: 'band', domain: yDomain, ...(scales.y ?? {}) }}
134
206
  data={flat}
135
207
  {getX}
136
208
  {getY}
137
- matchY={getY}
138
209
  {hoverPoints}
139
210
  label={(d) => fmtVal(Number(getZ(d)), '')}
140
211
  {tooltip}
141
- {markers}
142
- seriesData={resolvedSeries}
143
- scales={scales}
212
+ {hoverActive}
213
+ {hoverStrategy}
214
+ {hoverSync}
215
+ scales={resolvedScales}
144
216
  {margins}
145
- showHoverLabels={false}
146
- defaultStrategy="punctual"
147
217
  >
148
- <Cell
149
- data={flat as unknown as Record<string | symbol, never>[]}
150
- x={getX}
151
- y={getY}
152
- fill={(d) => getCellStyle(d as TData).fill ?? colorScale(Number(getZ(d as TData)))}
153
- fillOpacity={(d) => getCellStyle(d as TData).fillOpacity ?? 1}
154
- stroke={(d) => getCellStyle(d as TData).stroke}
155
- strokeWidth={(d) => getCellStyle(d as TData).strokeWidth ?? 0}
156
- inset={0.5}
157
- />
218
+ {#snippet children({ matchedPoints, ruleX, ruleY, impliesRuleX, impliesRuleY, numericMargins })}
219
+ <AxisLayout data={flat} {getX} {getY} {width} {height} {numericMargins} scales={resolvedScales} />
220
+ <GridLayout scales={resolvedScales} />
221
+ <RuleLayout {markers} seriesData={resolvedSeries} {matchedPoints} {ruleX} {ruleY} {impliesRuleX} {impliesRuleY} showHoverLabels={false}>
222
+ {#each styleGroups as group, i (i)}
223
+ <Cell
224
+ data={group.data as unknown as Record<string | symbol, never>[]}
225
+ x={getX}
226
+ y={getY}
227
+ fill={(d) => group.style.fill ?? colorScale(Number(getZ(d as TData)))}
228
+ fillOpacity={group.style.fillOpacity ?? 1}
229
+ stroke={group.style.stroke}
230
+ strokeWidth={group.style.strokeWidth ?? 0}
231
+ inset={0.5}
232
+ />
233
+ {/each}
158
234
 
159
235
  {#if showVals}
160
236
  <Text
@@ -162,17 +238,23 @@
162
238
  x={getX}
163
239
  y={getY}
164
240
  text={(d) => fmtVal(Number(getZ(d)), '')}
165
- fill={cfg.continuous.labelColor}
166
- fontSize={12}
167
- fontWeight={600}
168
- textAnchor={styles.values?.textAnchor ?? 'middle'}
241
+ fill={styles.values?.fill ?? cfg.continuous.labelColor}
242
+ fontSize={styles.values?.fontSize ?? 12}
243
+ fontWeight={styles.values?.fontWeight ?? 600}
244
+ fontStyle={styles.values?.fontStyle}
245
+ textAnchor={resolvedTextAnchor ?? 'middle'}
169
246
  lineAnchor={styles.values?.lineAnchor ?? 'middle'}
170
247
  dx={styles.values?.dx ?? 0}
171
248
  dy={styles.values?.dy ?? 0}
172
249
  lineHeight={styles.values?.lineHeight}
173
250
  rotate={styles.values?.rotate}
251
+ stroke={styles.values?.stroke}
252
+ strokeWidth={styles.values?.strokeWidth}
253
+ paintOrder={styles.values?.paintOrder}
174
254
  class={styles.values?.class}
175
255
  textClass={styles.values?.textClass}
176
256
  />
177
257
  {/if}
178
- </PlotLayout>
258
+ </RuleLayout>
259
+ {/snippet}
260
+ </BasePlotLayout>
@@ -1,7 +1,11 @@
1
- import type { PlotProps } from '../../types/plots/props';
2
- import type { CellSegmentStyle } from '../../types/plots/styles';
1
+ import type { BasePlotProps } from '../../types/plots/props';
2
+ import type { SeriesProps, DataProps } from '../../types/plots/data/common';
3
+ import type { AxisBasedScalesConfig } from '../../types/plots/props';
4
+ import type { BasePlotStyles } from '../../types/plots/styles/common';
5
+ import type { CellSegmentStyle } from '../../types/plots/segments/common';
6
+ import type { Marker } from '../../types/markers/common';
3
7
  declare function $$render<TData extends Record<string, unknown>>(): {
4
- props: PlotProps<TData, CellSegmentStyle>;
8
+ props: BasePlotProps<SeriesProps<TData> | DataProps<TData>, TData, BasePlotStyles, CellSegmentStyle, Marker, AxisBasedScalesConfig<TData>>;
5
9
  exports: {};
6
10
  bindings: "";
7
11
  slots: {};
@@ -8,13 +8,30 @@
8
8
  import { getConfiguration } from '../../configuration/config.svelte';
9
9
  import { paletteColor } from '../../utils/color';
10
10
  import { buildSeries } from '../../utils/grouping';
11
- import PlotLayout from '../../layout/plot/PlotLayout.svelte';
12
- import HoverMarker from '../markers/HoverMarker.svelte';
11
+ import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
12
+ import AxisLayout from '../../layout/plot/AxisLayout.svelte';
13
+ import GridLayout from '../../layout/plot/GridLayout.svelte';
14
+ import RuleLayout from '../../layout/plot/RuleLayout.svelte';
15
+ import HoverMarker from '../../markers/HoverMarker.svelte';
16
+ import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
13
17
  import { buildGroupedSeries, validateSegments, resolveSegmentsForSeries, matchesSegmentX, getDataForSegments } from '../utils/segments';
14
- import type { PlotProps } from '../../types/plots/props';
15
- import type { Series, AxisValue } from '../../types/plots/common';
16
- import type { LineSegmentStyle, ValueAnchor } from '../../types/plots/styles';
17
- import type { Marker } from '../../types/plots/markers';
18
+ import type { BasePlotProps } from '../../types/plots/props';
19
+ import type { AxisValue } from '../../types/plots/axis';
20
+ import type { Series, SeriesProps, DataProps } from '../../types/plots/data/common';
21
+ import type { AxisBasedScalesConfig } from '../../types/plots/props';
22
+ import type { BasePlotStyles } from '../../types/plots/styles/common';
23
+ import type { ValueAnchor } from '../../types/plots/constants';
24
+ import type { LineSegmentStyle } from '../../types/plots/segments/common';
25
+ import type { Marker } from '../../types/markers/common';
26
+
27
+ type Props = BasePlotProps<
28
+ SeriesProps<TData> | DataProps<TData>,
29
+ TData,
30
+ BasePlotStyles,
31
+ LineSegmentStyle,
32
+ Marker,
33
+ AxisBasedScalesConfig<TData>
34
+ >;
18
35
 
19
36
  let {
20
37
  width,
@@ -30,7 +47,7 @@
30
47
  scales = {},
31
48
  margins,
32
49
  tooltip = undefined,
33
- }: PlotProps<TData, LineSegmentStyle> = $props();
50
+ }: Props = $props();
34
51
 
35
52
  const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, z));
36
53
 
@@ -57,6 +74,10 @@
57
74
  styles.values?.format ?? ((v: number, name: string) => `${name} - ${v}`),
58
75
  );
59
76
  const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'start');
77
+ // 'outside' is a per-point escape hatch resolved by HoverMarker; line's own
78
+ // value labels already have their own `anchor: 'outside'` concept above,
79
+ // so treat it as unset here.
80
+ const resolvedTextAnchor = $derived(styles.values?.textAnchor === 'outside' ? undefined : styles.values?.textAnchor);
60
81
 
61
82
  const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
62
83
  const yAcc = $derived(resolveAccessor(resolvedSeries[0]?.y ?? ((d: TData) => d)));
@@ -82,26 +103,32 @@
82
103
  .map((s, idx) => ({ series: s, lastRow: s.data[s.data.length - 1], seriesIndex: idx }))
83
104
  .filter(({ lastRow }) => lastRow !== undefined),
84
105
  );
106
+
107
+ const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
108
+ const hoverStrategy = $derived(resolveHoverStrategy(tooltip?.strategy, markers, 'x'));
109
+ const hoverSync = $derived(resolveHoverSync(tooltip?.sync, markers));
85
110
  </script>
86
111
 
87
- <PlotLayout
112
+ <BasePlotLayout
88
113
  {width}
89
114
  {height}
90
- x={scales.x}
91
- y={scales.y}
92
115
  data={flat}
93
116
  getX={xAcc}
94
117
  getY={(d) => Number(yAcc(d))}
95
118
  {hoverPoints}
96
119
  label={(r) => String(yAcc(r))}
97
120
  {tooltip}
98
- {markers}
99
- seriesData={resolvedSeries}
121
+ {hoverActive}
122
+ {hoverStrategy}
123
+ {hoverSync}
100
124
  {scales}
101
125
  {margins}
102
- defaultStrategy="x"
126
+ xTickRotate={scales?.x?.tickRotate}
103
127
  >
104
- {#snippet children({ matchedPoints, ruleX, ruleY, impliesRuleX, impliesRuleY })}
128
+ {#snippet children({ matchedPoints, ruleX, ruleY, impliesRuleX, impliesRuleY, numericMargins })}
129
+ <AxisLayout data={flat} getX={xAcc} getY={(d) => Number(yAcc(d))} {width} {height} {numericMargins} {scales} />
130
+ <GridLayout {scales} />
131
+ <RuleLayout {markers} seriesData={resolvedSeries} {matchedPoints} {ruleX} {ruleY} {impliesRuleX} {impliesRuleY}>
105
132
  {#each groupedSeries as group, seriesIndex (group.series.name)}
106
133
  {@const defaultColor = paletteColor(seriesIndex, cfg.palette)}
107
134
  {#each group.visualSegments as seg, i (`${group.series.name}-${i}`)}
@@ -221,7 +248,7 @@
221
248
  text={() => fmtVal(Number(yFn(lastRow)), s.name)}
222
249
  dx={styles.values?.dx ?? defaultDx}
223
250
  dy={styles.values?.dy ?? defaultDy}
224
- textAnchor={styles.values?.textAnchor ?? defaultTA}
251
+ textAnchor={resolvedTextAnchor ?? defaultTA}
225
252
  lineAnchor={styles.values?.lineAnchor ?? defaultLA}
226
253
  lineHeight={styles.values?.lineHeight}
227
254
  rotate={styles.values?.rotate}
@@ -232,5 +259,6 @@
232
259
  />
233
260
  {/each}
234
261
  {/if}
262
+ </RuleLayout>
235
263
  {/snippet}
236
- </PlotLayout>
264
+ </BasePlotLayout>