@fundar/data-chart-telling 1.15.0 → 1.17.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 (197) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/configuration/ConfigVars.svelte +14 -0
  3. package/dist/configuration/ConfigVars.svelte.d.ts +3 -0
  4. package/dist/configuration/config.svelte.d.ts +5 -0
  5. package/dist/configuration/config.svelte.js +18 -4
  6. package/dist/configuration/themes/dark.js +1 -1
  7. package/dist/configuration/themes/editorial.js +1 -1
  8. package/dist/index.d.ts +6 -3
  9. package/dist/index.js +2 -1
  10. package/dist/layout/base/BasePlotLayout.svelte +1 -1
  11. package/dist/layout/figure/Figure.svelte +5 -12
  12. package/dist/layout/figure/FigureChrome.svelte +2 -10
  13. package/dist/layout/geometry/GeometryLayout.svelte +42 -1
  14. package/dist/layout/hover/HoverSettings.d.ts +3 -1
  15. package/dist/layout/hover/HoverSettings.js +4 -2
  16. package/dist/layout/legend/DisabledStyle.d.ts +1 -1
  17. package/dist/layout/legend/DisabledStyle.js +1 -1
  18. package/dist/layout/legend/DiscreteBadgeItem.svelte +1 -1
  19. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  20. package/dist/layout/legend/LegendState.svelte.d.ts +12 -2
  21. package/dist/layout/legend/LegendState.svelte.js +18 -4
  22. package/dist/layout/legend/LegendText.d.ts +4 -1
  23. package/dist/layout/legend/LegendText.js +7 -1
  24. package/dist/layout/margins/PlotMargins.svelte.d.ts +2 -2
  25. package/dist/layout/margins/PlotMargins.svelte.js +2 -2
  26. package/dist/layout/plot/Plot.svelte +4 -2
  27. package/dist/layout/plot/Plot.svelte.d.ts +2 -1
  28. package/dist/layout/scales/ScalesLayout.svelte +1 -1
  29. package/dist/layout/styles/ValueLabels.d.ts +33 -2
  30. package/dist/layout/styles/ValueLabels.js +67 -8
  31. package/dist/layout/timeline/Timeline.svelte +5 -1
  32. package/dist/layout/timeline/TimelineLayout.svelte +4 -1
  33. package/dist/layout/timeline/TimelineSteps.d.ts +6 -2
  34. package/dist/layout/timeline/TimelineSteps.js +9 -3
  35. package/dist/markers/area/AreaMarker.svelte +3 -0
  36. package/dist/markers/arrow/ArrowMarker.svelte +9 -5
  37. package/dist/markers/bar/BarMarker.svelte +45 -48
  38. package/dist/markers/bollinger/BollingerMarker.svelte +3 -0
  39. package/dist/markers/box/BoxMarker.svelte +3 -1
  40. package/dist/markers/cell/CellMarker.svelte +11 -7
  41. package/dist/markers/contour/ContourMarker.svelte +3 -0
  42. package/dist/markers/delaunay/DelaunayLinkMarker.svelte +9 -5
  43. package/dist/markers/delaunay/DelaunayMeshMarker.svelte +3 -0
  44. package/dist/markers/density/DensityMarker.svelte +3 -0
  45. package/dist/markers/difference/DifferenceMarker.svelte +3 -0
  46. package/dist/markers/dot/DotMarker.svelte +11 -6
  47. package/dist/markers/geo/GraticuleMarker.svelte +3 -0
  48. package/dist/markers/geo/SphereMarker.svelte +3 -0
  49. package/dist/markers/hover/HoverMarker.svelte +22 -6
  50. package/dist/markers/hull/HullMarker.svelte +3 -0
  51. package/dist/markers/image/ImageMarker.svelte +3 -0
  52. package/dist/markers/line/LineMarker.svelte +3 -0
  53. package/dist/markers/link/LinkMarker.svelte +3 -0
  54. package/dist/markers/raster/RasterMarker.svelte +3 -0
  55. package/dist/markers/rect/RectMarker.svelte +25 -17
  56. package/dist/markers/regression/RegressionMarker.svelte +3 -0
  57. package/dist/markers/rule/RuleMarker.svelte +15 -5
  58. package/dist/markers/text/TextFit.d.ts +44 -0
  59. package/dist/markers/text/TextFit.js +86 -0
  60. package/dist/markers/text/TextMarker.svelte +186 -90
  61. package/dist/markers/text/TextMarker.svelte.d.ts +2 -0
  62. package/dist/markers/tick/TickMarker.svelte +9 -3
  63. package/dist/markers/trail/TrailMarker.svelte +3 -0
  64. package/dist/markers/utils/BoxLabel.d.ts +58 -0
  65. package/dist/markers/utils/BoxLabel.js +175 -0
  66. package/dist/markers/utils/MarkerEvents.d.ts +23 -0
  67. package/dist/markers/utils/MarkerEvents.js +44 -0
  68. package/dist/markers/utils/MarkerLabel.svelte +33 -0
  69. package/dist/markers/utils/MarkerLabel.svelte.d.ts +10 -0
  70. package/dist/markers/utils/RowStroke.d.ts +9 -21
  71. package/dist/markers/utils/RowStroke.js +10 -21
  72. package/dist/markers/utils/RowStyle.d.ts +26 -0
  73. package/dist/markers/utils/RowStyle.js +55 -0
  74. package/dist/markers/vector/VectorMarker.svelte +3 -0
  75. package/dist/markers/voronoi/VoronoiMarker.svelte +9 -5
  76. package/dist/markers/voronoi/VoronoiMeshMarker.svelte +3 -0
  77. package/dist/markers/waffle/WaffleMarker.svelte +9 -5
  78. package/dist/phases/Pipeline.d.ts +3 -1
  79. package/dist/phases/Pipeline.js +1 -0
  80. package/dist/phases/data/DataPhase.js +51 -10
  81. package/dist/phases/hover/HoverPhase.js +9 -1
  82. package/dist/phases/mapping/MappingPhase.js +2 -1
  83. package/dist/phases/markers/MarkerStyles.d.ts +18 -0
  84. package/dist/phases/markers/MarkerStyles.js +111 -0
  85. package/dist/phases/markers/MarkersPhase.d.ts +1 -1
  86. package/dist/phases/markers/MarkersPhase.js +7 -5
  87. package/dist/phases/statistics/StatisticsPhase.js +1 -1
  88. package/dist/phases/transforms/Transforms.js +3 -1
  89. package/dist/phases/transforms/TransformsPhase.js +58 -4
  90. package/dist/phases/transforms/methods/TreemapTransform.d.ts +28 -0
  91. package/dist/phases/transforms/methods/TreemapTransform.js +119 -0
  92. package/dist/plots/correlation/heatmap/defaults.js +2 -2
  93. package/dist/plots/correlation/heatmap/markers.d.ts +0 -8
  94. package/dist/plots/correlation/heatmap/markers.js +8 -35
  95. package/dist/plots/correlation/scatter/markers.d.ts +1 -1
  96. package/dist/plots/correlation/scatter/markers.js +2 -2
  97. package/dist/plots/distribution/histogram/defaults.js +2 -2
  98. package/dist/plots/distribution/histogram/markers.d.ts +1 -10
  99. package/dist/plots/distribution/histogram/markers.js +17 -59
  100. package/dist/plots/distribution/pyramid/defaults.js +3 -3
  101. package/dist/plots/distribution/pyramid/markers.d.ts +0 -10
  102. package/dist/plots/distribution/pyramid/markers.js +13 -64
  103. package/dist/plots/distribution/violin/markers.js +1 -1
  104. package/dist/plots/map/geo/phases/markers.js +3 -2
  105. package/dist/plots/part-of-a-whole/treemap/Plot.svelte +47 -0
  106. package/dist/plots/part-of-a-whole/treemap/Plot.svelte.d.ts +26 -0
  107. package/dist/plots/part-of-a-whole/treemap/defaults.d.ts +10 -0
  108. package/dist/plots/part-of-a-whole/treemap/defaults.js +167 -0
  109. package/dist/plots/ranking/bar/defaults.js +2 -2
  110. package/dist/plots/ranking/bar/markers.d.ts +1 -11
  111. package/dist/plots/ranking/bar/markers.js +38 -80
  112. package/dist/plots/ranking/bullet/markers.js +2 -3
  113. package/dist/plots/ranking/cleveland/ClevelandMarkers.js +17 -13
  114. package/dist/types/configuration/styling.d.ts +33 -2
  115. package/dist/types/layout/legend.d.ts +8 -1
  116. package/dist/types/layout/mapping.d.ts +12 -3
  117. package/dist/types/layout/scales.d.ts +6 -0
  118. package/dist/types/layout/tooltip.d.ts +16 -4
  119. package/dist/types/markers/axis/arrow.d.ts +2 -8
  120. package/dist/types/markers/axis/bar.d.ts +4 -8
  121. package/dist/types/markers/axis/cell.d.ts +5 -5
  122. package/dist/types/markers/axis/delaunayLink.d.ts +2 -2
  123. package/dist/types/markers/axis/rect.d.ts +4 -8
  124. package/dist/types/markers/axis/rule.d.ts +2 -16
  125. package/dist/types/markers/axis/tick.d.ts +2 -2
  126. package/dist/types/markers/axis/voronoi.d.ts +2 -2
  127. package/dist/types/markers/axis/waffle.d.ts +4 -3
  128. package/dist/types/markers/basic/dot.d.ts +2 -2
  129. package/dist/types/markers/basic/text.d.ts +56 -14
  130. package/dist/types/markers/channels/commonMarkerConfig.d.ts +4 -1
  131. package/dist/types/markers/channels/commonMarkerConfigWithArea.d.ts +1 -2
  132. package/dist/types/markers/channels/commonMarkerConfigWithLabel.d.ts +10 -0
  133. package/dist/types/markers/channels/commonMarkerConfigWithLabel.js +1 -0
  134. package/dist/types/markers/channels/commonMarkerEvents.d.ts +17 -0
  135. package/dist/types/markers/channels/commonMarkerEvents.js +13 -0
  136. package/dist/types/markers/geo/contour.d.ts +2 -1
  137. package/dist/types/markers/geo/graticule.d.ts +2 -1
  138. package/dist/types/markers/geo/link.d.ts +2 -1
  139. package/dist/types/markers/geo/sphere.d.ts +2 -1
  140. package/dist/types/markers/geo/vector.d.ts +2 -1
  141. package/dist/types/phases/data/phase.d.ts +5 -0
  142. package/dist/types/phases/hover/phase.d.ts +2 -0
  143. package/dist/types/phases/state.d.ts +7 -0
  144. package/dist/types/phases/statistics/phase.d.ts +2 -1
  145. package/dist/types/phases/transforms/phase.d.ts +11 -0
  146. package/dist/types/plots/constants.d.ts +14 -14
  147. package/dist/types/plots/correlation/heatmap/props.d.ts +3 -3
  148. package/dist/types/plots/correlation/scatter/props.d.ts +3 -3
  149. package/dist/types/plots/data/common.d.ts +19 -6
  150. package/dist/types/plots/data/geo.d.ts +1 -1
  151. package/dist/types/plots/distribution/box/props.d.ts +3 -3
  152. package/dist/types/plots/distribution/density/props.d.ts +3 -11
  153. package/dist/types/plots/distribution/histogram/props.d.ts +3 -11
  154. package/dist/types/plots/distribution/pyramid/props.d.ts +3 -3
  155. package/dist/types/plots/distribution/violin/props.d.ts +3 -3
  156. package/dist/types/plots/evolution/area/props.d.ts +3 -3
  157. package/dist/types/plots/evolution/line/props.d.ts +3 -3
  158. package/dist/types/plots/part-of-a-whole/treemap/props.d.ts +19 -0
  159. package/dist/types/plots/part-of-a-whole/treemap/props.js +1 -0
  160. package/dist/types/plots/part-of-a-whole/treemap/state.d.ts +6 -0
  161. package/dist/types/plots/part-of-a-whole/treemap/state.js +1 -0
  162. package/dist/types/plots/props.d.ts +7 -4
  163. package/dist/types/plots/ranking/bar/props.d.ts +3 -3
  164. package/dist/types/plots/ranking/cleveland/props.d.ts +3 -3
  165. package/dist/types/plots/ranking/dumbbell/props.d.ts +3 -3
  166. package/dist/types/plots/ranking/forest/props.d.ts +3 -3
  167. package/dist/types/plots/ranking/lollipop/props.d.ts +3 -3
  168. package/dist/types/plots/styling.d.ts +24 -0
  169. package/dist/types/transforms/index.d.ts +3 -1
  170. package/dist/types/transforms/treemap.d.ts +32 -0
  171. package/dist/types/transforms/treemap.js +5 -0
  172. package/dist/utils/color/ColorTones.d.ts +19 -0
  173. package/dist/utils/color/ColorTones.js +64 -0
  174. package/dist/utils/color/CssColor.d.ts +14 -5
  175. package/dist/utils/color/CssColor.js +69 -27
  176. package/dist/utils/color/index.d.ts +4 -0
  177. package/dist/utils/color/index.js +4 -0
  178. package/dist/utils/grouping.d.ts +18 -1
  179. package/dist/utils/grouping.js +31 -0
  180. package/docs/concepts.md +3 -2
  181. package/docs/legend.md +8 -5
  182. package/docs/markers.md +76 -26
  183. package/docs/plots/part-of-a-whole/treemap.md +69 -0
  184. package/docs/plots/ranking/bar.md +1 -1
  185. package/docs/plots.md +21 -20
  186. package/docs/scales.md +3 -0
  187. package/docs/state.md +1 -1
  188. package/docs/styles-and-segments.md +19 -18
  189. package/docs/theming.md +5 -12
  190. package/docs/tooltip-and-hover.md +1 -1
  191. package/docs/transforms-and-statistics.md +17 -16
  192. package/examples/Treemap.svelte +13 -0
  193. package/examples/datasets/economic-activity.ts +40 -0
  194. package/examples/datasets/index.ts +1 -0
  195. package/llms-full.txt +5 -5
  196. package/llms.txt +2 -0
  197. package/package.json +3 -1
