@fundar/data-chart-telling 1.18.0 → 1.20.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.
Files changed (252) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +2 -1
  3. package/dist/configuration/config.svelte.js +1 -1
  4. package/dist/index.d.ts +4 -2
  5. package/dist/index.js +2 -1
  6. package/dist/layout/coordinates/CoordinatesLayout.svelte +3 -1
  7. package/dist/layout/facet/FacetLayout.svelte +54 -41
  8. package/dist/layout/geometry/GeometryLayout.svelte +57 -22
  9. package/dist/layout/geometry/ScaleFreeContext.d.ts +31 -0
  10. package/dist/layout/geometry/ScaleFreeContext.js +65 -0
  11. package/dist/layout/geometry/ScaleFreeMarks.svelte +14 -0
  12. package/dist/layout/geometry/ScaleFreeMarks.svelte.d.ts +7 -0
  13. package/dist/layout/geometry/ScaleGate.svelte.d.ts +8 -0
  14. package/dist/layout/geometry/ScaleGate.svelte.js +95 -0
  15. package/dist/layout/geometry/ScaleGateMarks.svelte +15 -0
  16. package/dist/layout/geometry/ScaleGateMarks.svelte.d.ts +7 -0
  17. package/dist/layout/hover/HoverShapes.d.ts +20 -8
  18. package/dist/layout/hover/HoverShapes.js +119 -59
  19. package/dist/layout/legend/ContinuousSection.svelte +10 -0
  20. package/dist/layout/legend/LegendState.svelte.d.ts +13 -1
  21. package/dist/layout/legend/LegendState.svelte.js +32 -0
  22. package/dist/layout/margins/Box.d.ts +4 -0
  23. package/dist/layout/margins/Box.js +14 -0
  24. package/dist/layout/margins/PlotMargins.svelte.js +26 -1
  25. package/dist/layout/plot/{Plot.svelte → AbstractPlot.svelte} +83 -6
  26. package/dist/layout/plot/{Plot.svelte.d.ts → AbstractPlot.svelte.d.ts} +4 -4
  27. package/dist/layout/plot/PublicPlotContext.d.ts +5 -0
  28. package/dist/layout/plot/PublicPlotContext.js +7 -0
  29. package/dist/layout/styles/ValueLabels.d.ts +109 -58
  30. package/dist/layout/styles/ValueLabels.js +299 -141
  31. package/dist/markers/MarkerKinds.d.ts +79 -1
  32. package/dist/markers/MarkerKinds.js +149 -23
  33. package/dist/markers/area/AreaMarker.svelte +1 -1
  34. package/dist/markers/band/BandMarker.svelte +2 -1
  35. package/dist/markers/bar/BarMarker.svelte +8 -6
  36. package/dist/markers/bollinger/BollingerMarker.svelte +1 -1
  37. package/dist/markers/box/BoxMarker.svelte +1 -1
  38. package/dist/markers/dot/DotRadius.d.ts +11 -0
  39. package/dist/markers/dot/DotRadius.js +21 -0
  40. package/dist/markers/geo/GeoHoverMarker.svelte +14 -11
  41. package/dist/markers/labels/CurveLabel.svelte +56 -0
  42. package/dist/markers/labels/CurveLabel.svelte.d.ts +9 -0
  43. package/dist/markers/labels/LabelMerge.d.ts +11 -0
  44. package/dist/markers/labels/LabelMerge.js +87 -0
  45. package/dist/markers/labels/MarkerLabel.svelte +36 -0
  46. package/dist/markers/labels/MarkerLabel.svelte.d.ts +10 -0
  47. package/dist/markers/labels/MarkerLabels.d.ts +52 -0
  48. package/dist/markers/labels/MarkerLabels.js +97 -0
  49. package/dist/markers/labels/geometry/BoxGeometry.d.ts +8 -0
  50. package/dist/markers/labels/geometry/BoxGeometry.js +101 -0
  51. package/dist/markers/labels/geometry/CurveGeometry.d.ts +34 -0
  52. package/dist/markers/labels/geometry/CurveGeometry.js +193 -0
  53. package/dist/markers/labels/geometry/PointGeometry.d.ts +9 -0
  54. package/dist/markers/labels/geometry/PointGeometry.js +22 -0
  55. package/dist/markers/labels/layouts/BoxLayout.d.ts +10 -0
  56. package/dist/markers/labels/layouts/BoxLayout.js +49 -0
  57. package/dist/markers/labels/layouts/PathLayout.d.ts +11 -0
  58. package/dist/markers/labels/layouts/PathLayout.js +91 -0
  59. package/dist/markers/labels/layouts/PointLayout.d.ts +19 -0
  60. package/dist/markers/labels/layouts/PointLayout.js +94 -0
  61. package/dist/markers/regression/RegressionMarker.svelte +1 -1
  62. package/dist/markers/text/Covering.d.ts +13 -0
  63. package/dist/markers/text/Covering.js +29 -0
  64. package/dist/markers/text/Declutter.d.ts +24 -3
  65. package/dist/markers/text/Declutter.js +46 -3
  66. package/dist/markers/text/TextMarker.svelte +90 -17
  67. package/dist/markers/text/TextMarker.svelte.d.ts +1 -1
  68. package/dist/markers/tick/TickMarker.svelte +1 -1
  69. package/dist/markers/utils/MarkerChannels.d.ts +1 -1
  70. package/dist/markers/utils/MarkerChannels.js +3 -2
  71. package/dist/markers/violin/ViolinMarker.svelte +1 -1
  72. package/dist/markers/waffle/WaffleMarker.svelte +1 -1
  73. package/dist/phases/context/RowContext.d.ts +2 -0
  74. package/dist/phases/context/RowContext.js +4 -2
  75. package/dist/phases/context/rowContextKey.d.ts +2 -0
  76. package/dist/phases/context/rowContextKey.js +2 -0
  77. package/dist/phases/markers/MarkerStyles.js +14 -11
  78. package/dist/phases/markers/MarkersPhase.d.ts +2 -3
  79. package/dist/phases/markers/MarkersPhase.js +17 -11
  80. package/dist/phases/sharedScales/SharedStepScales.d.ts +10 -0
  81. package/dist/phases/sharedScales/SharedStepScales.js +43 -0
  82. package/dist/phases/statistics/MarkerGrouping.js +5 -32
  83. package/dist/phases/statistics/MarkerInheritance.d.ts +11 -0
  84. package/dist/phases/statistics/MarkerInheritance.js +25 -0
  85. package/dist/phases/statistics/MarkerOrientation.d.ts +26 -0
  86. package/dist/phases/statistics/MarkerOrientation.js +29 -0
  87. package/dist/phases/statistics/StatisticsPhase.js +15 -15
  88. package/dist/phases/transforms/TransformsPhase.js +5 -3
  89. package/dist/plots/correlation/heatmap/Plot.svelte +2 -2
  90. package/dist/plots/correlation/heatmap/Plot.svelte.d.ts +0 -1
  91. package/dist/plots/correlation/heatmap/defaults.js +14 -1
  92. package/dist/plots/correlation/heatmap/markers.d.ts +0 -6
  93. package/dist/plots/correlation/heatmap/markers.js +5 -9
  94. package/dist/plots/correlation/scatter/Plot.svelte +2 -2
  95. package/dist/plots/correlation/scatter/Plot.svelte.d.ts +0 -1
  96. package/dist/plots/correlation/scatter/defaults.js +9 -3
  97. package/dist/plots/correlation/scatter/markers.d.ts +0 -9
  98. package/dist/plots/correlation/scatter/markers.js +21 -28
  99. package/dist/plots/distribution/box/BoxSummaries.d.ts +5 -0
  100. package/dist/plots/distribution/box/BoxSummaries.js +9 -0
  101. package/dist/plots/distribution/box/Plot.svelte +2 -2
  102. package/dist/plots/distribution/box/Plot.svelte.d.ts +0 -1
  103. package/dist/plots/distribution/box/defaults.js +11 -0
  104. package/dist/plots/distribution/box/markers.js +10 -1
  105. package/dist/plots/distribution/density/Plot.svelte +2 -2
  106. package/dist/plots/distribution/density/Plot.svelte.d.ts +0 -1
  107. package/dist/plots/distribution/density/defaults.js +16 -0
  108. package/dist/plots/distribution/density/markers.js +5 -1
  109. package/dist/plots/distribution/histogram/Plot.svelte +2 -2
  110. package/dist/plots/distribution/histogram/Plot.svelte.d.ts +0 -1
  111. package/dist/plots/distribution/histogram/defaults.js +16 -11
  112. package/dist/plots/distribution/histogram/markers.js +3 -3
  113. package/dist/plots/distribution/pyramid/Plot.svelte +2 -2
  114. package/dist/plots/distribution/pyramid/Plot.svelte.d.ts +0 -1
  115. package/dist/plots/distribution/pyramid/defaults.js +9 -2
  116. package/dist/plots/distribution/pyramid/markers.d.ts +0 -8
  117. package/dist/plots/distribution/pyramid/markers.js +6 -16
  118. package/dist/plots/distribution/violin/Plot.svelte +2 -2
  119. package/dist/plots/distribution/violin/Plot.svelte.d.ts +0 -1
  120. package/dist/plots/distribution/violin/defaults.js +11 -0
  121. package/dist/plots/distribution/violin/markers.js +27 -3
  122. package/dist/plots/evolution/area/Plot.svelte +2 -2
  123. package/dist/plots/evolution/area/Plot.svelte.d.ts +0 -1
  124. package/dist/plots/evolution/area/defaults.js +4 -7
  125. package/dist/plots/evolution/area/markers.js +3 -1
  126. package/dist/plots/evolution/line/Plot.svelte +2 -2
  127. package/dist/plots/evolution/line/Plot.svelte.d.ts +0 -1
  128. package/dist/plots/evolution/line/defaults.js +4 -6
  129. package/dist/plots/evolution/line/markers.js +3 -1
  130. package/dist/plots/evolution/slope/Plot.svelte +2 -2
  131. package/dist/plots/evolution/slope/Plot.svelte.d.ts +0 -1
  132. package/dist/plots/evolution/slope/defaults.js +4 -3
  133. package/dist/plots/evolution/slope/markers.d.ts +4 -3
  134. package/dist/plots/evolution/slope/markers.js +16 -9
  135. package/dist/plots/evolution/sparkline/Plot.svelte +2 -2
  136. package/dist/plots/evolution/sparkline/Plot.svelte.d.ts +0 -1
  137. package/dist/plots/evolution/sparkline/defaults.js +1 -1
  138. package/dist/plots/evolution/sparkline/markers.js +3 -1
  139. package/dist/plots/generic/Plot.svelte +42 -0
  140. package/dist/plots/generic/Plot.svelte.d.ts +25 -0
  141. package/dist/plots/generic/defaults.d.ts +4 -0
  142. package/dist/plots/generic/defaults.js +7 -0
  143. package/dist/plots/map/geo/Core.svelte +29 -6
  144. package/dist/plots/map/geo/Plot.svelte +40 -12
  145. package/dist/plots/map/geo/Plot.svelte.d.ts +0 -1
  146. package/dist/plots/map/geo/phases/data.d.ts +2 -1
  147. package/dist/plots/map/geo/phases/data.js +8 -2
  148. package/dist/plots/map/geo/phases/scales.js +6 -2
  149. package/dist/plots/map/geo/slices.d.ts +27 -3
  150. package/dist/plots/map/geo/slices.js +58 -5
  151. package/dist/plots/part-of-a-whole/treemap/Plot.svelte +2 -2
  152. package/dist/plots/part-of-a-whole/treemap/Plot.svelte.d.ts +0 -1
  153. package/dist/plots/part-of-a-whole/treemap/defaults.js +2 -2
  154. package/dist/plots/ranking/bar/Plot.svelte +2 -2
  155. package/dist/plots/ranking/bar/Plot.svelte.d.ts +0 -1
  156. package/dist/plots/ranking/bar/defaults.js +13 -3
  157. package/dist/plots/ranking/bar/markers.js +12 -29
  158. package/dist/plots/ranking/bullet/Plot.svelte +2 -2
  159. package/dist/plots/ranking/bullet/Plot.svelte.d.ts +0 -1
  160. package/dist/plots/ranking/bullet/defaults.js +20 -0
  161. package/dist/plots/ranking/bullet/markers.js +7 -1
  162. package/dist/plots/ranking/cleveland/ClevelandMarkers.d.ts +2 -9
  163. package/dist/plots/ranking/cleveland/ClevelandMarkers.js +65 -47
  164. package/dist/plots/ranking/cleveland/Plot.svelte +2 -2
  165. package/dist/plots/ranking/cleveland/Plot.svelte.d.ts +0 -1
  166. package/dist/plots/ranking/cleveland/defaults.js +33 -2
  167. package/dist/plots/ranking/dumbbell/Plot.svelte +2 -2
  168. package/dist/plots/ranking/dumbbell/Plot.svelte.d.ts +0 -1
  169. package/dist/plots/ranking/forest/Plot.svelte +2 -2
  170. package/dist/plots/ranking/forest/Plot.svelte.d.ts +0 -1
  171. package/dist/plots/ranking/lollipop/Plot.svelte +2 -2
  172. package/dist/plots/ranking/lollipop/Plot.svelte.d.ts +0 -1
  173. package/dist/types/layout/context.d.ts +2 -0
  174. package/dist/types/layout/plot.d.ts +2 -0
  175. package/dist/types/layout/styles/bullet.d.ts +4 -1
  176. package/dist/types/layout/styles/geo.d.ts +3 -3
  177. package/dist/types/layout/styles/values.d.ts +51 -11
  178. package/dist/types/markers/axis/area.d.ts +5 -1
  179. package/dist/types/markers/axis/bar.d.ts +5 -2
  180. package/dist/types/markers/axis/bollinger.d.ts +2 -1
  181. package/dist/types/markers/axis/box.d.ts +2 -1
  182. package/dist/types/markers/axis/cell.d.ts +4 -1
  183. package/dist/types/markers/axis/line.d.ts +4 -1
  184. package/dist/types/markers/axis/rect.d.ts +4 -1
  185. package/dist/types/markers/axis/regression.d.ts +2 -1
  186. package/dist/types/markers/axis/tick.d.ts +2 -1
  187. package/dist/types/markers/axis/trail.d.ts +4 -1
  188. package/dist/types/markers/axis/violin.d.ts +2 -1
  189. package/dist/types/markers/axis/waffle.d.ts +5 -1
  190. package/dist/types/markers/basic/dot.d.ts +4 -1
  191. package/dist/types/markers/basic/text.d.ts +25 -21
  192. package/dist/types/markers/channels/commonMarkerConfigWithLabel.d.ts +5 -9
  193. package/dist/types/markers/channels/commonMarkerConfigWithOrientation.d.ts +5 -0
  194. package/dist/types/markers/channels/commonMarkerConfigWithOrientation.js +1 -0
  195. package/dist/types/markers/labels/box.d.ts +57 -0
  196. package/dist/types/markers/labels/box.js +1 -0
  197. package/dist/types/markers/labels/common.d.ts +15 -0
  198. package/dist/types/markers/labels/common.js +1 -0
  199. package/dist/types/markers/labels/path.d.ts +52 -0
  200. package/dist/types/markers/labels/path.js +1 -0
  201. package/dist/types/markers/labels/point.d.ts +40 -0
  202. package/dist/types/markers/labels/point.js +1 -0
  203. package/dist/types/markers/traits.d.ts +10 -0
  204. package/dist/types/phases/markers/phase.d.ts +1 -8
  205. package/dist/types/phases/statistics/phase.d.ts +8 -3
  206. package/dist/types/phases/styles/phase.d.ts +15 -3
  207. package/dist/types/phases/transforms/phase.d.ts +2 -0
  208. package/dist/types/plots/constants.d.ts +0 -12
  209. package/dist/types/plots/correlation/heatmap/props.d.ts +3 -2
  210. package/dist/types/plots/correlation/scatter/props.d.ts +3 -2
  211. package/dist/types/plots/data/geo.d.ts +19 -5
  212. package/dist/types/plots/distribution/box/props.d.ts +6 -2
  213. package/dist/types/plots/distribution/density/props.d.ts +2 -1
  214. package/dist/types/plots/distribution/histogram/props.d.ts +3 -2
  215. package/dist/types/plots/distribution/pyramid/props.d.ts +3 -2
  216. package/dist/types/plots/distribution/violin/props.d.ts +4 -1
  217. package/dist/types/plots/evolution/area/props.d.ts +3 -2
  218. package/dist/types/plots/evolution/line/props.d.ts +3 -2
  219. package/dist/types/plots/generic/props.d.ts +16 -0
  220. package/dist/types/plots/generic/props.js +1 -0
  221. package/dist/types/plots/generic/state.d.ts +4 -0
  222. package/dist/types/plots/generic/state.js +1 -0
  223. package/dist/types/plots/map/geo/props.d.ts +9 -3
  224. package/dist/types/plots/map/geo/state.d.ts +3 -1
  225. package/dist/types/plots/part-of-a-whole/treemap/props.d.ts +4 -2
  226. package/dist/types/plots/ranking/bar/props.d.ts +3 -2
  227. package/dist/types/plots/ranking/cleveland/props.d.ts +3 -2
  228. package/dist/types/plots/ranking/dumbbell/props.d.ts +3 -2
  229. package/dist/types/plots/ranking/forest/props.d.ts +5 -2
  230. package/dist/types/plots/ranking/lollipop/props.d.ts +3 -2
  231. package/dist/utils/ConfigEquality.d.ts +11 -0
  232. package/dist/utils/ConfigEquality.js +37 -0
  233. package/docs/architecture.md +9 -9
  234. package/docs/facets.md +12 -0
  235. package/docs/geo.md +35 -7
  236. package/docs/legend.md +6 -0
  237. package/docs/markers.md +2 -4
  238. package/docs/plots/ranking/bar.md +1 -1
  239. package/docs/plots/ranking/cleveland.md +1 -1
  240. package/docs/state.md +1 -1
  241. package/docs/styles-and-segments.md +1 -14
  242. package/docs/timeline.md +1 -1
  243. package/docs/transforms-and-statistics.md +1 -1
  244. package/docs/value-labels.md +159 -0
  245. package/examples/BarValues.svelte +1 -1
  246. package/llms-full.txt +5 -5
  247. package/llms.txt +1 -0
  248. package/package.json +3 -1
  249. package/dist/markers/utils/BoxLabel.d.ts +0 -58
  250. package/dist/markers/utils/BoxLabel.js +0 -175
  251. package/dist/markers/utils/MarkerLabel.svelte +0 -33
  252. package/dist/markers/utils/MarkerLabel.svelte.d.ts +0 -10
