@fundar/data-chart-telling 0.0.14 → 0.0.16

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 (148) hide show
  1. package/README.md +121 -17
  2. package/dist/charts/{Chart.svelte → BaseChart.svelte} +2 -0
  3. package/dist/charts/{Chart.svelte.d.ts → BaseChart.svelte.d.ts} +3 -3
  4. package/dist/charts/bar/Chart.svelte +5 -5
  5. package/dist/charts/bar/Chart.svelte.d.ts +1 -2
  6. package/dist/charts/heatmap/Chart.svelte +8 -7
  7. package/dist/charts/heatmap/Chart.svelte.d.ts +2 -3
  8. package/dist/charts/line/Chart.svelte +5 -5
  9. package/dist/charts/line/Chart.svelte.d.ts +1 -2
  10. package/dist/charts/pyramid/Chart.svelte +7 -6
  11. package/dist/charts/pyramid/Chart.svelte.d.ts +2 -3
  12. package/dist/configuration/config.svelte.js +2 -1
  13. package/dist/configuration/themes/index.d.ts +60 -40
  14. package/dist/index.d.ts +27 -13
  15. package/dist/index.js +12 -7
  16. package/dist/layout/facet/FacetLayout.svelte +1 -1
  17. package/dist/layout/facet/FacetLayout.svelte.d.ts +1 -1
  18. package/dist/layout/geo/GeoLayout.svelte +47 -0
  19. package/dist/layout/geo/GeoLayout.svelte.d.ts +22 -0
  20. package/dist/layout/geo/resolveProjection.d.ts +10 -0
  21. package/dist/layout/geo/resolveProjection.js +48 -0
  22. package/dist/layout/legend/ContinuousSection.svelte +158 -17
  23. package/dist/layout/legend/DiscreteSection.svelte +56 -4
  24. package/dist/layout/legend/interaction.svelte.d.ts +7 -0
  25. package/dist/layout/legend/interaction.svelte.js +24 -0
  26. package/dist/layout/plot/AxisLayout.svelte +95 -0
  27. package/dist/layout/plot/AxisLayout.svelte.d.ts +39 -0
  28. package/dist/layout/plot/BasePlotLayout.svelte +302 -0
  29. package/dist/layout/plot/BasePlotLayout.svelte.d.ts +95 -0
  30. package/dist/layout/plot/GridLayout.svelte +30 -0
  31. package/dist/layout/plot/GridLayout.svelte.d.ts +27 -0
  32. package/dist/layout/plot/RuleLayout.svelte +111 -0
  33. package/dist/layout/plot/RuleLayout.svelte.d.ts +40 -0
  34. package/dist/layout/plot/margins.d.ts +34 -0
  35. package/dist/layout/plot/margins.js +26 -0
  36. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  37. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  38. package/dist/layout/tooltip/Tooltip.svelte +15 -0
  39. package/dist/layout/tooltip/Tooltip.svelte.d.ts +7 -0
  40. package/dist/layout/tooltip/TooltipLayout.svelte +45 -0
  41. package/dist/layout/tooltip/TooltipLayout.svelte.d.ts +41 -0
  42. package/dist/layout/tooltip/controller.svelte.d.ts +4 -3
  43. package/dist/layout/tooltip/controller.svelte.js +5 -6
  44. package/dist/layout/tooltip/hover.svelte.d.ts +1 -1
  45. package/dist/layout/tooltip/hover.svelte.js +1 -1
  46. package/dist/layout/tooltip/utils.d.ts +2 -2
  47. package/dist/markers/ContourClipMarker.svelte +77 -0
  48. package/dist/markers/ContourClipMarker.svelte.d.ts +10 -0
  49. package/dist/{plots/markers → markers}/DeltaMarker.svelte +4 -4
  50. package/dist/{plots/markers → markers}/DeltaMarker.svelte.d.ts +3 -3
  51. package/dist/{plots/markers → markers}/DotMarker.svelte +3 -3
  52. package/dist/{plots/markers → markers}/DotMarker.svelte.d.ts +3 -3
  53. package/dist/markers/HoverMarker.svelte +136 -0
  54. package/dist/{plots/markers → markers}/HoverMarker.svelte.d.ts +4 -4
  55. package/dist/{plots/markers → markers}/PeakMarker.svelte +2 -2
  56. package/dist/{plots/markers → markers}/PeakMarker.svelte.d.ts +2 -2
  57. package/dist/{plots/markers → markers}/TextMarker.svelte +11 -4
  58. package/dist/{plots/markers → markers}/TextMarker.svelte.d.ts +3 -3
  59. package/dist/markers/inset/InsetMarker.svelte +170 -0
  60. package/dist/markers/inset/InsetMarker.svelte.d.ts +51 -0
  61. package/dist/markers/inset/insetLayout.d.ts +25 -0
  62. package/dist/markers/inset/insetLayout.js +39 -0
  63. package/dist/markers/inset/insetProjection.d.ts +8 -0
  64. package/dist/markers/inset/insetProjection.js +39 -0
  65. package/dist/plots/bar/BarSegments.svelte +69 -0
  66. package/dist/plots/bar/BarSegments.svelte.d.ts +40 -0
  67. package/dist/plots/bar/Plot.svelte +140 -74
  68. package/dist/plots/bar/Plot.svelte.d.ts +7 -3
  69. package/dist/plots/bar/ValueLabels.svelte +89 -0
  70. package/dist/plots/bar/ValueLabels.svelte.d.ts +39 -0
  71. package/dist/plots/bar/hoverPoints.d.ts +15 -0
  72. package/dist/plots/bar/hoverPoints.js +35 -0
  73. package/dist/plots/bar/layout.svelte.d.ts +37 -0
  74. package/dist/plots/bar/layout.svelte.js +132 -0
  75. package/dist/plots/geo/Plot.svelte +577 -0
  76. package/dist/plots/geo/Plot.svelte.d.ts +29 -0
  77. package/dist/plots/geo/TileLayer.svelte +77 -0
  78. package/dist/plots/geo/TileLayer.svelte.d.ts +11 -0
  79. package/dist/plots/geo/projections/argentina.d.ts +8 -0
  80. package/dist/plots/geo/projections/argentina.js +35 -0
  81. package/dist/plots/geo/projections/fit.d.ts +8 -0
  82. package/dist/plots/geo/projections/fit.js +14 -0
  83. package/dist/plots/geo/rotate.svelte.d.ts +26 -0
  84. package/dist/plots/geo/rotate.svelte.js +79 -0
  85. package/dist/plots/geo/zoom.svelte.d.ts +32 -0
  86. package/dist/plots/geo/zoom.svelte.js +50 -0
  87. package/dist/plots/heatmap/Plot.svelte +171 -58
  88. package/dist/plots/heatmap/Plot.svelte.d.ts +7 -3
  89. package/dist/plots/line/Plot.svelte +67 -30
  90. package/dist/plots/line/Plot.svelte.d.ts +7 -3
  91. package/dist/plots/pyramid/Plot.svelte +88 -35
  92. package/dist/plots/pyramid/Plot.svelte.d.ts +7 -3
  93. package/dist/plots/utils/delta.d.ts +1 -1
  94. package/dist/plots/utils/geoAccessors.d.ts +64 -0
  95. package/dist/plots/utils/geoAccessors.js +188 -0
  96. package/dist/plots/utils/geoSegments.d.ts +14 -0
  97. package/dist/plots/utils/geoSegments.js +17 -0
  98. package/dist/plots/utils/legendDisabled.d.ts +15 -0
  99. package/dist/plots/utils/legendDisabled.js +41 -0
  100. package/dist/plots/utils/segments.d.ts +14 -5
  101. package/dist/plots/utils/segments.js +25 -13
  102. package/dist/plots/utils/tiles.d.ts +89 -0
  103. package/dist/plots/utils/tiles.js +159 -0
  104. package/dist/plots/utils/topojson.d.ts +9 -0
  105. package/dist/plots/utils/topojson.js +30 -0
  106. package/dist/types/charts/common.d.ts +2 -1
  107. package/dist/types/charts/legend.d.ts +32 -1
  108. package/dist/types/charts/props.d.ts +9 -6
  109. package/dist/types/configuration/styling.d.ts +13 -5
  110. package/dist/types/layout/legend.d.ts +26 -0
  111. package/dist/types/layout/tooltip.d.ts +14 -21
  112. package/dist/types/{plots/markers.d.ts → markers/common.d.ts} +2 -18
  113. package/dist/types/markers/geo.d.ts +237 -0
  114. package/dist/types/markers/props.d.ts +11 -0
  115. package/dist/types/markers/props.js +1 -0
  116. package/dist/types/plots/axis.d.ts +55 -0
  117. package/dist/types/plots/axis.js +1 -0
  118. package/dist/types/plots/constants.d.ts +30 -0
  119. package/dist/types/plots/constants.js +1 -0
  120. package/dist/types/plots/data/common.d.ts +40 -0
  121. package/dist/types/plots/data/common.js +1 -0
  122. package/dist/types/plots/data/geo.d.ts +74 -0
  123. package/dist/types/plots/data/geo.js +1 -0
  124. package/dist/types/plots/delta.d.ts +2 -2
  125. package/dist/types/plots/props.d.ts +105 -16
  126. package/dist/types/plots/scales/geo.d.ts +44 -0
  127. package/dist/types/plots/scales/geo.js +1 -0
  128. package/dist/types/plots/segments/common.d.ts +25 -0
  129. package/dist/types/plots/segments/common.js +1 -0
  130. package/dist/types/plots/segments/config.d.ts +51 -0
  131. package/dist/types/plots/segments/config.js +1 -0
  132. package/dist/types/plots/styles/common.d.ts +37 -0
  133. package/dist/types/plots/styles/common.js +1 -0
  134. package/dist/types/plots/styles/geo.d.ts +42 -0
  135. package/dist/types/plots/styles/geo.js +1 -0
  136. package/dist/types/plots/styling.d.ts +45 -0
  137. package/dist/types/plots/styling.js +1 -0
  138. package/dist/utils/grouping.d.ts +2 -1
  139. package/dist/utils/interpolate.d.ts +2 -1
  140. package/package.json +27 -13
  141. package/dist/layout/plot/PlotLayout.svelte +0 -294
  142. package/dist/layout/plot/PlotLayout.svelte.d.ts +0 -70
  143. package/dist/plots/markers/HoverMarker.svelte +0 -98
  144. package/dist/types/plots/common.d.ts +0 -97
  145. package/dist/types/plots/styles.d.ts +0 -187
  146. /package/dist/types/{plots/common.js → layout/legend.js} +0 -0
  147. /package/dist/types/{plots/markers.js → markers/common.js} +0 -0
  148. /package/dist/types/{plots/styles.js → markers/geo.js} +0 -0