@@ -1,4 +1,6 @@
1
1
  <script lang="ts" generics="TData = Record<string, unknown>">
2
+ import { RowStyle } from '../utils/RowStyle';
3
+ import { MarkerEvents } from '../utils/MarkerEvents';
2
4
  import { Text } from 'svelteplot';
3
5
  import { usePlot } from '../../layout/plot/usePlot';
4
6
  import { getConfiguration } from '../../configuration/config.svelte';
@@ -6,10 +8,13 @@
6
8
  import { RowOffset } from '../utils/RowOffset';
7
9
  import RowOffsetMarks from '../utils/RowOffsetMarks.svelte';
8
10
  import { Declutter } from './Declutter';
11
+ import { SvelteMap } from 'svelte/reactivity';
12
+ import { TextFit, type FittedText } from './TextFit';
9
13
  import { NumberFormat } from '../../utils/NumberFormat';
10
14
  import { PlotMargins } from '../../layout/margins/PlotMargins.svelte';
11
15
  import { Box } from '../../layout/margins/Box';
12
16
  import type { TextMarkerConfig } from '../../types/markers/common';
17
+ import type { BoxChannels, BoxSizer } from '../utils/BoxLabel';
13
18
  import type { TextAnchor } from '../../types/plots/constants';
14
19
 