package/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [1.20.0] - 2026-11-11
6
+
7
+ ### Added
8
+
9
+ - **`GeoPlot` facets and timeline from one layer of features.** `facet.values` (or a grid's `rowValues` and `colValues`) and `timeline.values` now create their panels and steps though no feature carries the field, so the same features serve every panel with no copy per value. A slice's values are the ones named, in order, then any more found in the features.
10
+ - **The slice reaches the callbacks of a `GeoPlot`.** `color`, `opacity`, `grouping`, `featureId` and `styles.onFeatureClick` get `{ facet?: { value, row?, col? }, time? }` as their last argument (empty without `facet` or `timeline`), and a tooltip's `format` has the same `facet` and `time` in its context: `color={(f, { facet }) => valueFor(f, facet?.value)}`. The color ramp spans every panel and step, each read in its own slice, unless `facet.scales` (or `timeline.scales`, with no facet) is `'free'`: then each slice fits its own, and a `grouping` by panel with a `segments` `scales.color` per panel gives each its own scheme - and the legend leaves out the plot's own ramp when every colored feature has one of its own. New type `GeoSliceContext`. See [Maps](docs/geo.md#facets-and-timeline).
11
+ - **`useFigure().legend.setRange([min, max] | null, mode?)`.** Narrows an interactive continuous legend from code as dragging its handles does: they move and the plots filter or rescale to the range, in the section's own mode unless one is given. `null`, or a span covering the whole ramp, resets it. For a figure that draws its own mini legends, such as one per map panel, to apply a panel's range to the shared legend. See [Legend](docs/legend.md).
12
+
13
+ ### Fixed
14
+
15
+ - **`BarPlot` hover on grouped series.** Each series' hover zone now covers only its own strip of the category band, so side-by-side bars report the series under the pointer instead of always the last one.
16
+ - **`band` marker on grouped horizontal bars.** A `slice` of a band on `y` is counted from the bottom, as the bars, their hit areas and the hover's dot, text and rule are, so the hover's band lies over the series under the pointer and not the other one.
17
+ - **`GeoPlot` hover no longer lays the map out again.** The hovered province is drawn on the plot's projection without joining its scales, so entering or leaving one no longer works out the scales and projection anew - which, with facets and a synced tooltip, was repeated in every panel.
18
+ - **Facet grids no longer crawl through a timeline.** A step used to give `FacetLayout` new entry objects, which its each blocks wrote into their items from inside their own effect, and Svelte then walked every derived the write reached, in each panel. The blocks now iterate the panels' keys, and each cell reads its entry from a derived.
19
+ - **Shared domains on a stacked, faceted timeline.** With a `timeline` that shows each step's own rows (`filterMode: 'exact'`), a shared value axis was fitted to the rows of every step stacked together. It now reaches the largest total any step stacks in any panel. With `'upTo'` a step shows every row up to it, so the last step's stack still sets it.
20
+ - **Docs: horizontal stacked bars use `stackX`.** The bar plot's page, the transforms page and the LLM notes said stacking is always `stackY`, also for horizontal bars. A stack runs along the value axis: `stackY` with values on `y` (vertical bars), `stackX` with values on `x` (horizontal bars); the other makes the category axis numeric and the bars come out empty.
21
+ - **A faceted plot repaints when only its `segments` change.** The data phases are run again when a prop of the plot, the configuration, the rows or the facet changes - props that are configuration (`segments`, `styles`, `scales`...) compared as they hold, so building the same ones again changes nothing - where before a change of `segments` alone left every panel as it was unless `data` changed too. Moving a timeline or a pointer still runs nothing again.
22
+
23
+ ## [1.19.0] - 2026-11-11
24
+
25
+ ### Added
26
+
27
+ - **Value labels, laid out three ways.** Every plot's `styles.values`, and every marker's `label`, takes a `layout`:
28
+ - `'point'` - beside a point: a line's last value, a scatter's dots, a corner of a bar. `side` (`'after'`, `'before'`, `'above'`, `'below'`, `'on'`) and `gap` place it.
29
+ - `'path'` - along an edge or a stroke: past a bar's tip (its sign followed per bar), along a line or an area's edge, around a dot's rim. `edge`, `side` and `offset` place it, `offset: 'max'` putting it at each curve's peak.
30
+ - `'box'` - inside each shape, fitted to it: a heatmap's cells, a stacked bar's segments.
31
+
32
+ Each plot kind keeps its own default and takes only the layouts its shapes can write, checked by its types: `<LinePlot styles={{ values: { layout: 'path' } }} />` writes each series' name along its line. See the new [Value labels](docs/value-labels.md) page and the _Layout/Values_ stories.
33
+
34
+ - **Labels that stay readable.** On a scatter, labels move clockwise around their dot to the first free spot, and are left out where there is none (`declutter: 'around'`, also on a marker's label or a `text` marker). A value written on its own dot is hidden where a later dot covers it.
35
+ - **Several values per row.** `styles.values.parts` picks which values a row writes, typed per plot: a box's or violin's `'min'`, `'p25'`, `'median'`, `'p75'`, `'max'` and `'outliers'`, a bullet's `'target'` and `'ranges'`, a forest's `'lower'` and `'upper'` - as a list, or a record with options for each (`parts: { median: true, max: { format } }`). A label's `format` gets the part in `context.part`.
36
+ - **Value labels on `BoxPlot`, `ViolinPlot` and `DensityPlot`.** A box writes its whisker ends and median, a violin its median, a density each curve's name at its peak.
37
+ - **More on value labels:** a marker's `label` can be a list; any option of `styles.values` turns the labels on, without `show: true`; `select` and `series` now also pick the labelled rows of bar, histogram, heatmap and scatter plots.
38
+ - **A generic `Plot`** that draws only its `markers`, with every option a plot takes, for annotations and custom compositions: `<Plot markers={[{ type: 'density', data, x: 'gdp', y: 'life' }]} />`.
39
+ - **Markers inherit from their plot** the position and channels they leave unset (`x`, `y`, `opacity`, `symbol`, `r`, `length`, with their scales), and run horizontally or vertically by their own channels when the plot has none.
40
+
41
+ ### Changed
42
+
43
+ - **Breaking:** `styles.values.anchor` is replaced by `layout` and its options. On a line plot, `'start'` is now `side: 'after'` (the default), `'end'` is `side: 'before'`, `'outside'` is `side: 'above'`, `'middle'` is `side: 'on'`. On a bar plot, `'outside'` is the default, `'tip'` and `'base'` are `{ layout: 'path', edge: 'tip' | 'base', side: 'inside' }`, and `'middle'` or a page edge is `{ layout: 'box', anchor }`.
44
+ - **Breaking:** `styles.values.fontStyle` only styles the text: `dx`, `dy`, `textAnchor` and `lineAnchor` are gone, the layout places it (`fontStyle: { dy: -12 }` is `gap: 12`).
45
+ - **Breaking:** a shape label's `outside` is gone: `{ anchor: 'top', outside: true }` is `{ layout: 'path', edge: 'top' }`.
46
+ - **Breaking:** the plot shell exported as `Plot` (to build your own plot kinds) is now `AbstractPlot`; `Plot` is the new generic plot.
47
+ - A scatter's value labels sit above their dots by default, and no longer overlap.
48
+
49
+ ### Fixed
50
+
51
+ - `styles.values.color` now colors the labels of bar, pyramid, histogram and scatter plots.
52
+ - A violin plot colored by its category draws each violin centered in its band, instead of side by side with empty slots.
53
+ - The x axis title no longer lands on the tick labels when value labels grow the plot's margins.
54
+ - A label along a line, near one of its ends, is no longer cut short.
55
+ - **Hovering no longer lays the whole plot out again at every move of the pointer.** svelteplot works a plot's scales out from every mark's rows, and the hover changed them at each move: its dots, rules and values came and went as marks of the plot, the `active` and `others` styles split a marker's rows anew, and a mark copied its rows whenever one of their styles changed. Each move rebuilt every scale from every row, scaled every mark again and measured the margins again, more so the more series, rows and synced panels a plot has. Now the hover's `back` and `front` markers are drawn on the plot's scales without taking part in them, a marker the hover restyles stays whole with each style read off the row as it is hovered, and the plot works its scales out again only when a mark's rows hold something new. A move of the pointer over a fifteen-line plot settles in about a quarter of the time, and over a treemap or a heatmap that restyles its shapes in a tenth to a quarter.
56
+ - The outline the `hover` marker's `active` gives the hovered shapes (its `stroke`, `strokeWidth`, `strokeOpacity` and `strokeDasharray`, the plot's own `hover.active` by default) is drawn whole, over every shape of the plot. It was drawn on the shape itself, in its marker's order, so the tiles of a treemap, the cells of a heatmap with no gap or the bars of a plot drawn after it covered part of it. The outline never takes the pointer from the shape under it.
57
+
5
58
  ## [1.18.0] - 2026-11-09
6
59
 
7
60
  ### Added
package/README.md CHANGED
@@ -136,7 +136,8 @@ Start with the concepts; the rest is organised by what you want a plot to do.
136
136
  | [Concepts](docs/concepts.md) | Plots and figures, rows and series, channels, the props every plot shares |
137
137
  | [Plots](docs/plots.md) | Every plot kind: what it draws and what it needs |
138
138
  | [Scales](docs/scales.md) | Axes (`x`, `y`) and channel scales (`color`, `grouping`, `opacity`, `symbol`, `r`, `length`) |
139
- | [Styles and segments](docs/styles-and-segments.md) | Value labels, frames, gap filling, and styling ranges of the data |
139
+ | [Styles and segments](docs/styles-and-segments.md) | Value labels in brief, frames, gap filling, and styling ranges of the data |
140
+ | [Value labels](docs/value-labels.md) | A plot's values on its shapes, and a marker's labels: layouts and their options |
140
141
  | [Markers](docs/markers.md) | Extra marks on a plot: annotations, regions, hover highlights, custom components |
141
142
  | [Transforms and statistics](docs/transforms-and-statistics.md) | Stacking, binning, smoothing, regression and the other data transforms |
142
143
  | [Tooltip and hover](docs/tooltip-and-hover.md) | Floating tooltips, in-plot highlights, syncing hover across plots |
@@ -158,7 +158,7 @@ export const DEFAULT_CONFIG = {
158
158
  border: 'none'
159
159
  },
160
160
  hover: {
161
- rule: { stroke: '#e6580d', strokeOpacity: 0.35, strokeWidth: 1.5, strokeDasharray: '4 4' },
161
+ rule: { stroke: '#64748b', strokeOpacity: 0.35, strokeWidth: 1.5, strokeDasharray: '4 4' },
162
162
  dot: { dotRadius: 5, dotFill: 'currentColor' },
163
163
  font: { fill: 'currentColor', fontSize: 12, dy: -18 },
164
164
  area: { fill: '#94a3b8', fillOpacity: 0.4 },
package/dist/index.d.ts CHANGED
@@ -19,7 +19,8 @@ export { default as TreemapPlot } from './plots/part-of-a-whole/treemap/Plot.sve
19
19
  export { default as ViolinPlot } from './plots/distribution/violin/Plot.svelte';
20
20
  export { default as PlotComposer } from './layout/composer/PlotComposer.svelte';
21
21
  export { default as GridCell } from './layout/grid/GridCell.svelte';
22
- export { default as Plot } from './layout/plot/Plot.svelte';
22
+ export { default as Plot } from './plots/generic/Plot.svelte';
23
+ export { default as AbstractPlot } from './layout/plot/AbstractPlot.svelte';
23
24
  export { default as BasePlotLayout } from './layout/base/BasePlotLayout.svelte';
24
25
  export { usePlot, useFigure } from './layout/plot/usePlot';
25
26
  export { RowContext } from './phases/context/RowContext';
@@ -44,6 +45,7 @@ export type { AxisScale, AxisBasedScalesConfig, ChannelScalesConfig } from './ty
44
45
  export type { NumberDisplay } from './types/layout/numbers';
45
46
  export type { TreemapPlotProps, TreemapRowContext, TreemapTooltipContext } from './types/plots/part-of-a-whole/treemap/props';
46
47
  export type { TreemapTile } from './types/transforms/treemap';
48
+ export type { GenericPlotProps, GenericDataProps } from './types/plots/generic/props';
47
49
  export type { Accessor, Series, SeriesProps, NonAxisDataProps, SingleAxisDataProps, DualAxisDataProps } from './types/plots/data/common';
48
50
  export type { BasePlotProps, PlotCoreProps, PlotChromeProps, FigureBoxProps, SegmentsConfig, ScalesConfig, MarginConfig, TooltipConfig, HoverConfig } from './types/plots/props';
49
51
  export type { AxisBasedStylesConfig, ConnectorBasedStylesConfig, BulletStylesConfig, GapsAwarePlotStylesConfig, CurveAwarePlotStylesConfig, GeoStylesConfig, GeoTileLayerConfig } from './types/layout/styles';
@@ -55,7 +57,7 @@ export type { FontStyle, StrokeStyle, DotStyle, AreaStyle } from './types/plots/
55
57
  export type { AxisBasedMarkersConfig, BrushSelection, HoverMarkerConfig, HoverShapeStyle, HoverLayerMarker } from './types/markers/common';
56
58
  export type { StatisticConfig } from './types/layout/statistics';
57
59
  export type * from './types/transforms';
58
- export type { GeoFeature, GeoJsonInput, GeoDataProps, Topology } from './types/plots/data/geo';
60
+ export type { GeoFeature, GeoJsonInput, GeoDataProps, GeoSliceContext, Topology } from './types/plots/data/geo';
59
61
  export type { GeoCustomProjection, GeoCustomProjectionFactory, GeoScalesConfig, CoordinatesConfig } from './types/layout/coordinates';
60
62
  export type { GeoCameraConfig } from './types/plots/camera';
61
63
  export type { GeoMarkersConfig } from './types/markers/geo';
package/dist/index.js CHANGED
@@ -25,7 +25,8 @@ export { default as ViolinPlot } from './plots/distribution/violin/Plot.svelte';
25
25
  export { default as PlotComposer } from './layout/composer/PlotComposer.svelte';
26
26
  export { default as GridCell } from './layout/grid/GridCell.svelte';
27
27
  // The generic plot, to build your own plot kind: wrap it with your kind's core and defaults.
28
- export { default as Plot } from './layout/plot/Plot.svelte';
28
+ export { default as Plot } from './plots/generic/Plot.svelte';
29
+ export { default as AbstractPlot } from './layout/plot/AbstractPlot.svelte';
29
30
  export { default as BasePlotLayout } from './layout/base/BasePlotLayout.svelte';
30
31
  export { usePlot, useFigure } from './layout/plot/usePlot';
31
32
  // What a plot knows of each row it draws, which every format reads.
@@ -2,6 +2,7 @@
2
2
  import type { ComponentProps, Snippet } from 'svelte';
3
3
  import { geoCentroid, type GeoPermissibleObjects } from 'd3-geo';
4
4
  import { Plot } from 'svelteplot';
5
+ import ScaleGateMarks from '../geometry/ScaleGateMarks.svelte';
5
6
  import { resolveProjection } from './resolveProjection';
6
7
  import { resolveCameraTarget } from './resolveCameraTarget';
7
8
  import { createGeoZoom, type GeoZoomTransform } from './zoom.svelte';
@@ -182,7 +183,8 @@
182
183
  </defs>
183
184
  <g clip-path="url(#{clipId})">
184
185
  <ProjectionZoom inner={geoZoom.transform} outer={cameraTransform}>
185
- {@render children()}
186
+ <!-- The plot's scales follow what its marks' rows hold, not every copy a mark makes of them. -->
187
+ <ScaleGateMarks>{@render children()}</ScaleGateMarks>
186
188
  </ProjectionZoom>
187
189
  </g>
188
190
  </Plot>
@@ -139,6 +139,13 @@
139
139
  });
140
140
  });