@@ -0,0 +1,11 @@
1
+ import type { GeoTileLayerConfig } from '../../types/plots/styles/geo';
2
+ type $$ComponentProps = {
3
+ config: GeoTileLayerConfig;
4
+ left: number;
5
+ top: number;
6
+ width: number;
7
+ height: number;
8
+ };
9
+ declare const TileLayer: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type TileLayer = ReturnType<typeof TileLayer>;
11
+ export default TileLayer;
@@ -0,0 +1,8 @@
1
+ import type { GeoCustomProjectionFactory } from '../../../types/plots/scales/geo';
2
+ /**
3
+ * Argentina's territory in one unbroken frame: the mainland and the
4
+ * Antarctic/South Atlantic sector both visible, related to each other at
5
+ * true scale (azimuthal-equidistant, rotated so the split falls to the
6
+ * sides rather than through the middle of either half).
7
+ */
8
+ export declare const argentinaBicontinental: GeoCustomProjectionFactory;
@@ -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,24 @@
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 { getLegendInteraction } from '../../layout/legend/interaction.svelte';
9
+ import { disabledFillOverride } from '../utils/legendDisabled';
10
+ import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
11
+ import AxisLayout from '../../layout/plot/AxisLayout.svelte';
12
+ import GridLayout from '../../layout/plot/GridLayout.svelte';
13
+ import RuleLayout from '../../layout/plot/RuleLayout.svelte';
14
+ import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
15
+ import type { BasePlotProps } from '../../types/plots/props';
16
+ import type { AxisValue } from '../../types/plots/axis';
17
+ import type { SeriesProps, DataProps } from '../../types/plots/data/common';
18
+ import type { AxisBasedScalesConfig } from '../../types/plots/props';
19
+ import type { BasePlotStyles } from '../../types/plots/styles/common';
20
+ import type { CellSegmentStyle } from '../../types/plots/segments/common';
21
+ import type { Marker } from '../../types/markers/common';
11
22
 