15
20
  /**
@@ -20,7 +25,8 @@
20
25
  * draws one label per row, inside the plot's content box. With
21
26
  * `fitMargins`, the plot's margins grow until the labels fit.
22
27
  */
23
- const { marker }: { marker: TextMarkerConfig } = $props();
28
+ const { marker, box }: { marker: TextMarkerConfig; box?: BoxChannels | BoxSizer } = $props();
29
+ const events = MarkerEvents.use(() => marker);
24
30
 
25
31
  // A text's anchors as the side of its point it sits on, and back.
26
32
  const TEXT_SIDES = { start: 'after', middle: 'middle', end: 'before' } as const;
@@ -47,15 +53,16 @@
47
53
  data.length > 0 ? data : placedByRows(x) || placedByRows(y) ? [] : [{} as TData]
48
54
  );
49
55
  // Per-row accessors give literal values; an empty one takes the flat default.
50
- const flatFill = $derived(styles?.fill ?? 'currentColor');
51
56
  const fill = $derived.by(() => {
52
- const own = marker.fill;
53
- if (typeof own !== 'function') return own ?? flatFill;
54
- return { value: (d: TData) => own(d) ?? flatFill, scale: null };
57
+ const own = styles?.fill;
58
+ if (typeof own !== 'function') return RowStyle.of(own, displayData) ?? 'currentColor';
59
+ return { value: (d: TData) => own(d) ?? 'currentColor', scale: null };
55
60
  });
56
61
  const fillOpacity = $derived.by(() => {
57
- const own = marker.fillOpacity;
58
- return typeof own === 'function' ? { value: (d: TData) => own(d) ?? 1, scale: null } : own;
62
+ const own = styles?.fillOpacity;
63
+ return typeof own === 'function'
64
+ ? { value: (d: TData) => own(d) ?? 1, scale: null }
65
+ : RowStyle.opacity(own);
59
66
  });
60
67
  const fontSize = $derived(styles?.fontSize ?? 12);
61
68
  const fontWeight = $derived(styles?.fontWeight ?? 'normal');
@@ -97,11 +104,7 @@
97
104
  };