141
141
 
142
+ // The panels' keys hold still while their data changes, and each cell reads its own entry by key. A timeline step
143
+ // gives every entry a new object; an each block over the entries would write each into its item from inside its
144
+ // own effect, and Svelte then walks every derived that state reaches, panel after panel.
145
+ const entryByKey = $derived(new Map([...wrapEntries, ...matrixEntries].map((e) => [e.key, e])));
146
+ const wrapKeys = $derived(wrapEntries.map((e) => e.key));
147
+ const matrixKeys = $derived(matrixEntries.map((e) => e.key));
148
+
142
149
  // ── wrapped modes ────────────────────────────────────────────────────────
143
150
  const resolvedMode = $derived(wrap ? resolveResponsive(mode, $isMobile) : 'grid');
144
151
 
@@ -150,10 +157,10 @@
150
157
  const resolvedPageSize = $derived(Math.max(1, pageSize ?? gridCols * gridRows));
151
158
  let page = $state(0);
152
159
  const totalPages = $derived(Math.max(1, Math.ceil(wrapEntries.length / resolvedPageSize)));
153
- const pageEntries = $derived(
160
+ const pageKeys = $derived(
154
161
  resolvedMode === 'paginated'
155
- ? wrapEntries.slice(page * resolvedPageSize, page * resolvedPageSize + resolvedPageSize)
156
- : wrapEntries
162
+ ? wrapKeys.slice(page * resolvedPageSize, page * resolvedPageSize + resolvedPageSize)
163
+ : wrapKeys
157
164
  );
158
165
 
159
166
  $effect(() => {
@@ -162,7 +169,7 @@
162
169
 
163
170
  const showNav = $derived(resolvedMode === 'paginated' && totalPages > 1);
164
171
  // A page sizes its grid off what it shows, not the full entry count.
165
- const pageCols = $derived(Math.max(1, Math.min(gridCols, pageEntries.length || 1)));
172
+ const pageCols = $derived(Math.max(1, Math.min(gridCols, pageKeys.length || 1)));
166
173
  const gridHeight = $derived(
167
174
  navLayout === 'below' && showNav ? Math.max(0, height - NAV_BELOW_H) : height
168
175
  );
@@ -255,28 +262,31 @@
255
262
  </span>
256
263
  {/snippet}
257
264
 
258
- {#snippet facetCells(entries: Entry[], columnCount: number)}
259
- {#each entries as entry, i (entry.key)}
260
- <GridCell
261
- row={entry.r ?? Math.floor(i / columnCount)}
262
- col={entry.c ?? i % columnCount}
263
- label={wrap ? entry.label : undefined}
264
- data-row={entry.row}
265
- data-col={entry.col}
266
- >
267
- {#snippet children(box)}
268
- {@render cell({
269
- facetValue: entry.label,
270
- row: entry.row,
271
- col: entry.col,
272
- data: entry.data,
273
- width: box.width,
274
- height: box.height,
275
- margins: box.margins,
276
- edges: box.edges
277
- })}
278
- {/snippet}
279
- </GridCell>
265
+ {#snippet facetCells(keys: string[], columnCount: number)}
266
+ {#each keys as key, i (key)}
267
+ {@const entry = entryByKey.get(key)}
268
+ {#if entry}
269
+ <GridCell
270
+ row={entry.r ?? Math.floor(i / columnCount)}
271
+ col={entry.c ?? i % columnCount}
272
+ label={wrap ? entry.label : undefined}
273
+ data-row={entry.row}
274
+ data-col={entry.col}
275
+ >
276
+ {#snippet children(box)}
277
+ {@render cell({
278
+ facetValue: entry.label,
279
+ row: entry.row,
280
+ col: entry.col,
281
+ data: entry.data,
282
+ width: box.width,
283
+ height: box.height,
284
+ margins: box.margins,
285
+ edges: box.edges
286
+ })}
287
+ {/snippet}
288
+ </GridCell>
289
+ {/if}
280
290
  {/each}
281
291
  {/snippet}
282
292
 
@@ -291,7 +301,7 @@
291
301
  colLabels={getCol ? colKeys : undefined}
292
302
  rowLabels={getRow ? rowKeys : undefined}
293
303
  >
294
- {@render facetCells(matrixEntries, colKeys.length)}
304
+ {@render facetCells(matrixKeys, colKeys.length)}
295
305
  </Grid>
296
306
  {:else if resolvedMode === 'carousel'}
297
307
  <div
@@ -310,19 +320,22 @@
310
320
  style:gap="{gap}px"
311
321
  style:transform="translateX({translate}px)"
312
322
  >
313
- {#each wrapEntries as entry (entry.key)}
314
- <div class="dct-facet-carousel__cell" style:width="{carouselCellWidth}px">
315
- <div class="dct-facet-carousel__title" style:height="{TITLE_H}px">{entry.label}</div>
316
- {#if carouselCellWidth > 0 && carouselCellHeight > 0}
317
- {@render cell({
318
- facetValue: entry.label,
319
- data: entry.data,
320
- width: carouselCellWidth,
321
- height: carouselCellHeight,
322
- edges: CAROUSEL_EDGES
323
- })}
324
- {/if}
325
- </div>
323
+ {#each wrapKeys as key (key)}
324
+ {@const entry = entryByKey.get(key)}
325
+ {#if entry}
326
+ <div class="dct-facet-carousel__cell" style:width="{carouselCellWidth}px">
327
+ <div class="dct-facet-carousel__title" style:height="{TITLE_H}px">{entry.label}</div>
328
+ {#if carouselCellWidth > 0 && carouselCellHeight > 0}
329
+ {@render cell({
330
+ facetValue: entry.label,
331
+ data: entry.data,
332
+ width: carouselCellWidth,
333
+ height: carouselCellHeight,
334
+ edges: CAROUSEL_EDGES
335
+ })}
336
+ {/if}
337
+ </div>
338
+ {/if}
326
339
  {/each}
327
340
  </div>
328
341
  {#if wrapEntries.length > 1}
@@ -333,7 +346,7 @@
333
346
  <div class="dct-facet-paginated" style:width="{width}px" style:height="{height}px">
334
347
  {#key page}
335
348
  <Grid {width} height={gridHeight} {gap} {align} header={TITLE_H} columns={pageCols}>
336
- {@render facetCells(pageEntries, pageCols)}
349
+ {@render facetCells(pageKeys, pageCols)}
337
350
  </Grid>
338
351
  {/key}
339
352
  {#if showNav}
@@ -40,9 +40,10 @@
40
40
  import ContourMarker from '../../markers/contour/ContourMarker.svelte';
41
41
  import ContourClipMarker from '../../markers/contour/ContourClipMarker.svelte';
42
42
  import InsetMarker from '../../markers/inset/InsetMarker.svelte';
43
- import MarkerLabel from '../../markers/utils/MarkerLabel.svelte';
43
+ import MarkerLabel from '../../markers/labels/MarkerLabel.svelte';
44
44
  import FrameClip from './FrameClip.svelte';
45
45
  import MarkerOffset from './MarkerOffset.svelte';
46
+ import ScaleFreeMarks from './ScaleFreeMarks.svelte';
46
47
  import { getConfiguration } from '../../configuration/config.svelte';
47
48
  import { usePlot } from '../plot/usePlot';
48
49
  import { PlotMargins } from '../margins/PlotMargins.svelte';
@@ -395,12 +396,11 @@
395
396
  {/snippet}
396
397
 
397
398
  {#snippet renderMarker(marker: MarkerConfig, rows: TData[] | TSeries[])}
398
- <!-- In parts the hover styles alike, while it restyles the marker's shapes. -->
399
- {#each shapes.split(withHover(marker, rows), rows, reach) as part (part.key)}
400
- <MarkerOffset marker={part.marker}>
401
- {@render drawMarker(part.marker, rows)}
402
- </MarkerOffset>
403
- {/each}
399
+ <!-- One marker over all its rows, each style the hover sets read off the row as it is hovered. -->
400
+ {@const styled = shapes.styled(withHover(marker, rows), rows, reach)}
401
+ <MarkerOffset marker={styled}>
402
+ {@render drawMarker(styled, rows)}
403
+ </MarkerOffset>
404
404
  {/snippet}
405
405
 
406
406
  {#snippet renderScoped(marker: MarkerConfig, group: ScopedMarkerGroup<TSeries>)}
@@ -423,15 +423,18 @@
423
423
  {/snippet}
424
424
 
425
425
  {#snippet renderHoverLayer(index: number, layer: 'back' | 'front')}
426
- {#each hoverLayers.get(index)?.[layer] ?? [] as part (part.key)}
427
- <!-- A point in a slice of its band (a grouped bar's series) is drawn in the middle of it. -->
428
- {@const nudge = BandNudge.of(svelteplot.scales, part.slice)}
429
- <g transform="translate({nudge.dx}, {nudge.dy})">
430
- <FrameClip clip={FrameClipping.isClipped(part.marker)}>
431
- {@render renderMarker(part.marker as MarkerConfig, data)}
432
- </FrameClip>
433
- </g>
434
- {/each}
426
+ <!-- Drawn on the plot's scales, never shaping them: a hover that moves never lays the plot out again. -->
427
+ <ScaleFreeMarks>
428
+ {#each hoverLayers.get(index)?.[layer] ?? [] as part (part.key)}
429
+ <!-- A point in a slice of its band (a grouped bar's series) is drawn in the middle of it. -->
430
+ {@const nudge = BandNudge.of(svelteplot.scales, part.slice)}
431
+ <g transform="translate({nudge.dx}, {nudge.dy})">
432
+ <FrameClip clip={FrameClipping.isClipped(part.marker)}>
433
+ {@render renderMarker(part.marker as MarkerConfig, data)}
434
+ </FrameClip>
435
+ </g>
436
+ {/each}
437
+ </ScaleFreeMarks>
435
438
  {/snippet}
436
439
 
437
440
  <!-- What the hover draws behind the plot's shapes, before any of them. -->
@@ -459,6 +462,39 @@
459
462
  {/if}
460
463
  {/each}
461
464
 
465
+ {#snippet renderOutline(marker: MarkerConfig, rows: TData[] | TSeries[])}
466
+ {@const outline = shapes.outline(marker, rows, reach)}
467
+ {#if outline}
468
+ <FrameClip clip={FrameClipping.isClipped(outline)}>
469
+ <MarkerOffset marker={outline}>
470
+ {@render drawMarker(outline, rows)}
471
+ </MarkerOffset>
472
+ </FrameClip>
473
+ {/if}
474
+ {/snippet}
475
+
476
+ <!--
477
+ The hovered shapes' outline, over every shape: a shape is drawn in its marker's order, so a neighbour drawn after
478
+ it would cover part of its outline. Drawn on the plot's scales, never shaping them, and never taking the pointer.
479
+ -->
480
+ {#if shapes.outlines}
481
+ <g class="dct-hover-outline" pointer-events="none">
482
+ <ScaleFreeMarks>
483
+ {#each markers as marker, i (markerKey(marker, i))}
484
+ {@const scope = scopedSeriesOf(marker)}
485
+ {#if marker.type !== 'hover' && marker.type !== 'component'}
486
+ {#if scope === undefined}
487
+ {@render renderOutline(marker, data)}
488
+ {:else if groupByName.has(scope)}
489
+ {@const group = groupByName.get(scope)!}
490
+ {@render renderOutline(MarkerGrouping.withSeriesColor(marker, group.color), group.data)}
491
+ {/if}
492
+ {/if}
493
+ {/each}
494
+ </ScaleFreeMarks>
495
+ </g>
496
+ {/if}
497
+
462
498
  <!--
463
499
  The labels the shapes write in themselves, after every marker - the hover's hit areas too, which are the last of them -
464
500
  so a label that listens for the pointer is not covered, and one outside its shape is not clipped with it.
@@ -466,11 +502,10 @@
466
502
  {#each markers as marker, i (markerKey(marker, i))}
467
503
  {@const scope = scopedSeriesOf(marker)}
468
504
  {#if 'label' in marker && marker.label && (scope === undefined || groupByName.has(scope))}
469
- <!-- Split as the shapes are, so a label reads on its shape's color under the hover. -->
470
- {#each shapes.split(marker, data, reach) as part (part.key)}
471
- <MarkerOffset marker={part.marker}>
472
- <MarkerLabel marker={{ ...part.marker, data: part.marker.data ?? data }} />
473
- </MarkerOffset>
474
- {/each}
505
+ <!-- Styled as the shapes are, so a label reads on its shape's color under the hover. -->
506
+ {@const styled = shapes.styled(marker, data, reach)}
507
+ <MarkerOffset marker={styled}>
508
+ <MarkerLabel marker={{ ...styled, data: styled.data ?? data }} />
509
+ </MarkerOffset>
475
510
  {/if}
476
511
  {/each}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * svelteplot's plot context as the marks drawn under a `ScaleFreeMarks` see it: the plot's own state and scales,
3
+ * but marks that never join the plot's list of marks.
4
+ *
5
+ * svelteplot works out a plot's scales from every mark it holds - their data and options - so a mark whose data
6
+ * changes at every move of the pointer (a hover's dot, its rule, its value) lays the whole plot out again each time:
7
+ * every scale built anew from every row, every mark scaled again, the margins measured again. A mark that is only
8
+ * drawn on the plot's scales, never shaping them, can be left out of them.
9
+ *
10
+ * Left out, a mark is in none of the scales' `skip` lists either - the marks whose channel values are already
11
+ * output (a color, a symbol, an opacity) and go through no scale. What these marks draw has no scale of its own to
12
+ * go through, so every such channel of theirs is taken as it is: a hover's colors are its points' own, resolved.
13
+ */
14
+ /** The parts of svelteplot's plot context a mark reads. */
15
+ export type SvelteplotContext = {
16
+ addMark: (mark: {
17
+ id: symbol;
18
+ }) => void;
19
+ removeMark: (mark: {
20
+ id: symbol;
21
+ }) => void;
22
+ updateMark?: (mark: {
23
+ id: symbol;
24
+ }) => void;
25
+ getPlotState: () => object;
26
+ [key: string]: unknown;
27
+ };
28
+ export declare const ScaleFreeContext: {
29
+ /** `parent`, the plot's own context, for marks drawn on its scales that never take part in them. */
30
+ of(parent: SvelteplotContext): SvelteplotContext;
31
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * svelteplot's plot context as the marks drawn under a `ScaleFreeMarks` see it: the plot's own state and scales,
3
+ * but marks that never join the plot's list of marks.
4
+ *
5
+ * svelteplot works out a plot's scales from every mark it holds - their data and options - so a mark whose data
6
+ * changes at every move of the pointer (a hover's dot, its rule, its value) lays the whole plot out again each time:
7
+ * every scale built anew from every row, every mark scaled again, the margins measured again. A mark that is only
8
+ * drawn on the plot's scales, never shaping them, can be left out of them.
9
+ *
10
+ * Left out, a mark is in none of the scales' `skip` lists either - the marks whose channel values are already
11
+ * output (a color, a symbol, an opacity) and go through no scale. What these marks draw has no scale of its own to
12
+ * go through, so every such channel of theirs is taken as it is: a hover's colors are its points' own, resolved.
13
+ */
14
+ /** The scales whose channel values a scale-free mark takes as they are, never through the scale. */
15
+ const OUTPUT_SCALES = new Set(['color', 'opacity', 'symbol']);
16
+ /** A `skip` list holding every mark: each of the channel's values is output already. */
17
+ const EVERY_MARK = { has: () => true };
18
+ const SKIP_ALL = { get: () => EVERY_MARK };
19
+ /** A Proxy's `get` reading `target`'s own property (a class getter runs on the target, its private fields there). */
20
+ const read = (target, key) => {
21
+ const value = Reflect.get(target, key);
22
+ return typeof value === 'function' ? value.bind(target) : value;
23
+ };
24
+ const scaleViews = new WeakMap();
25
+ const scalesViews = new WeakMap();
26
+ const stateViews = new WeakMap();
27
+ /** One cached view of `target` per object, so a mark that derives from it sees the same one until it changes. */
28
+ function viewOf(cache, target, make) {
29
+ let view = cache.get(target);
30
+ if (!view) {
31
+ view = make(target);
32
+ cache.set(target, view);
33
+ }
34
+ return view;
35
+ }
36
+ /** A scale whose `skip` holds every mark. */
37
+ const outputScale = (scale) => viewOf(scaleViews, scale, (s) => new Proxy(s, { get: (t, k) => (k === 'skip' ? SKIP_ALL : read(t, k)) }));
38
+ /** The plot's scales, its output scales skipping every mark. */
39
+ const scalesView = (scales) => viewOf(scalesViews, scales, (s) => new Proxy(s, {
40
+ get: (t, k) => {
41
+ const scale = read(t, k);
42
+ return OUTPUT_SCALES.has(k) && scale && typeof scale === 'object'
43
+ ? outputScale(scale)
44
+ : scale;
45
+ }
46
+ }));
47
+ /** The plot's state, read live, with its scales as `scalesView` shows them. */
48
+ const stateView = (state) => viewOf(stateViews, state, (s) => new Proxy(s, {
49
+ get: (t, k) => {
50
+ const value = read(t, k);
51
+ return k === 'scales' && value && typeof value === 'object' ? scalesView(value) : value;
52
+ }
53
+ }));
54
+ export const ScaleFreeContext = {
55
+ /** `parent`, the plot's own context, for marks drawn on its scales that never take part in them. */
56
+ of(parent) {
57
+ return {
58
+ ...parent,
59
+ addMark: () => { },
60
+ removeMark: () => { },
61
+ updateMark: () => { },
62
+ getPlotState: () => stateView(parent.getPlotState())
63
+ };
64
+ }
65
+ };
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { getContext, setContext, type Snippet } from 'svelte';
3
+ import { ScaleFreeContext, type SvelteplotContext } from './ScaleFreeContext';
4
+
5
+ /**
6
+ * Draws its markers on the plot's scales without them taking part in the scales: what changes at every move of
7
+ * the pointer - the hover's markers - never makes svelteplot work out the scales again. See `ScaleFreeContext`.
8
+ */
9
+ let { children }: { children: Snippet } = $props();
10
+
11
+ setContext('svelteplot', ScaleFreeContext.of(getContext<SvelteplotContext>('svelteplot')));
12
+ </script>
13
+
14
+ {@render children()}
@@ -0,0 +1,7 @@
1
+ import { type Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ children: Snippet;
4
+ };
5
+ declare const ScaleFreeMarks: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ScaleFreeMarks = ReturnType<typeof ScaleFreeMarks>;
7
+ export default ScaleFreeMarks;
@@ -0,0 +1,8 @@
1
+ import type { SvelteplotContext } from './ScaleFreeContext';
2
+ export declare const ScaleGate: {
3
+ /**
4
+ * `parent`, the plot's own context, with each mark handed to the plot behind a `GatedMark`: the scales are
5
+ * worked out again when a mark's rows hold something new, not each time the mark copies them.
6
+ */
7
+ of(parent: SvelteplotContext): SvelteplotContext;
8
+ };
@@ -0,0 +1,95 @@
1
+ import { RowContext } from '../../phases/context/RowContext';
2
+ /**
3
+ * A mark as its plot sees it: the mark itself, but for its rows, which change only when they hold something new.
4
+ *
5
+ * svelteplot works a plot's scales out from every mark's rows, again whenever one of them is a new array. A mark
6
+ * hands it a new array each time it copies its rows (any of its props a new value is enough) and the next time
7
+ * one row's style changes, even a style that no scale reads - a hover's, at every move of the pointer. The copy
8
+ * holds the same rows, so the scales would come out the same: the plot is told of it only when a row differs.
9
+ */
10
+ class GatedMark {
11
+ #mark;
12
+ /** The rows the plot last saw, kept while the mark's are a copy of them. */
13
+ data = $derived.by(() => this.#rows(this.#mark.data));
14
+ #seen;
15
+ constructor(mark) {
16
+ this.#mark = mark;
17
+ }
18
+ get id() {
19
+ return this.#mark.id;
20
+ }
21
+ get type() {
22
+ return this.#mark.type;
23
+ }
24
+ get channels() {
25
+ return this.#mark.channels;
26
+ }
27
+ get scales() {
28
+ return this.#mark.scales;
29
+ }
30
+ get options() {
31
+ return this.#mark.options;
32
+ }
33
+ #rows(rows) {
34
+ const seen = this.#seen;
35
+ if (seen && GatedMark.#sameRows(seen, rows))
36
+ return seen;
37
+ this.#seen = rows;
38
+ return rows;
39
+ }
40
+ /** Whether two lists of rows hold the same values, row by row and field by field. */
41
+ static #sameRows(a, b) {
42
+ if (a === b)
43
+ return true;
44
+ if (a.length !== b.length)
45
+ return false;
46
+ for (let i = 0; i < a.length; i++) {
47
+ if (!GatedMark.#sameRow(a[i], b[i]))
48
+ return false;
49
+ }
50
+ return true;
51
+ }
52
+ /**
53
+ * Whether two rows hold the same values, field by field - but for the context the plot keeps on each row, which
54
+ * no scale reads and which the plot works out anew as it lays its rows out again.
55
+ */
56
+ static #sameRow(a, b) {
57
+ if (a === b)
58
+ return true;
59
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
60
+ return false;
61
+ const keys = Reflect.ownKeys(a);
62
+ if (keys.length !== Reflect.ownKeys(b).length)
63
+ return false;
64
+ return keys.every((key) => key === RowContext.key ||
65
+ Object.is(a[key], b[key]));
66
+ }
67
+ }
68
+ export const ScaleGate = {
69
+ /**
70
+ * `parent`, the plot's own context, with each mark handed to the plot behind a `GatedMark`: the scales are
71
+ * worked out again when a mark's rows hold something new, not each time the mark copies them.
72
+ */
73
+ of(parent) {
74
+ // Which gate stands for which mark: bookkeeping no view reads, so nothing has to react to it.
75
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
76
+ const gates = new Map();
77
+ return {
78
+ ...parent,
79
+ addMark: (mark) => {
80
+ if (gates.has(mark.id))
81
+ return;
82
+ const gate = new GatedMark(mark);
83
+ gates.set(mark.id, gate);
84
+ parent.addMark(gate);
85
+ },
86
+ removeMark: (mark) => {
87
+ const gate = gates.get(mark.id);
88
+ if (!gate)
89
+ return;
90
+ gates.delete(mark.id);
91
+ parent.removeMark(gate);
92
+ }
93
+ };
94
+ }
95
+ };