12
23
  /**
13
24
  * A categorical grid of value-coloured cells. Accepts either a pre-built
@@ -18,6 +29,15 @@
18
29
  * The colour domain is read from `scales.z.domain`; when absent it is derived
19
30
  * from the visible slice of data.
20
31
  */
32
+ type Props = BasePlotProps<
33
+ SeriesProps<TData> | DataProps<TData>,
34
+ TData,
35
+ BasePlotStyles,
36
+ CellSegmentStyle,
37
+ Marker,
38
+ AxisBasedScalesConfig<TData>
39
+ >;
40
+
21
41
  let {
22
42
  width,
23
43
  height,
@@ -32,9 +52,10 @@
32
52
  margins,
33
53
  markers = [],
34
54
  tooltip = undefined,
35
- }: PlotProps<TData, CellSegmentStyle> = $props();
55
+ }: Props = $props();
36
56
 
37
57
  $effect(() => {
58
+ validateSegments(segments);
38
59
  const scoped = markers.filter(
39
60
  (m) => (m.type === 'hover' || m.type === 'component') && m.series,
40
61
  );
@@ -43,12 +64,10 @@
43
64
  'HeatmapPlot: markers scoped to a `series` are ignored — heatmaps have no series concept.',
44
65
  );
45
66
  }