98
105
  });
99
106
  const perRow = $derived(
100
- !!declutter ||
101
- !!marker.edgeAware ||
102
- !!marker.onPointerEnter ||
103
- !!marker.onPointerLeave ||
104
- typeof marker.textAnchor === 'function'
107
+ !!declutter || !!marker.edgeAware || events.any || typeof marker.textAnchor === 'function'
105
108
  );
106
109
  const dir = $derived(Math.sign(dx) || 1);
107
110
 
@@ -128,9 +131,101 @@
128
131
  return typeof value === 'number' ? numberText(value) : String(value ?? '');
129
132
  }
130
133
 
134
+ /** The pixel size of the box a row's label is fitted to - from the edges it names, or measured by the shape it labels. */
135
+ const boxSizeOf = $derived.by(() => {
136
+ const fit = marker.fit === true ? {} : marker.fit;
137
+ if (!fit) return undefined;
138
+ const own = typeof box === 'function' ? undefined : box;
139
+ const [x1, x2, y1, y2] = [
140
+ fit.x1 ?? own?.x1,
141
+ fit.x2 ?? own?.x2,
142
+ fit.y1 ?? own?.y1,
143
+ fit.y2 ?? own?.y2
144
+ ];
145
+ const xScale = plot.scales.x?.fn;
146
+ const yScale = plot.scales.y?.fn;
147
+ if (![x1, x2, y1, y2].includes(undefined)) {
148
+ if (!xScale || !yScale) return undefined;
149
+ const [left, right, top, bottom] = [x1, x2, y1, y2].map(channel);
150
+ const span = (
151
+ scale: typeof xScale,
152
+ from: (d: TData) => unknown,
153
+ to: (d: TData) => unknown,
154
+ d: TData
155
+ ) => Math.abs(Number(scale(to(d) as never)) - Number(scale(from(d) as never)));
156
+ return (d: TData) => ({
157
+ width: span(xScale, left, right, d),
158
+ height: span(yScale, top, bottom, d)
159
+ });
160
+ }
161
+ return typeof box === 'function' ? (d: TData) => box(d, plot.scales as never) : undefined;
162
+ });
163
+
164
+ /**
165
+ * What each label is written as and at, fitted to its box (`fit`), less the padding. A label with no room is left
166
+ * out, with `undefined`; one with no box to measure is written as it is.
167
+ */
168
+ const fitted = $derived.by(() => {
169
+ const fit = marker.fit === true ? {} : marker.fit;
170
+ if (!fit || !boxSizeOf) return undefined;
171
+ const padding = fit.padding ?? cfg.text.label.padding;
172
+ const measure = (text: string, size: number) => Box.ofText(text, size, cfg).width;
173
+ const fallback = fit.fallback;
174
+ const fallbackOf = (d: TData) => {
175
+ // As `text` is: a function's result, a field of the row, else the text itself.
176
+ const value =
177
+ typeof fallback === 'function'
178
+ ? fallback(d)
179
+ : typeof fallback === 'string' && typeof d === 'object' && d !== null && fallback in d
180
+ ? (d as Record<string, unknown>)[fallback]
181
+ : fallback;
182
+ return typeof value === 'number' ? numberText(value) : String(value);
183
+ };
184
+ // Built whole inside this derivation and never changed after it, so it has nothing to react to.
185
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
186
+ const out = new Map<TData, FittedText | undefined>();
187
+ for (const d of displayData) {
188
+ const size = boxSizeOf(d);
189
+ out.set(
190
+ d,
191
+ size
192
+ ? TextFit.fit({
193
+ text: textOf(d),
194
+ fallback: fallback === undefined ? undefined : fallbackOf(d),
195
+ width: size.width - 2 * padding,
196
+ height: size.height - 2 * padding,
197
+ fontSize,
198
+ minFontSize: fit.minFontSize ?? cfg.text.label.minFontSize,
199
+ lineHeight: styles?.lineHeight ?? 1.2,
200
+ minLetters: fit.minLetters ?? cfg.text.label.minLetters,
201
+ measure
202
+ })
203
+ : { text: textOf(d), fontSize }
204
+ );
205
+ }
206
+ return out;
207
+ });
208
+ /** `rows` by the size their labels are written at: svelteplot takes one size for a mark, and a function of the row sets its scales off in a loop. */
209
+ function sizeGroups(rows: TData[]): { size: number; rows: TData[]; texts?: string[] }[] {
210
+ if (!fitted) return [{ size: fontSize, rows }];
211
+ const bySize = new SvelteMap<number, { rows: TData[]; texts: string[] }>();
212
+ for (const d of rows) {
213
+ const own = fitted.get(d);
214
+ const group = bySize.get(own?.fontSize ?? fontSize) ?? { rows: [], texts: [] };
215
+ group.rows.push(d);
216
+ group.texts.push(own?.text ?? '');
217
+ bySize.set(own?.fontSize ?? fontSize, group);
218
+ }
219
+ return [...bySize].map(([size, group]) => ({ size, ...group }));
220
+ }
221
+
222
+ /** The rows with room for a label. */
223
+ const shown = $derived(fitted ? displayData.filter((d) => fitted.get(d)) : displayData);
224
+
131
225
  function opacityOf(d: TData): number | undefined {
132
- const value = marker.fillOpacity;
133
- return typeof value === 'function' ? value(d) : value;
226
+ const value = styles?.fillOpacity;
227
+ if (typeof value === 'function') return value(d);
228
+ return typeof value === 'string' ? (d as Record<string, number>)[value] : value;
134
229
  }
135
230
 
136
231
  /** Each row's pixel position, once both scales resolve. */
@@ -188,10 +283,10 @@
188
283
  let groupEl: SVGGElement | undefined = $state();
189
284
 
190
285
  // The plot measures what it draws: how far it reaches out, and with `fitMargins` its margins grow to fit it.