46
- const seriesKeys = segments.flatMap((g) =>
47
- Object.keys(g as Record<string, unknown>).filter((k) => k !== 'default'),
48
- );
67
+ const seriesKeys = Object.keys(segments).filter((k) => k !== 'default');
49
68
  if (seriesKeys.length > 0) {
50
69
  console.warn(
51
- `HeatmapPlot: segment groups have series-specific keys (${[...new Set(seriesKeys)].join(', ')}) — heatmaps have no named series. Use the 'default' key instead.`,
70
+ `HeatmapPlot: segment groups have series-specific keys (${seriesKeys.join(', ')}) — heatmaps have no named series. Use the 'default' key instead.`,
52
71
  );
53
72
  }
54
73
  });
@@ -58,6 +77,9 @@
58
77
  const maxColor = $derived(styles.colors?.max ?? cfg.continuous.max);
59
78
  const showVals = $derived(styles.values?.show ?? true);
60
79
  const fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
80
+ // 'outside' is a per-point escape hatch resolved by HoverMarker; heatmaps
81
+ // have no signed axis to resolve it against, so treat it as unset.
82
+ const resolvedTextAnchor = $derived(styles.values?.textAnchor === 'outside' ? undefined : styles.values?.textAnchor);
61
83
 
62
84
  const resolvedSeries = $derived(series ?? buildSeries(data!, x!, y!, undefined));
63
85
  const flat = $derived(resolvedSeries.flatMap((s) => s.data));
@@ -66,15 +88,6 @@
66
88
  const getY = $derived(resolveAccessor<TData>(resolvedSeries[0]?.y ?? y!));
67
89
  const getZ = $derived(resolveAccessor<TData>(resolvedSeries[0]?.z ?? z!));
68
90
 
69
- const hoverPoints = $derived(
70
- flat.map((d) => ({
71
- x: getX(d) as AxisValue,
72
- y: getY(d) as AxisValue,
73
- row: d,
74
- label: fmtVal(Number(getZ(d)), ''),
75
- })),
76
- );
77
-
78
91
  const xDomain = $derived([...new Set(flat.map((d) => getX(d) as AxisValue))]);
79
92
  const yDomain = $derived([...new Set(flat.map((d) => getY(d) as AxisValue))]);
80
93
 
@@ -86,33 +99,77 @@
86
99
  return vals.length ? [Math.min(...vals, 0), Math.max(...vals)] : [0, 1];
87
100
  });
88
101
 
102
+ // ── Legend-driven range filter ────────────────────────────────────────────
103
+ const legendInteraction = $derived(getLegendInteraction());
104
+ const range = $derived(legendInteraction?.continuousRange);
105
+ // 'rescale' reprojects the ramp onto the narrowed range; 'filter' (and no
106
+ // active range) keeps the original domain — only which cells are hidden
107
+ // changes.
108
+ const effectiveDomain = $derived.by((): [number, number] =>
109
+ range && range.mode === 'rescale' ? [range.min, range.max] : valueDomain,
110
+ );
111
+
112
+ function rangeFilteredStyle(base: CellSegmentStyle, value: number): CellSegmentStyle {
113
+ const r = range;
114
+ if (r == null || (value >= r.min && value <= r.max)) return base;
115
+ return { ...base, ...disabledFillOverride(r.disabledStyle) };
116
+ }
117
+
89
118
  const colorScale = $derived(
90
- makeColorScale(valueDomain[0], valueDomain[1], minColor, maxColor),
119
+ makeColorScale(effectiveDomain[0], effectiveDomain[1], minColor, maxColor),
120
+ );
121
+
122
+ // A cell outside the active range is dimmed in place, but never surfaces
123
+ // in hover/tooltip — it's simply absent from the candidate list.
124
+ const hoverPoints = $derived(
125
+ flat
126
+ .filter((d) => {
127
+ const r = range;
128
+ if (r == null) return true;
129
+ const v = Number(getZ(d));
130
+ return v >= r.min && v <= r.max;
131
+ })
132
+ .map((d) => ({
133
+ x: getX(d) as AxisValue,
134
+ y: getY(d) as AxisValue,
135
+ row: d,
136
+ label: fmtVal(Number(getZ(d)), ''),
137
+ })),
91
138
  );
92
139
 
93
- const segList = $derived(segments.flatMap((g) => (g as Record<string, CellSegmentStyle[]>)['default'] ?? []));
140
+ const segList = $derived(segments.default ?? []);
94
141
 
142
+ /**
143
+ * A catch-all (no `areas`) always matches, so evaluating segments in plain
144
+ * array order and merging every match — as this used to do — makes a
145
+ * catch-all listed *after* an area-specific one (the documented, and only
146
+ * demonstrated, convention: baseline first, highlight second) overwrite it
147
+ * on every cell, since it's applied last unconditionally. Area-specific
148
+ * segments must always win over the catch-all regardless of list order —
149
+ * so the catch-all is applied once as the baseline, and only
150
+ * area-specific segments can override it on top, mirroring the same
151
+ * catch-all/area-specific priority every other plot kind's segments
152
+ * already follow (see `resolveSeriesSegments` in `utils/segments.ts`).
153
+ */
95
154
  const segStyleMap = $derived.by(() => {
96
155
  const map = new Map<string, CellSegmentStyle>();
97
156
  if (!segList.length) return map;
157
+ const catchAll = segList.find((s) => !s.areas || s.areas.length === 0);
158
+ const areaSegs = segList.filter((s) => s.areas && s.areas.length > 0);
98
159
  for (const d of flat) {
99
160
  const cx = getX(d) as AxisValue;
100
161
  const cy = getY(d) as AxisValue;
101
162
  const key = `${String(cx)}::${String(cy)}`;
102
163
  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
- });
164
+ let style: CellSegmentStyle = catchAll?.style ? { ...catchAll.style } : {};
165
+ for (const seg of areaSegs) {
166
+ const matches = seg.areas!.some((area) => {
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ const xOk = !area.x || ((area.x.from === undefined || (cx as any) >= area.x.from) && (area.x.to === undefined || (cx as any) <= area.x.to));
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ const yOk = !area.y || ((area.y.from === undefined || (cy as any) >= area.y.from) && (area.y.to === undefined || (cy as any) <= area.y.to));
171
+ return xOk && yOk;
172
+ });
116
173
  if (matches) style = { ...style, ...seg.style };
117
174
  }
118
175
  map.set(key, style);
@@ -122,39 +179,89 @@
122
179
  });
123
180
 