191
- const measured = $derived(!!plotMargins && (!!marker.onOverflow || !!marker.fitMargins));
286
+ const measured = $derived(!!plotMargins && (!!marker.onoverflow || !!marker.fitMargins));
192
287
  plotMargins?.watch(() => (measured ? groupEl : undefined), {
193
288
  grow: () => !!marker.fitMargins,
194
- onOverflow: (overflow) => marker.onOverflow?.(overflow),
289
+ onoverflow: (overflow) => marker.onoverflow?.(overflow),
195
290
  // The box isn't tracked reactively: a new layout measures again.
196
291
  deps: () => [marker, points]
197
292
  });
@@ -205,38 +300,41 @@
205
300
  rowLineAnchor: typeof lineAnchor
206
301
  )}
207
302
  <g transform={projected ? `translate(${rowDx},${rowDy})` : undefined}>
208
- <Text
209
- data={rows}
210
- x={x ?? (marker.frameAnchor ? undefined : 0)}
211
- y={y ?? (marker.frameAnchor ? undefined : 0)}
212
- frameAnchor={marker.frameAnchor}
213
- text={((d: TData) => {
214
- const value = valueOf(d);
215
- return typeof value === 'number' ? numberText(value) : value;
216
- }) as (d: TData) => string}
217
- dx={projected ? 0 : rowDx}
218
- dy={projected ? 0 : rowDy}
219
- fill={fill as never}
220
- fillOpacity={fillOpacity as never}
221
- {fontSize}
222
- {fontWeight}
223
- textAnchor={rowAnchor}
224
- lineAnchor={rowLineAnchor}
225
- fontStyle={fontStyleProp}
226
- lineHeight={styles?.lineHeight}
227
- rotate={styles?.rotate}
228
- stroke={styles?.stroke}
229
- strokeWidth={styles?.strokeWidth}
230
- paintOrder={styles?.paintOrder}
231
- class={styles?.class}
232
- textClass={styles?.textClass}
233
- />
303
+ {#each sizeGroups(rows) as group (group.size)}
304
+ <Text
305
+ data={group.rows}
306
+ x={x ?? (marker.frameAnchor ? undefined : 0)}
307
+ y={y ?? (marker.frameAnchor ? undefined : 0)}
308
+ frameAnchor={marker.frameAnchor}
309
+ text={((d: TData, index: number) => {
310
+ if (group.texts) return group.texts[index];
311
+ const value = valueOf(d);
312
+ return typeof value === 'number' ? numberText(value) : value;
313
+ }) as (d: TData) => string}
314
+ dx={projected ? 0 : rowDx}
315
+ dy={projected ? 0 : rowDy}
316
+ fill={fill as never}
317
+ fillOpacity={fillOpacity as never}
318
+ fontSize={group.size}
319
+ {fontWeight}
320
+ textAnchor={rowAnchor}
321
+ lineAnchor={rowLineAnchor}
322
+ fontStyle={fontStyleProp}
323
+ lineHeight={styles?.lineHeight}
324
+ rotate={styles?.rotate}
325
+ stroke={RowStyle.of(styles?.stroke, rows)}
326
+ strokeWidth={RowStyle.field(styles?.strokeWidth)}
327
+ paintOrder={styles?.paintOrder}
328
+ class={styles?.class}
329
+ textClass={styles?.textClass}
330
+ />
331
+ {/each}
234
332
  </g>
235
333
  {/snippet}
236
334
 
237
335
  {#snippet labels()}
238
336
  {#if !perRow}
239
- <RowOffsetMarks {marker} rows={displayData}>
337
+ <RowOffsetMarks {marker} rows={shown}>
240
338
  {#snippet mark(rows)}
241
339
  {@render label(rows, dx, dy, textAnchor, lineAnchor)}
242
340
  {/snippet}
@@ -244,54 +342,52 @@
244
342
  {:else if (!declutter && !marker.edgeAware) || points}
245
343
  <!-- A decluttered or edge-aware label waits for both scales. -->
246
344
  {#each displayData as d, i (i)}
247
- {@const lane = lanes?.get(i)}
248
- {@const placement = placements?.[i]}
249
- {@const extraDy = lane?.extraDy ?? 0}
250
- {@const laneDir = lane?.aligned ? lane.dir : dir}
251
- {@const labelDx = lane?.aligned ? laneDir * lane.reach : dx}
252
- {#if declutter && points && (lane?.aligned || extraDy !== 0)}
253
- {@const { px, py } = points[i]}
254
- {@const laneX = lane?.aligned ? px + laneDir * lane.laneReach : px + labelDx}
255
- {@const connector = declutter.connector}
256
- <path
257
- d={`M ${px} ${py} H ${laneX} V ${py + extraDy} H ${px + labelDx}`}
258
- fill="none"
259
- stroke={connector.stroke}
260
- stroke-width={connector.strokeWidth}
261
- stroke-opacity={connector.strokeOpacity ?? opacityOf(d) ?? 1}
262
- stroke-dasharray={connector.strokeDasharray}
263
- stroke-linecap={connector.strokeLinecap ?? 'square'}
264
- stroke-linejoin={connector.strokeLinejoin ?? 'miter'}
265
- />
345
+ {#if !fitted || fitted.get(d)}
346
+ {@const lane = lanes?.get(i)}
347
+ {@const placement = placements?.[i]}
348
+ {@const extraDy = lane?.extraDy ?? 0}
349
+ {@const laneDir = lane?.aligned ? lane.dir : dir}
350
+ {@const labelDx = lane?.aligned ? laneDir * lane.reach : dx}
351
+ {#if declutter && points && (lane?.aligned || extraDy !== 0)}
352
+ {@const { px, py } = points[i]}
353
+ {@const laneX = lane?.aligned ? px + laneDir * lane.laneReach : px + labelDx}
354
+ {@const connector = declutter.connector}
355
+ <path
356
+ d={`M ${px} ${py} H ${laneX} V ${py + extraDy} H ${px + labelDx}`}
357
+ fill="none"
358
+ stroke={connector.stroke}
359
+ stroke-width={connector.strokeWidth}
360
+ stroke-opacity={connector.strokeOpacity ?? opacityOf(d) ?? 1}
361
+ stroke-dasharray={connector.strokeDasharray}
362
+ stroke-linecap={connector.strokeLinecap ?? 'square'}
363
+ stroke-linejoin={connector.strokeLinejoin ?? 'miter'}
364
+ />
365
+ {/if}
366
+ <!-- svelteplot's Text never wires pointer handlers to its DOM node; a wrapping <g> catches them. -->
367
+ <g role="presentation" {...events.forRow(d, i)}>
368
+ <RowOffsetMarks {marker} rows={[d]}>
369
+ {#snippet mark(rows)}
370
+ {#if placement}
371
+ {@render label(
372
+ rows,
373
+ placement.dx,
374
+ placement.dy + extraDy,
375
+ placement.textAnchor,
376
+ placement.lineAnchor
377
+ )}
378
+ {:else}
379
+ {@render label(
380
+ rows,
381
+ labelDx,
382
+ dy + extraDy,
383
+ lane?.aligned && laneDir !== dir ? flipAnchor(anchorAt(d)) : anchorAt(d),
384
+ lineAnchor
385
+ )}
386
+ {/if}
387
+ {/snippet}
388
+ </RowOffsetMarks>
389
+ </g>
266
390
  {/if}
267
- <!-- svelteplot's Text never wires pointer handlers to its DOM node; a wrapping <g> catches them. -->
268
- <g
269
- role="presentation"
270
- onpointerenter={marker.onPointerEnter && (() => marker.onPointerEnter?.(d))}
271
- onpointerleave={marker.onPointerLeave && (() => marker.onPointerLeave?.(d))}
272
- >
273
- <RowOffsetMarks {marker} rows={[d]}>
274
- {#snippet mark(rows)}
275
- {#if placement}
276
- {@render label(
277
- rows,
278
- placement.dx,
279
- placement.dy + extraDy,
280
- placement.textAnchor,
281
- placement.lineAnchor
282
- )}
283
- {:else}
284
- {@render label(
285
- rows,
286
- labelDx,
287
- dy + extraDy,
288
- lane?.aligned && laneDir !== dir ? flipAnchor(anchorAt(d)) : anchorAt(d),
289
- lineAnchor
290
- )}
291
- {/if}
292
- {/snippet}
293
- </RowOffsetMarks>
294
- </g>
295
391
  {/each}
296
392
  {/if}
297
393
  {/snippet}
@@ -1,6 +1,8 @@
1
1
  import type { TextMarkerConfig } from '../../types/markers/common';
2
+ import type { BoxChannels, BoxSizer } from '../utils/BoxLabel';
2
3
  type $$ComponentProps = {
3
4
  marker: TextMarkerConfig;
5
+ box?: BoxChannels | BoxSizer;
4
6
  };
5
7
  declare function $$render<TData = Record<string, unknown>>(): {
6
8
  props: $$ComponentProps;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { RowStyle } from '../utils/RowStyle';
3
+ import { MarkerEvents } from '../utils/MarkerEvents';
2
4
  import { TickX, TickY } from 'svelteplot';
3
5
  import { usePlot } from '../../layout/plot/usePlot';
4
6
  import { MarkerChannels } from '../utils/MarkerChannels';
@@ -7,6 +9,7 @@
7
9
 
8
10
  /** Renders one `'tick'` marker as `<TickX>` or `<TickY>`, picked from the way the plot runs (`usePlot().axes`). */
9
11
  let { marker }: { marker: TickMarkerConfig } = $props();
12
+ const events = MarkerEvents.use(() => marker);
10
13
 
11
14
  const plot = usePlot();
12
15
  const isHorizontal = $derived(plot.axes?.isHorizontal ?? false);
@@ -15,7 +18,9 @@
15
18
  const cfg = $derived(getConfiguration());
16
19
  const data = $derived(marker.data ?? []);
17
20
  const channelStroke = $derived(MarkerChannels.buildFill(data, marker.color, marker.scales, cfg));
18
- const stroke = $derived(channelStroke ?? marker.styles?.stroke ?? 'currentColor');
21
+ const stroke = $derived(
22
+ channelStroke ?? RowStyle.of(marker.styles?.stroke, data) ?? 'currentColor'
23
+ );
19
24
 
20
25
  /**
21
26
  * A tick across a band axis spans the whole band, whatever `tickLength` says -
@@ -33,14 +38,15 @@
33
38
  </script>
34
39
 
35
40
  <TickMark
41
+ {...events.props}
36
42
  {data}
37
43
  x={marker.x}
38
44
  y={marker.y}
39
45
  tickLength={marker.tickLength}
40
46
  {...insetProps}
41
47
  {stroke}
42
- strokeWidth={marker.styles?.strokeWidth}
43
- strokeOpacity={marker.styles?.strokeOpacity}
48
+ strokeWidth={RowStyle.field(marker.styles?.strokeWidth)}
49
+ strokeOpacity={RowStyle.opacity(marker.styles?.strokeOpacity)}
44
50
  strokeDasharray={marker.styles?.strokeDasharray}
45
51
  strokeLinecap={marker.styles?.strokeLinecap}
46
52
  />
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { MarkerEvents } from '../utils/MarkerEvents';
2
3
  import { Trail } from 'svelteplot';
3
4
  import { MarkerChannels } from '../utils/MarkerChannels';
4
5
  import { getConfiguration } from '../../configuration/config.svelte';
@@ -6,6 +7,7 @@
6
7
 
7
8
  /** Renders one `'trail'` marker as `<Trail>` - a ribbon whose width follows `r`. */
8
9
  let { marker }: { marker: TrailMarkerConfig } = $props();
10
+ const events = MarkerEvents.use(() => marker);
9
11
 
10
12
  const cfg = $derived(getConfiguration());
11
13
  const data = $derived(marker.data ?? []);
@@ -13,6 +15,7 @@
13
15
  </script>
14
16
 
15
17
  <Trail
18
+ {...events.props}
16
19
  {data}
17
20
  x={marker.x}
18
21
  y={marker.y}
@@ -0,0 +1,58 @@
1
+ import type { TextMarkerConfig } from '../../types/markers/common';
2
+ import type { TextLabelConfig } from '../../types/markers/basic/text';
3
+ /** Where a row's edge along an axis is: a field of the rows, the symbol a transform keyed it by, a function of the row, or a literal. */
4
+ type Edge = unknown;
5
+ /** The edges of the box each row spans, in the data's own terms. */
6
+ export type BoxChannels = {
7
+ x1: Edge;
8
+ x2: Edge;
9
+ y1: Edge;
10
+ y2: Edge;
11
+ };
12
+ /** The scales a box is measured on. */
13
+ export type BoxScales = Record<string, {
14
+ type?: string;
15
+ fn: any;
16
+ } | undefined>;
17
+ /** The pixel size of the box of a row, for the markers whose boxes are bands of a category axis rather than edges. */
18
+ export type BoxSizer = (row: unknown, scales: BoxScales) => {
19
+ width: number;
20
+ height: number;
21
+ } | undefined;
22
+ /** The `text` marker that labels the boxes a marker draws, and where each label's box is. */
23
+ export type BoxLabelMarker = {
24
+ marker: TextMarkerConfig;
25
+ box: BoxChannels | BoxSizer;
26
+ };
27
+ /** What a marker has to say for its boxes to be labelled. */
28
+ export type Labelled = {
29
+ type?: string;
30
+ data?: unknown[];
31
+ label?: TextLabelConfig;
32
+ x?: Edge;
33
+ y?: Edge;
34
+ x1?: Edge;
35
+ x2?: Edge;
36
+ y1?: Edge;
37
+ y2?: Edge;
38
+ isHorizontal?: boolean;
39
+ role?: string;
40
+ thickness?: number;
41
+ /** The marker's styles: the insets of a cell or a bar are read off them. */
42
+ styles?: object;
43
+ };
44
+ /**
45
+ * How a marker that draws a box per row writes a label in each: the `text` marker for them, centred on the box,
46
+ * and where the box is for it to fit to. A `rect` says it with its edges, a `cell` is a band of each axis, and a bar
47
+ * or a waffle is a band of its category axis and the stretch of its value.
48
+ */
49
+ export declare class BoxLabel {
50
+ #private;
51
+ /** `undefined` when the marker has no label, or does not say where its boxes are. */
52
+ static of(marker: Labelled, plot?: {
53
+ isHorizontal?: boolean;
54
+ scales?: () => BoxScales | undefined;
55
+ gap?: number;
56
+ }): BoxLabelMarker | undefined;
57
+ }
58
+ export {};
@@ -0,0 +1,175 @@
1
+ const read = (edge) => (row) => typeof edge === 'function'
2
+ ? edge(row)
3
+ : typeof edge === 'string' || typeof edge === 'symbol'
4
+ ? row?.[edge]
5
+ : edge;
6
+ /** The middle of two positions along an axis, whether numbers or dates. */
7
+ const middle = (from, to) => from instanceof Date || to instanceof Date
8
+ ? new Date((Number(from) + Number(to)) / 2)
9
+ : (Number(from) + Number(to)) / 2;
10
+ const bandwidth = (scale) => scale?.type === 'band' ? Number(scale.fn.bandwidth()) : undefined;
11
+ const distance = (scale, from, to) => scale ? Math.abs(Number(scale.fn(to)) - Number(scale.fn(from))) : undefined;
12
+ /**
13
+ * How a marker that draws a box per row writes a label in each: the `text` marker for them, centred on the box,
14
+ * and where the box is for it to fit to. A `rect` says it with its edges, a `cell` is a band of each axis, and a bar
15
+ * or a waffle is a band of its category axis and the stretch of its value.
16
+ */
17
+ export class BoxLabel {
18
+ /** `undefined` when the marker has no label, or does not say where its boxes are. */
19
+ static of(marker, plot = {}) {
20
+ if (!marker.label)
21
+ return undefined;
22
+ const own = BoxLabel.#shape(marker, plot.isHorizontal ?? marker.isHorizontal ?? false);
23
+ if (!own)
24
+ return undefined;
25
+ const { anchor, outside, gap, ...label } = marker.label;
26
+ const size = typeof own.box === 'function' ? own.box : own.size;
27
+ const placed = BoxLabel.#anchored({
28
+ anchor,
29
+ outside,
30
+ gap: gap ?? plot.gap
31
+ }, label, size, plot.scales);
32
+ return {
33
+ box: own.box,
34
+ marker: {
35
+ // A label outside its shape has nothing to fit to.
36
+ fit: !outside,
37
+ ...label,
38
+ ...placed,
39
+ type: 'text',
40
+ data: marker.data,
41
+ x: own.x,
42
+ y: own.y
43
+ }
44
+ };
45
+ }
46
+ /**
47
+ * What puts a label at `anchor` of its shape: the pixels it is moved from the middle - a function of the row, for
48
+ * the size of its shape is the row's own - and the sides of its point it is written on. `{}` for the middle.
49
+ */
50
+ static #anchored(where, label, size, scales) {
51
+ const { anchor = 'middle', outside = false } = where;
52
+ if (anchor === 'middle' || !size)
53
+ return {};
54
+ const gap = where.gap ?? 4;
55
+ const horizontal = anchor.endsWith('left') ? -1 : anchor.endsWith('right') ? 1 : 0;
56
+ const vertical = anchor.startsWith('top') ? -1 : anchor.startsWith('bottom') ? 1 : 0;
57
+ // Beyond an edge only means something for an edge: a corner is inside its shape.
58
+ const beyond = outside && horizontal * vertical === 0;
59
+ const away = beyond ? 1 : -1;
60
+ const move = (axis, side, row) => {
61
+ // Read when a label is drawn, not when its marker is made: the scales move as the labels register.
62
+ const current = scales?.();
63
+ const box = current ? size(row, current) : undefined;
64
+ return box ? side * (box[axis] / 2 + away * gap) : 0;
65
+ };
66
+ // A number of the label's own (or its styles') is added to the move: it is the push from the anchor.
67
+ const own = (value, fallback) => Number(typeof value === 'number' ? value : (fallback ?? 0));
68
+ const baseX = own(label.dx, label.styles?.dx);
69
+ const baseY = own(label.dy, label.styles?.dy);
70
+ return {
71
+ dx: horizontal === 0 ? label.dx : (row) => baseX + move('width', horizontal, row),
72
+ dy: vertical === 0 ? label.dy : (row) => baseY + move('height', vertical, row),
73
+ styles: {
74
+ ...label.styles,
75
+ textAnchor: label.styles?.textAnchor ??
76
+ (horizontal === 0 ? 'middle' : (horizontal === 1) === beyond ? 'start' : 'end'),
77
+ lineAnchor: label.styles?.lineAnchor ??
78
+ (vertical === 0 ? 'middle' : (vertical === 1) === beyond ? 'top' : 'bottom')
79
+ }
80
+ };
81
+ }
82
+ /** A style of the marker, when it is a number: a function of the row says nothing of a box in general. */
83
+ static #style(marker, name) {
84
+ const value = marker.styles?.[name];
85
+ return typeof value === 'number' ? value : undefined;
86
+ }
87
+ static #shape(marker, isHorizontal) {
88
+ if (marker.type === 'cell')
89
+ return BoxLabel.#cell(marker);
90
+ if (marker.type === 'bar') {
91
+ return marker.role === 'segment'
92
+ ? BoxLabel.#bar(marker, marker.isHorizontal ?? false)
93
+ : undefined;
94
+ }
95
+ if (marker.type === 'waffle')
96
+ return BoxLabel.#bar(marker, isHorizontal);
97
+ return BoxLabel.#edges(marker);
98
+ }
99
+ /** A rectangle: the box is between its edges. */
100
+ static #edges({ x, y, x1, x2, y1, y2 }) {
101
+ // A side with a value and no start, next to one with both its edges, runs from zero to it, as a bar does.
102
+ if (y1 === undefined &&
103
+ y2 === undefined &&
104
+ y !== undefined &&
105
+ x1 !== undefined &&
106
+ x2 !== undefined) {
107
+ [y1, y2] = [0, y];
108
+ }
109
+ if (x1 === undefined &&
110
+ x2 === undefined &&
111
+ x !== undefined &&
112
+ y1 !== undefined &&
113
+ y2 !== undefined) {
114
+ [x1, x2] = [0, x];
115
+ }
116
+ if ([x1, x2, y1, y2].some((edge) => edge === undefined))
117
+ return undefined;
118
+ const [left, right, top, bottom] = [x1, x2, y1, y2].map(read);
119
+ const size = (row, scales) => {
120
+ const [width, height] = [
121
+ distance(scales.x, left(row), right(row)),
122
+ distance(scales.y, top(row), bottom(row))
123
+ ];
124
+ return width === undefined || height === undefined ? undefined : { width, height };
125
+ };
126
+ return {
127
+ box: { x1, x2, y1, y2 },
128
+ size,
129
+ x: (row) => middle(left(row), right(row)),
130
+ y: (row) => middle(top(row), bottom(row))
131
+ };
132
+ }
133
+ /** A cell: a band of each axis, less its inset. */
134
+ static #cell(marker) {
135
+ if (marker.x === undefined || marker.y === undefined)
136
+ return undefined;
137
+ const inset = Number(BoxLabel.#style(marker, 'inset') ?? 0.5);
138
+ const box = (_, scales) => {
139
+ const [width, height] = [bandwidth(scales.x), bandwidth(scales.y)];
140
+ return width === undefined || height === undefined
141
+ ? undefined
142
+ : { width: width - 2 * inset, height: height - 2 * inset };
143
+ };
144
+ return { box: box, x: marker.x, y: marker.y };
145
+ }
146
+ /** A bar or a waffle: a band of the category axis, as wide as its insets leave it, and as long as its value. */
147
+ static #bar(marker, isHorizontal) {
148
+ const [category, from, to] = isHorizontal
149
+ ? [marker.y, marker.x1, marker.x2]
150
+ : [marker.x, marker.y1, marker.y2];
151
+ const value = isHorizontal ? marker.x : marker.y;
152
+ // Without both ends, the bar runs from zero to its value.
153
+ const [start, end] = from !== undefined && to !== undefined ? [from, to] : [0, value];
154
+ if (category === undefined || end === undefined)
155
+ return undefined;
156
+ const [first, last] = [start, end].map(read);
157
+ const [before, after] = isHorizontal
158
+ ? [BoxLabel.#style(marker, 'insetTop'), BoxLabel.#style(marker, 'insetBottom')]
159
+ : [BoxLabel.#style(marker, 'insetLeft'), BoxLabel.#style(marker, 'insetRight')];
160
+ const middleOf = (row) => middle(first(row), last(row));
161
+ const box = (row, scales) => {
162
+ const band = bandwidth(isHorizontal ? scales.y : scales.x);
163
+ const length = distance(isHorizontal ? scales.x : scales.y, first(row), last(row));
164
+ if (band === undefined || length === undefined)
165
+ return undefined;
166
+ const thickness = marker.thickness ?? band * (1 - Number(before ?? 0) - Number(after ?? 0));
167
+ return isHorizontal
168
+ ? { width: length, height: thickness }
169
+ : { width: thickness, height: length };
170
+ };
171
+ return isHorizontal
172
+ ? { box: box, x: middleOf, y: category }
173
+ : { box: box, x: category, y: middleOf };
174
+ }
175
+ }