124
181
  const getCellStyle = $derived(
125
- (d: TData) => segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`) ?? {},
182
+ (d: TData) =>
183
+ rangeFilteredStyle(
184
+ segStyleMap.get(`${String(getX(d))}::${String(getY(d))}`) ?? {},
185
+ Number(getZ(d)),
186
+ ),
126
187
  );
188
+
189
+ /**
190
+ * One `<Cell>` call per *distinct resolved style*, not one call for the
191
+ * whole dataset. svelteplot registers `fillOpacity`/`strokeWidth` under
192
+ * its shared "opacity"/numeric scale machinery — passed as a per-row
193
+ * accessor that genuinely varies within a single mark instance, that
194
+ * scale collapses every row to the same (wrong) value instead of the
195
+ * literal numbers set on each segment. A literal, per-call constant (one
196
+ * call per style bucket) bypasses that entirely, which is exactly how
197
+ * bar/line avoid the same trap (one mark per visual segment). `fill`
198
+ * stays a per-cell accessor within each group — unlike fillOpacity, it
199
+ * must keep varying continuously with the underlying value wherever a
200
+ * segment doesn't explicitly override it.
201
+ */
202
+ type StyleGroup = { style: CellSegmentStyle; data: TData[] };
203
+ const styleGroups = $derived.by((): StyleGroup[] => {
204
+ const groups = new Map<string, StyleGroup>();
205
+ for (const d of flat) {
206
+ const style = getCellStyle(d);
207
+ const key = JSON.stringify(style);
208
+ let group = groups.get(key);
209
+ if (!group) {
210
+ group = { style, data: [] };
211
+ groups.set(key, group);
212
+ }
213
+ group.data.push(d);
214
+ }
215
+ return [...groups.values()];
216
+ });
217
+
218
+ // The single, fully-resolved scales object BasePlotLayout reads for
219
+ // everything (ticks, grid, sort, and the real svelteplot scale) — avoids
220
+ // keeping a second, separately-defaulted copy of `scales.x`/`scales.y`
221
+ // (our own band-scale + domain defaults) out of sync with the one
222
+ // actually forwarded.
223
+ const resolvedScales = $derived<AxisBasedScalesConfig<TData>>({
224
+ ...scales,
225
+ x: { type: 'band', labelAnchor: 'right', domain: xDomain, ...(scales.x ?? {}) },
226
+ y: { type: 'band', domain: yDomain, ...(scales.y ?? {}) },
227
+ });
228
+
229
+ const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
230
+ const hoverStrategy = $derived(resolveHoverStrategy(tooltip?.strategy, markers, 'punctual'));
231
+ const hoverSync = $derived(resolveHoverSync(tooltip?.sync, markers));
127
232
  </script>
128
233
 
129
- <PlotLayout
234
+ <BasePlotLayout
130
235
  {width}
131
236
  {height}
132
- x={{ type: 'band', labelAnchor: 'right', domain: xDomain, ...(scales.x ?? {}) }}
133
- y={{ type: 'band', domain: yDomain, ...(scales.y ?? {}) }}
134
237
  data={flat}
135
238
  {getX}
136
239
  {getY}
137
- matchY={getY}
138
240
  {hoverPoints}
139
241
  label={(d) => fmtVal(Number(getZ(d)), '')}
140
242
  {tooltip}
141
- {markers}
142
- seriesData={resolvedSeries}
143
- scales={scales}
243
+ {hoverActive}
244
+ {hoverStrategy}
245
+ {hoverSync}
246
+ scales={resolvedScales}
144
247
  {margins}
145
- showHoverLabels={false}
146
- defaultStrategy="punctual"
147
248
  >
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
- />
249
+ {#snippet children({ matchedPoints, ruleX, ruleY, impliesRuleX, impliesRuleY, numericMargins })}
250
+ <AxisLayout data={flat} {getX} {getY} {width} {height} {numericMargins} scales={resolvedScales} />
251
+ <GridLayout scales={resolvedScales} />
252
+ <RuleLayout {markers} seriesData={resolvedSeries} {matchedPoints} {ruleX} {ruleY} {impliesRuleX} {impliesRuleY} showHoverLabels={false}>
253
+ {#each styleGroups as group, i (i)}
254
+ <Cell
255
+ data={group.data as unknown as Record<string | symbol, never>[]}
256
+ x={getX}
257
+ y={getY}
258
+ fill={(d) => group.style.fill ?? colorScale(Number(getZ(d as TData)))}
259
+ fillOpacity={group.style.fillOpacity ?? 1}
260
+ stroke={group.style.stroke}
261
+ strokeWidth={group.style.strokeWidth ?? 0}
262
+ inset={0.5}
263
+ />
264
+ {/each}
158
265
 
159
266
  {#if showVals}
160
267
  <Text
@@ -162,17 +269,23 @@
162
269
  x={getX}
163
270
  y={getY}
164
271
  text={(d) => fmtVal(Number(getZ(d)), '')}
165
- fill={cfg.continuous.labelColor}
166
- fontSize={12}
167
- fontWeight={600}
168
- textAnchor={styles.values?.textAnchor ?? 'middle'}
272
+ fill={styles.values?.fill ?? cfg.continuous.labelColor}
273
+ fontSize={styles.values?.fontSize ?? 12}
274
+ fontWeight={styles.values?.fontWeight ?? 600}
275
+ fontStyle={styles.values?.fontStyle}
276
+ textAnchor={resolvedTextAnchor ?? 'middle'}
169
277
  lineAnchor={styles.values?.lineAnchor ?? 'middle'}
170
278
  dx={styles.values?.dx ?? 0}
171
279
  dy={styles.values?.dy ?? 0}
172
280
  lineHeight={styles.values?.lineHeight}
173
281
  rotate={styles.values?.rotate}
282
+ stroke={styles.values?.stroke}
283
+ strokeWidth={styles.values?.strokeWidth}
284
+ paintOrder={styles.values?.paintOrder}
174
285
  class={styles.values?.class}
175
286
  textClass={styles.values?.textClass}
176
287
  />
177
288
  {/if}
178
- </PlotLayout>
289
+ </RuleLayout>
290
+ {/snippet}
291
+ </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: {};