@fundar/data-chart-telling 1.18.0 → 1.19.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 (220) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +2 -1
  3. package/dist/configuration/config.svelte.js +1 -1
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +2 -1
  6. package/dist/layout/coordinates/CoordinatesLayout.svelte +3 -1
  7. package/dist/layout/geometry/GeometryLayout.svelte +57 -22
  8. package/dist/layout/geometry/ScaleFreeContext.d.ts +31 -0
  9. package/dist/layout/geometry/ScaleFreeContext.js +65 -0
  10. package/dist/layout/geometry/ScaleFreeMarks.svelte +14 -0
  11. package/dist/layout/geometry/ScaleFreeMarks.svelte.d.ts +7 -0
  12. package/dist/layout/geometry/ScaleGate.svelte.d.ts +8 -0
  13. package/dist/layout/geometry/ScaleGate.svelte.js +95 -0
  14. package/dist/layout/geometry/ScaleGateMarks.svelte +15 -0
  15. package/dist/layout/geometry/ScaleGateMarks.svelte.d.ts +7 -0
  16. package/dist/layout/hover/HoverShapes.d.ts +20 -8
  17. package/dist/layout/hover/HoverShapes.js +119 -59
  18. package/dist/layout/margins/Box.d.ts +4 -0
  19. package/dist/layout/margins/Box.js +14 -0
  20. package/dist/layout/margins/PlotMargins.svelte.js +26 -1
  21. package/dist/layout/plot/{Plot.svelte.d.ts → AbstractPlot.svelte.d.ts} +3 -3
  22. package/dist/layout/styles/ValueLabels.d.ts +109 -58
  23. package/dist/layout/styles/ValueLabels.js +299 -141
  24. package/dist/markers/MarkerKinds.d.ts +79 -1
  25. package/dist/markers/MarkerKinds.js +149 -23
  26. package/dist/markers/area/AreaMarker.svelte +1 -1
  27. package/dist/markers/bar/BarMarker.svelte +8 -6
  28. package/dist/markers/bollinger/BollingerMarker.svelte +1 -1
  29. package/dist/markers/box/BoxMarker.svelte +1 -1
  30. package/dist/markers/dot/DotRadius.d.ts +11 -0
  31. package/dist/markers/dot/DotRadius.js +21 -0
  32. package/dist/markers/labels/CurveLabel.svelte +56 -0
  33. package/dist/markers/labels/CurveLabel.svelte.d.ts +9 -0
  34. package/dist/markers/labels/LabelMerge.d.ts +11 -0
  35. package/dist/markers/labels/LabelMerge.js +87 -0
  36. package/dist/markers/labels/MarkerLabel.svelte +36 -0
  37. package/dist/markers/labels/MarkerLabel.svelte.d.ts +10 -0
  38. package/dist/markers/labels/MarkerLabels.d.ts +52 -0
  39. package/dist/markers/labels/MarkerLabels.js +97 -0
  40. package/dist/markers/labels/geometry/BoxGeometry.d.ts +8 -0
  41. package/dist/markers/labels/geometry/BoxGeometry.js +101 -0
  42. package/dist/markers/labels/geometry/CurveGeometry.d.ts +34 -0
  43. package/dist/markers/labels/geometry/CurveGeometry.js +193 -0
  44. package/dist/markers/labels/geometry/PointGeometry.d.ts +9 -0
  45. package/dist/markers/labels/geometry/PointGeometry.js +22 -0
  46. package/dist/markers/labels/layouts/BoxLayout.d.ts +10 -0
  47. package/dist/markers/labels/layouts/BoxLayout.js +49 -0
  48. package/dist/markers/labels/layouts/PathLayout.d.ts +11 -0
  49. package/dist/markers/labels/layouts/PathLayout.js +91 -0
  50. package/dist/markers/labels/layouts/PointLayout.d.ts +19 -0
  51. package/dist/markers/labels/layouts/PointLayout.js +94 -0
  52. package/dist/markers/regression/RegressionMarker.svelte +1 -1
  53. package/dist/markers/text/Covering.d.ts +13 -0
  54. package/dist/markers/text/Covering.js +29 -0
  55. package/dist/markers/text/Declutter.d.ts +24 -3
  56. package/dist/markers/text/Declutter.js +46 -3
  57. package/dist/markers/text/TextMarker.svelte +90 -17
  58. package/dist/markers/text/TextMarker.svelte.d.ts +1 -1
  59. package/dist/markers/tick/TickMarker.svelte +1 -1
  60. package/dist/markers/utils/MarkerChannels.d.ts +1 -1
  61. package/dist/markers/utils/MarkerChannels.js +3 -2
  62. package/dist/markers/violin/ViolinMarker.svelte +1 -1
  63. package/dist/markers/waffle/WaffleMarker.svelte +1 -1
  64. package/dist/phases/context/RowContext.d.ts +2 -0
  65. package/dist/phases/context/RowContext.js +2 -0
  66. package/dist/phases/markers/MarkerStyles.js +14 -11
  67. package/dist/phases/markers/MarkersPhase.d.ts +2 -3
  68. package/dist/phases/markers/MarkersPhase.js +17 -11
  69. package/dist/phases/statistics/MarkerGrouping.js +5 -32
  70. package/dist/phases/statistics/MarkerInheritance.d.ts +11 -0
  71. package/dist/phases/statistics/MarkerInheritance.js +25 -0
  72. package/dist/phases/statistics/MarkerOrientation.d.ts +26 -0
  73. package/dist/phases/statistics/MarkerOrientation.js +29 -0
  74. package/dist/phases/statistics/StatisticsPhase.js +15 -15
  75. package/dist/phases/transforms/TransformsPhase.js +5 -3
  76. package/dist/plots/correlation/heatmap/Plot.svelte +2 -2
  77. package/dist/plots/correlation/heatmap/Plot.svelte.d.ts +0 -1
  78. package/dist/plots/correlation/heatmap/defaults.js +14 -1
  79. package/dist/plots/correlation/heatmap/markers.d.ts +0 -6
  80. package/dist/plots/correlation/heatmap/markers.js +5 -9
  81. package/dist/plots/correlation/scatter/Plot.svelte +2 -2
  82. package/dist/plots/correlation/scatter/Plot.svelte.d.ts +0 -1
  83. package/dist/plots/correlation/scatter/defaults.js +9 -3
  84. package/dist/plots/correlation/scatter/markers.d.ts +0 -9
  85. package/dist/plots/correlation/scatter/markers.js +21 -28
  86. package/dist/plots/distribution/box/BoxSummaries.d.ts +5 -0
  87. package/dist/plots/distribution/box/BoxSummaries.js +9 -0
  88. package/dist/plots/distribution/box/Plot.svelte +2 -2
  89. package/dist/plots/distribution/box/Plot.svelte.d.ts +0 -1
  90. package/dist/plots/distribution/box/defaults.js +11 -0
  91. package/dist/plots/distribution/box/markers.js +10 -1
  92. package/dist/plots/distribution/density/Plot.svelte +2 -2
  93. package/dist/plots/distribution/density/Plot.svelte.d.ts +0 -1
  94. package/dist/plots/distribution/density/defaults.js +16 -0
  95. package/dist/plots/distribution/density/markers.js +5 -1
  96. package/dist/plots/distribution/histogram/Plot.svelte +2 -2
  97. package/dist/plots/distribution/histogram/Plot.svelte.d.ts +0 -1
  98. package/dist/plots/distribution/histogram/defaults.js +16 -11
  99. package/dist/plots/distribution/histogram/markers.js +3 -3
  100. package/dist/plots/distribution/pyramid/Plot.svelte +2 -2
  101. package/dist/plots/distribution/pyramid/Plot.svelte.d.ts +0 -1
  102. package/dist/plots/distribution/pyramid/defaults.js +9 -2
  103. package/dist/plots/distribution/pyramid/markers.d.ts +0 -8
  104. package/dist/plots/distribution/pyramid/markers.js +6 -16
  105. package/dist/plots/distribution/violin/Plot.svelte +2 -2
  106. package/dist/plots/distribution/violin/Plot.svelte.d.ts +0 -1
  107. package/dist/plots/distribution/violin/defaults.js +11 -0
  108. package/dist/plots/distribution/violin/markers.js +27 -3
  109. package/dist/plots/evolution/area/Plot.svelte +2 -2
  110. package/dist/plots/evolution/area/Plot.svelte.d.ts +0 -1
  111. package/dist/plots/evolution/area/defaults.js +4 -7
  112. package/dist/plots/evolution/area/markers.js +3 -1
  113. package/dist/plots/evolution/line/Plot.svelte +2 -2
  114. package/dist/plots/evolution/line/Plot.svelte.d.ts +0 -1
  115. package/dist/plots/evolution/line/defaults.js +4 -6
  116. package/dist/plots/evolution/line/markers.js +3 -1
  117. package/dist/plots/evolution/slope/Plot.svelte +2 -2
  118. package/dist/plots/evolution/slope/Plot.svelte.d.ts +0 -1
  119. package/dist/plots/evolution/slope/defaults.js +4 -3
  120. package/dist/plots/evolution/slope/markers.d.ts +4 -3
  121. package/dist/plots/evolution/slope/markers.js +16 -9
  122. package/dist/plots/evolution/sparkline/Plot.svelte +2 -2
  123. package/dist/plots/evolution/sparkline/Plot.svelte.d.ts +0 -1
  124. package/dist/plots/evolution/sparkline/defaults.js +1 -1
  125. package/dist/plots/evolution/sparkline/markers.js +3 -1
  126. package/dist/plots/generic/Plot.svelte +42 -0
  127. package/dist/plots/generic/Plot.svelte.d.ts +25 -0
  128. package/dist/plots/generic/defaults.d.ts +4 -0
  129. package/dist/plots/generic/defaults.js +7 -0
  130. package/dist/plots/map/geo/Plot.svelte +2 -2
  131. package/dist/plots/map/geo/Plot.svelte.d.ts +0 -1
  132. package/dist/plots/part-of-a-whole/treemap/Plot.svelte +2 -2
  133. package/dist/plots/part-of-a-whole/treemap/Plot.svelte.d.ts +0 -1
  134. package/dist/plots/part-of-a-whole/treemap/defaults.js +2 -2
  135. package/dist/plots/ranking/bar/Plot.svelte +2 -2
  136. package/dist/plots/ranking/bar/Plot.svelte.d.ts +0 -1
  137. package/dist/plots/ranking/bar/defaults.js +13 -3
  138. package/dist/plots/ranking/bar/markers.js +10 -27
  139. package/dist/plots/ranking/bullet/Plot.svelte +2 -2
  140. package/dist/plots/ranking/bullet/Plot.svelte.d.ts +0 -1
  141. package/dist/plots/ranking/bullet/defaults.js +20 -0
  142. package/dist/plots/ranking/bullet/markers.js +7 -1
  143. package/dist/plots/ranking/cleveland/ClevelandMarkers.d.ts +2 -9
  144. package/dist/plots/ranking/cleveland/ClevelandMarkers.js +65 -47
  145. package/dist/plots/ranking/cleveland/Plot.svelte +2 -2
  146. package/dist/plots/ranking/cleveland/Plot.svelte.d.ts +0 -1
  147. package/dist/plots/ranking/cleveland/defaults.js +33 -2
  148. package/dist/plots/ranking/dumbbell/Plot.svelte +2 -2
  149. package/dist/plots/ranking/dumbbell/Plot.svelte.d.ts +0 -1
  150. package/dist/plots/ranking/forest/Plot.svelte +2 -2
  151. package/dist/plots/ranking/forest/Plot.svelte.d.ts +0 -1
  152. package/dist/plots/ranking/lollipop/Plot.svelte +2 -2
  153. package/dist/plots/ranking/lollipop/Plot.svelte.d.ts +0 -1
  154. package/dist/types/layout/context.d.ts +2 -0
  155. package/dist/types/layout/styles/bullet.d.ts +4 -1
  156. package/dist/types/layout/styles/values.d.ts +51 -11
  157. package/dist/types/markers/axis/area.d.ts +5 -1
  158. package/dist/types/markers/axis/bar.d.ts +5 -2
  159. package/dist/types/markers/axis/bollinger.d.ts +2 -1
  160. package/dist/types/markers/axis/box.d.ts +2 -1
  161. package/dist/types/markers/axis/cell.d.ts +4 -1
  162. package/dist/types/markers/axis/line.d.ts +4 -1
  163. package/dist/types/markers/axis/rect.d.ts +4 -1
  164. package/dist/types/markers/axis/regression.d.ts +2 -1
  165. package/dist/types/markers/axis/tick.d.ts +2 -1
  166. package/dist/types/markers/axis/trail.d.ts +4 -1
  167. package/dist/types/markers/axis/violin.d.ts +2 -1
  168. package/dist/types/markers/axis/waffle.d.ts +5 -1
  169. package/dist/types/markers/basic/dot.d.ts +4 -1
  170. package/dist/types/markers/basic/text.d.ts +25 -21
  171. package/dist/types/markers/channels/commonMarkerConfigWithLabel.d.ts +5 -9
  172. package/dist/types/markers/channels/commonMarkerConfigWithOrientation.d.ts +5 -0
  173. package/dist/types/markers/channels/commonMarkerConfigWithOrientation.js +1 -0
  174. package/dist/types/markers/labels/box.d.ts +57 -0
  175. package/dist/types/markers/labels/box.js +1 -0
  176. package/dist/types/markers/labels/common.d.ts +15 -0
  177. package/dist/types/markers/labels/common.js +1 -0
  178. package/dist/types/markers/labels/path.d.ts +52 -0
  179. package/dist/types/markers/labels/path.js +1 -0
  180. package/dist/types/markers/labels/point.d.ts +40 -0
  181. package/dist/types/markers/labels/point.js +1 -0
  182. package/dist/types/markers/traits.d.ts +10 -0
  183. package/dist/types/phases/markers/phase.d.ts +1 -8
  184. package/dist/types/phases/statistics/phase.d.ts +8 -3
  185. package/dist/types/phases/styles/phase.d.ts +15 -3
  186. package/dist/types/phases/transforms/phase.d.ts +2 -0
  187. package/dist/types/plots/constants.d.ts +0 -12
  188. package/dist/types/plots/correlation/heatmap/props.d.ts +3 -2
  189. package/dist/types/plots/correlation/scatter/props.d.ts +3 -2
  190. package/dist/types/plots/distribution/box/props.d.ts +6 -2
  191. package/dist/types/plots/distribution/density/props.d.ts +2 -1
  192. package/dist/types/plots/distribution/histogram/props.d.ts +3 -2
  193. package/dist/types/plots/distribution/pyramid/props.d.ts +3 -2
  194. package/dist/types/plots/distribution/violin/props.d.ts +4 -1
  195. package/dist/types/plots/evolution/area/props.d.ts +3 -2
  196. package/dist/types/plots/evolution/line/props.d.ts +3 -2
  197. package/dist/types/plots/generic/props.d.ts +16 -0
  198. package/dist/types/plots/generic/props.js +1 -0
  199. package/dist/types/plots/generic/state.d.ts +4 -0
  200. package/dist/types/plots/generic/state.js +1 -0
  201. package/dist/types/plots/part-of-a-whole/treemap/props.d.ts +4 -2
  202. package/dist/types/plots/ranking/bar/props.d.ts +3 -2
  203. package/dist/types/plots/ranking/cleveland/props.d.ts +3 -2
  204. package/dist/types/plots/ranking/dumbbell/props.d.ts +3 -2
  205. package/dist/types/plots/ranking/forest/props.d.ts +5 -2
  206. package/dist/types/plots/ranking/lollipop/props.d.ts +3 -2
  207. package/docs/architecture.md +9 -9
  208. package/docs/markers.md +2 -4
  209. package/docs/plots/ranking/cleveland.md +1 -1
  210. package/docs/styles-and-segments.md +1 -14
  211. package/docs/value-labels.md +159 -0
  212. package/examples/BarValues.svelte +1 -1
  213. package/llms-full.txt +2 -2
  214. package/llms.txt +1 -0
  215. package/package.json +3 -1
  216. package/dist/markers/utils/BoxLabel.d.ts +0 -58
  217. package/dist/markers/utils/BoxLabel.js +0 -175
  218. package/dist/markers/utils/MarkerLabel.svelte +0 -33
  219. package/dist/markers/utils/MarkerLabel.svelte.d.ts +0 -10
  220. /package/dist/layout/plot/{Plot.svelte → AbstractPlot.svelte} +0 -0
@@ -0,0 +1,49 @@
1
+ /** The `text` marker that writes a `'box'` label, anchored in the box of each shape. */
2
+ export class BoxLayout {
3
+ static build(label, geometry, plot = {}) {
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ const { anchor, gap, layout, ...text } = label;
6
+ const placed = BoxLayout.#anchored({ anchor, gap: gap ?? plot.gap }, text, geometry.size, plot.scales);
7
+ return {
8
+ box: geometry.box,
9
+ marker: {
10
+ fit: true,
11
+ ...text,
12
+ ...placed,
13
+ type: 'text',
14
+ data: plot.data,
15
+ x: geometry.x,
16
+ y: geometry.y
17
+ }
18
+ };
19
+ }
20
+ /** The offset (per row, as each box has its own size) and text anchors that put a label at `anchor`. */
21
+ static #anchored(where, label, size, scales) {
22
+ const { anchor = 'middle' } = where;
23
+ if (anchor === 'middle' || !size)
24
+ return {};
25
+ const gap = where.gap ?? 4;
26
+ const horizontal = anchor.endsWith('left') ? -1 : anchor.endsWith('right') ? 1 : 0;
27
+ const vertical = anchor.startsWith('top') ? -1 : anchor.startsWith('bottom') ? 1 : 0;
28
+ const move = (axis, side, row) => {
29
+ // Read at draw time: the scales move as the labels register.
30
+ const current = scales?.();
31
+ const box = current ? size(row, current) : undefined;
32
+ return box ? side * (box[axis] / 2 - gap) : 0;
33
+ };
34
+ const own = (value, fallback) => Number(typeof value === 'number' ? value : (fallback ?? 0));
35
+ const baseX = own(label.dx, label.styles?.dx);
36
+ const baseY = own(label.dy, label.styles?.dy);
37
+ return {
38
+ dx: horizontal === 0 ? label.dx : (row) => baseX + move('width', horizontal, row),
39
+ dy: vertical === 0 ? label.dy : (row) => baseY + move('height', vertical, row),
40
+ styles: {
41
+ ...label.styles,
42
+ textAnchor: label.styles?.textAnchor ??
43
+ (horizontal === 0 ? 'middle' : horizontal === 1 ? 'end' : 'start'),
44
+ lineAnchor: label.styles?.lineAnchor ??
45
+ (vertical === 0 ? 'middle' : vertical === 1 ? 'bottom' : 'top')
46
+ }
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,11 @@
1
+ import type { BoxGeometryOf, BoxLabelMarker, BoxScales } from '../../../types/markers/labels/box';
2
+ import type { PathLabelConfig } from '../../../types/markers/labels/path';
3
+ /** The `text` marker that writes a `'path'` label along an edge of each shape's box. */
4
+ export declare class PathLayout {
5
+ #private;
6
+ static build(label: PathLabelConfig, geometry: BoxGeometryOf, plot?: {
7
+ data?: unknown[];
8
+ scales?: () => BoxScales | undefined;
9
+ gap?: number;
10
+ }): BoxLabelMarker | undefined;
11
+ }
@@ -0,0 +1,91 @@
1
+ /** Each page edge's outward direction. */
2
+ const NORMALS = {
3
+ top: [0, -1],
4
+ bottom: [0, 1],
5
+ left: [-1, 0],
6
+ right: [1, 0]
7
+ };
8
+ const OPPOSITE = {
9
+ top: 'bottom',
10
+ bottom: 'top',
11
+ left: 'right',
12
+ right: 'left'
13
+ };
14
+ /** The `text` marker that writes a `'path'` label along an edge of each shape's box. */
15
+ export class PathLayout {
16
+ static build(label, geometry, plot = {}) {
17
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
+ const { layout, edge, side = 'outside', offset = 'middle', gap, ...text } = label;
19
+ const { size, ends } = geometry;
20
+ if (!size)
21
+ return undefined;
22
+ const sign = side === 'outside' ? 1 : side === 'inside' ? -1 : 0;
23
+ const space = sign * (gap ?? plot.gap ?? 4);
24
+ const along = PathLayout.#fraction(offset);
25
+ const own = (value, fallback) => Number(typeof value === 'number' ? value : (fallback ?? 0));
26
+ const [baseX, baseY] = [own(text.dx, text.styles?.dx), own(text.dy, text.styles?.dy)];
27
+ // Read at draw time: the scales move as the labels register.
28
+ const placed = (row) => {
29
+ const scales = plot.scales?.();
30
+ const box = scales ? size(row, scales) : undefined;
31
+ const page = PathLayout.#pageEdge(edge ?? (ends ? 'tip' : 'top'), ends, row, scales);
32
+ const [h, v] = NORMALS[page];
33
+ const [width, height] = [box?.width ?? 0, box?.height ?? 0];
34
+ const across = (extent) => extent / 2 + space;
35
+ const end = (from, to) => along === 0 ? from : along === 1 ? to : 'middle';
36
+ const out = (towards, before, after) => sign === 0 ? 'middle' : towards * sign > 0 ? after : before;
37
+ return v !== 0
38
+ ? {
39
+ dx: (along - 0.5) * width,
40
+ dy: v * across(height),
41
+ textAnchor: end('start', 'end'),
42
+ lineAnchor: out(v, 'bottom', 'top')
43
+ }
44
+ : {
45
+ dx: h * across(width),
46
+ dy: (along - 0.5) * height,
47
+ textAnchor: out(h, 'end', 'start'),
48
+ lineAnchor: end('top', 'bottom')
49
+ };
50
+ };
51
+ const styles = text.styles ?? {};
52
+ return {
53
+ box: geometry.box,
54
+ marker: {
55
+ fit: false,
56
+ ...text,
57
+ dx: (row) => baseX + placed(row).dx,
58
+ dy: (row) => baseY + placed(row).dy,
59
+ ...(styles.textAnchor ? {} : { textAnchor: (row) => placed(row).textAnchor }),
60
+ ...(styles.lineAnchor ? {} : { lineAnchor: (row) => placed(row).lineAnchor }),
61
+ type: 'text',
62
+ data: plot.data,
63
+ x: geometry.x,
64
+ y: geometry.y
65
+ }
66
+ };
67
+ }
68
+ /** How far along the edge, from its left or top end. */
69
+ static #fraction(offset) {
70
+ if (offset === 'start')
71
+ return 0;
72
+ if (offset === 'end')
73
+ return 1;
74
+ if (offset === 'middle')
75
+ return 0.5;
76
+ const percent = Number.parseFloat(offset);
77
+ return Number.isFinite(percent) ? Math.min(Math.max(percent / 100, 0), 1) : 0.5;
78
+ }
79
+ /** The page edge a row's `edge` is: its tip is the end of the shape its value runs to. */
80
+ static #pageEdge(edge, ends, row, scales) {
81
+ if (edge !== 'tip' && edge !== 'base')
82
+ return edge;
83
+ const scale = ends && scales?.[ends.axis];
84
+ const [from, to] = scale
85
+ ? [Number(scale.fn(ends.from(row))), Number(scale.fn(ends.to(row)))]
86
+ : [0, 0];
87
+ // Pixels grow rightward and downward: a value up the page runs to a smaller `y`.
88
+ const tip = ends?.axis === 'x' ? (to >= from ? 'right' : 'left') : to <= from ? 'top' : 'bottom';
89
+ return edge === 'tip' ? tip : OPPOSITE[tip];
90
+ }
91
+ }
@@ -0,0 +1,19 @@
1
+ import type { TextMarkerConfig } from '../../../types/markers/basic/text';
2
+ import type { PointGeometryOf, PointLabelConfig, PointSide } from '../../../types/markers/labels/point';
3
+ import type { LineAnchor, TextAnchor } from '../../../types/plots/constants';
4
+ /** The `text` marker that writes a `'point'` label beside the points it selects. */
5
+ export declare class PointLayout {
6
+ #private;
7
+ static build(label: PointLabelConfig, point: PointGeometryOf, rows: unknown[], plot?: {
8
+ grouping?: unknown;
9
+ valueAxis?: 'x' | 'y';
10
+ gap?: number;
11
+ }): TextMarkerConfig | undefined;
12
+ /** The offset and anchors that write a label on `side` of its point, `gap` from it. */
13
+ static sideOf(side: PointSide, gap?: number): {
14
+ dx: number;
15
+ dy: number;
16
+ textAnchor: TextAnchor;
17
+ lineAnchor: LineAnchor;
18
+ };
19
+ }
@@ -0,0 +1,94 @@
1
+ import { ValueSelection } from '../../../phases/transforms/ValueSelection';
2
+ const read = (channel) => (row) => typeof channel === 'function'
3
+ ? channel(row)
4
+ : typeof channel === 'string' || typeof channel === 'symbol'
5
+ ? row?.[channel]
6
+ : channel;
7
+ const placed = (value) => value !== null && value !== undefined && !(typeof value === 'number' && Number.isNaN(value));
8
+ /** The `text` marker that writes a `'point'` label beside the points it selects. */
9
+ export class PointLayout {
10
+ static build(label, point, rows, plot = {}) {
11
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
12
+ const { layout, select, anchor, side, gap, ...text } = label;
13
+ const [x, y] = [read(point.x), read(point.y)];
14
+ const placeable = rows.filter((row) => placed(x(row)) && placed(y(row)));
15
+ const data = select === undefined
16
+ ? placeable
17
+ : typeof select === 'function'
18
+ ? placeable.filter(select)
19
+ : ValueSelection.pick(placeable, ValueSelection.transforms(select, undefined, plot.valueAxis ?? 'y'), { x, y, group: plot.grouping ?? (() => 0) });
20
+ if (data.length === 0)
21
+ return undefined;
22
+ const onBox = point.size && anchor && anchor !== 'middle'
23
+ ? PointLayout.#onBox(text, point.size, anchor, gap ?? plot.gap ?? 4)
24
+ : side
25
+ ? { styles: { ...PointLayout.sideOf(side, gap), ...PointLayout.#defined(text.styles) } }
26
+ : {};
27
+ // A label on its own point is read in its shape: one drawn over it hides it.
28
+ const { size, round } = point;
29
+ const onPoint = !(size && anchor && anchor !== 'middle') && (side === undefined || side === 'on');
30
+ const covers = onPoint && size
31
+ ? (row, scales) => {
32
+ const shape = size(row, scales);
33
+ return shape && { ...shape, round };
34
+ }
35
+ : undefined;
36
+ // A point shifted off its `x`/`y` (a series' slot of its band) carries its label, and its anchor's offset, along.
37
+ const anchored = onBox.origin;
38
+ const origin = point.shift &&
39
+ ((row, scales) => {
40
+ const shift = point.shift(row, scales) ?? { dx: 0, dy: 0 };
41
+ const own = anchored?.(row, scales) ?? { dx: 0, dy: 0 };
42
+ return { dx: shift.dx + own.dx, dy: shift.dy + own.dy };
43
+ });
44
+ return {
45
+ ...text,
46
+ ...onBox,
47
+ ...(origin ? { origin } : {}),
48
+ ...(covers ? { covers } : {}),
49
+ type: 'text',
50
+ data,
51
+ x: point.x,
52
+ y: point.y
53
+ };
54
+ }
55
+ static #defined(styles) {
56
+ return Object.fromEntries(Object.entries(styles ?? {}).filter(([, value]) => value !== undefined));
57
+ }
58
+ /** The offset and anchors that write a label on `side` of its point, `gap` from it. */
59
+ static sideOf(side, gap) {
60
+ const across = gap ?? 6;
61
+ const along = gap ?? 8;
62
+ switch (side) {
63
+ case 'after':
64
+ return { dx: across, dy: 0, textAnchor: 'start', lineAnchor: 'middle' };
65
+ case 'before':
66
+ return { dx: -across, dy: 0, textAnchor: 'end', lineAnchor: 'middle' };
67
+ case 'above':
68
+ return { dx: 0, dy: -along, textAnchor: 'middle', lineAnchor: 'bottom' };
69
+ case 'below':
70
+ return { dx: 0, dy: along, textAnchor: 'middle', lineAnchor: 'top' };
71
+ default:
72
+ return { dx: 0, dy: 0, textAnchor: 'middle', lineAnchor: 'middle' };
73
+ }
74
+ }
75
+ /** Starts each label at `anchor` on its box, written away from it. */
76
+ static #onBox(text, size, anchor, gap) {
77
+ const horizontal = anchor.endsWith('left') ? -1 : anchor.endsWith('right') ? 1 : 0;
78
+ const vertical = anchor.startsWith('top') ? -1 : anchor.startsWith('bottom') ? 1 : 0;
79
+ const styles = text.styles ?? {};
80
+ return {
81
+ origin: (row, scales) => {
82
+ const box = size(row, scales);
83
+ return box && { dx: (horizontal * box.width) / 2, dy: (vertical * box.height) / 2 };
84
+ },
85
+ styles: {
86
+ ...styles,
87
+ dx: styles.dx ?? horizontal * gap,
88
+ dy: styles.dy ?? vertical * gap,
89
+ textAnchor: styles.textAnchor ?? ['end', 'middle', 'start'][horizontal + 1],
90
+ lineAnchor: styles.lineAnchor ?? ['bottom', 'middle', 'top'][vertical + 1]
91
+ }
92
+ };
93
+ }
94
+ }
@@ -22,7 +22,7 @@
22
22
  const events = MarkerEvents.use(() => marker);
23
23
 
24
24
  const plot = usePlot();
25
- const isHorizontal = $derived(plot.axes?.isHorizontal ?? false);
25
+ const isHorizontal = $derived(marker.isHorizontal ?? plot.axes?.isHorizontal ?? false);
26
26
  const RegressionMark = $derived(isHorizontal ? RegressionX : RegressionY);
27
27
 
28
28
  const data = $derived(marker.data ?? []);
@@ -0,0 +1,13 @@
1
+ import type { Box } from '../../layout/margins/Box';
2
+ /** A row's shape in pixels: its box, or the circle in it. */
3
+ type Shape = {
4
+ box: Box;
5
+ round?: boolean;
6
+ };
7
+ /** Which labels the shapes drawn after their own row cover, as a dot drawn over another hides that one's label. */
8
+ export declare class Covering {
9
+ #private;
10
+ /** The indices of `labels` some shape of a later row meets; shapes and labels are in draw order, one per row. */
11
+ static hidden(labels: Box[], shapes: (Shape | undefined)[]): Set<number>;
12
+ }
13
+ export {};
@@ -0,0 +1,29 @@
1
+ /** Which labels the shapes drawn after their own row cover, as a dot drawn over another hides that one's label. */
2
+ export class Covering {
3
+ /** The indices of `labels` some shape of a later row meets; shapes and labels are in draw order, one per row. */
4
+ static hidden(labels, shapes) {
5
+ const hidden = new Set();
6
+ labels.forEach((label, i) => {
7
+ for (let j = i + 1; j < shapes.length; j++) {
8
+ const shape = shapes[j];
9
+ if (shape && Covering.#meets(shape, label)) {
10
+ hidden.add(i);
11
+ return;
12
+ }
13
+ }
14
+ });
15
+ return hidden;
16
+ }
17
+ static #meets({ box, round }, label) {
18
+ if (!label.intersects(box))
19
+ return false;
20
+ if (!round)
21
+ return true;
22
+ // The circle meets the label where the label's nearest point to its center is within its radius.
23
+ const r = Math.min(box.width, box.height) / 2;
24
+ const [cx, cy] = [box.x + box.width / 2, box.y + box.height / 2];
25
+ const nx = Math.min(Math.max(cx, label.left), label.right);
26
+ const ny = Math.min(Math.max(cy, label.top), label.bottom);
27
+ return (nx - cx) ** 2 + (ny - cy) ** 2 < r * r;
28
+ }
29
+ }
@@ -1,11 +1,32 @@
1
+ import { Box } from '../../layout/margins/Box';
1
2
  import type { LabelLane } from '../../types/markers/basic/text';
3
+ import type { BoxAnchors, BoxFlip } from '../../types/layout/box';
2
4
  /**
3
- * Lays out point labels that would overlap: spreads their pixel `y` apart,
4
- * gives each moved label its own lane for its leader line, and lines every
5
- * label up on one shared column once any of them clusters.
5
+ * Lays out point labels that would overlap: either spreads their pixel `y`
6
+ * apart into lanes ({@link Declutter.buildLanes}), or puts each one on a free
7
+ * spot around its point ({@link Declutter.around}).
6
8
  */
7
9
  export declare class Declutter {
8
10
  #private;
11
+ /**
12
+ * Each label on the first spot around its point - its `preferred` one, then on clockwise -
13
+ * that overlaps no label placed before it nor any point, inside `bounds`.
14
+ * A label with no free spot is left out of the map: it is not drawn.
15
+ */
16
+ static around<K>(args: {
17
+ items: {
18
+ key: K;
19
+ px: number;
20
+ py: number;
21
+ box: Box;
22
+ }[];
23
+ preferred: BoxAnchors;
24
+ /** Distance (px) from the point to the label. */
25
+ gap: number;
26
+ bounds: Box;
27
+ /** Room (px) kept clear around every point. */
28
+ clearance?: number;
29
+ }): Map<K, BoxFlip>;
9
30
  /**
10
31
  * Lays out labels keyed by each item's `key`; fewer than two need none. A
11
32
  * leftward fan with no room before the content box's left edge (the Y
@@ -1,10 +1,53 @@
1
+ import { Box } from '../../layout/margins/Box';
1
2
  const clamp = (value, lo, hi) => Math.min(Math.max(value, lo), hi);
3
+ /** The spots around a point, clockwise from above it. */
4
+ const AROUND = [
5
+ { x: 'middle', y: 'before' },
6
+ { x: 'after', y: 'before' },
7
+ { x: 'after', y: 'middle' },
8
+ { x: 'after', y: 'after' },
9
+ { x: 'middle', y: 'after' },
10
+ { x: 'before', y: 'after' },
11
+ { x: 'before', y: 'middle' },
12
+ { x: 'before', y: 'before' }
13
+ ];
2
14
  /**
3
- * Lays out point labels that would overlap: spreads their pixel `y` apart,
4
- * gives each moved label its own lane for its leader line, and lines every
5
- * label up on one shared column once any of them clusters.
15
+ * Lays out point labels that would overlap: either spreads their pixel `y`
16
+ * apart into lanes ({@link Declutter.buildLanes}), or puts each one on a free
17
+ * spot around its point ({@link Declutter.around}).
6
18
  */
7
19
  export class Declutter {
20
+ /**
21
+ * Each label on the first spot around its point - its `preferred` one, then on clockwise -
22
+ * that overlaps no label placed before it nor any point, inside `bounds`.
23
+ * A label with no free spot is left out of the map: it is not drawn.
24
+ */
25
+ static around(args) {
26
+ const { items, preferred, gap, bounds, clearance = 3 } = args;
27
+ // Its own spot first, then on clockwise; a label on the point itself tries it, then goes round from above.
28
+ const first = AROUND.findIndex((spot) => spot.x === preferred.x && spot.y === preferred.y);
29
+ const ring = [...AROUND.slice(Math.max(first, 0)), ...AROUND.slice(0, Math.max(first, 0))];
30
+ const spots = first < 0 ? [preferred, ...ring] : ring;
31
+ const taken = items.map(({ px, py }) => new Box(px - clearance, py - clearance, 2 * clearance, 2 * clearance));
32
+ const placed = [];
33
+ const out = new Map();
34
+ items.forEach(({ key, px, py, box }, index) => {
35
+ const clashes = (at) => placed.some((other) => at.intersects(other)) ||
36
+ taken.some((other, i) => i !== index && at.intersects(other));
37
+ for (const spot of spots) {
38
+ const corner = spot.x !== 'middle' && spot.y !== 'middle';
39
+ const space = corner ? gap * Math.SQRT1_2 : gap;
40
+ const at = box.beside({ x: px, y: py }, spot, { x: space, y: space });
41
+ if (!at.inside(bounds) || clashes(at))
42
+ continue;
43
+ placed.push(at);
44
+ const offset = (side) => side === 'after' ? space : side === 'before' ? -space : 0;
45
+ out.set(key, { anchor: spot, dx: offset(spot.x), dy: offset(spot.y) });
46
+ break;
47
+ }
48
+ });
49
+ return out;
50
+ }
8
51
  /**
9
52
  * Lays out labels keyed by each item's `key`; fewer than two need none. A
10
53
  * leftward fan with no room before the content box's left edge (the Y
@@ -8,13 +8,14 @@
8
8
  import { RowOffset } from '../utils/RowOffset';
9
9
  import RowOffsetMarks from '../utils/RowOffsetMarks.svelte';
10
10
  import { Declutter } from './Declutter';
11
+ import { Covering } from './Covering';
11
12
  import { SvelteMap } from 'svelte/reactivity';
12
13
  import { TextFit, type FittedText } from './TextFit';
13
14
  import { NumberFormat } from '../../utils/NumberFormat';
14
15
  import { PlotMargins } from '../../layout/margins/PlotMargins.svelte';
15
16
  import { Box } from '../../layout/margins/Box';
16
17
  import type { TextMarkerConfig } from '../../types/markers/common';
17
- import type { BoxChannels, BoxSizer } from '../utils/BoxLabel';
18
+ import type { BoxChannels, BoxSizer } from '../../types/markers/labels/box';
18
19
  import type { TextAnchor } from '../../types/plots/constants';
19
20
 
20
21
  /**
@@ -77,7 +78,13 @@
77
78
  : (marker.textAnchor ?? styles?.textAnchor)
78
79
  )
79
80
  );
80
- const lineAnchor = $derived(styles?.lineAnchor ?? 'middle');
81
+ const lineAnchor = $derived(
82
+ (typeof marker.lineAnchor === 'function' ? undefined : marker.lineAnchor) ??
83
+ styles?.lineAnchor ??
84
+ 'middle'
85
+ );
86
+ const lineAnchorAt = (d: TData) =>
87
+ typeof marker.lineAnchor === 'function' ? marker.lineAnchor(d) : lineAnchor;
81
88
  // A label in a slice of its band sits off the band's middle, at its drawn width.
82
89
  const nudge = $derived(BandNudge.of(plot.scales, marker.bandSlice));
83
90
  // The fixed offset, which `declutter` and `edgeAware` place labels by; a function of the row moves each label after that, through `RowOffsetMarks`.
@@ -92,9 +99,15 @@
92
99
 
93
100
  const declutter = $derived.by(() => {
94
101
  if (!marker.declutter) return undefined;
95
- const own = marker.declutter === true ? {} : marker.declutter;
102
+ const own =
103
+ marker.declutter === true
104
+ ? {}
105
+ : typeof marker.declutter === 'string'
106
+ ? { mode: marker.declutter }
107
+ : marker.declutter;
96
108
  const defaults = cfg.text.declutter;
97
109
  return {
110
+ mode: own.mode ?? 'lanes',
98
111
  elbowGap: own.elbowGap ?? defaults.elbowGap,
99
112
  laneStep: own.laneStep ?? defaults.laneStep,
100
113
  labelGap: own.labelGap ?? defaults.labelGap,
@@ -104,8 +117,16 @@
104
117
  };
105
118
  });
106
119
  const perRow = $derived(
107
- !!declutter || !!marker.edgeAware || events.any || typeof marker.textAnchor === 'function'
120
+ !!declutter ||
121
+ !!marker.edgeAware ||
122
+ !!marker.origin ||
123
+ events.any ||
124
+ !!marker.covers ||
125
+ typeof marker.textAnchor === 'function' ||
126
+ typeof marker.lineAnchor === 'function'
108
127
  );
128
+ /** Where a row's point sits off its `x`/`y`. */
129
+ const originOf = (d: TData) => marker.origin?.(d, plot.scales) ?? { dx: 0, dy: 0 };
109
130
  const dir = $derived(Math.sign(dx) || 1);
110
131
 
111
132
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -232,21 +253,24 @@
232
253
  const points = $derived.by(() => {
233
254
  const xScale = plot.scales.x?.fn;
234
255
  const yScale = plot.scales.y?.fn;
235
- if ((!declutter && !marker.edgeAware) || !xScale || !yScale) return undefined;
256
+ if ((!declutter && !marker.edgeAware && !marker.covers) || !xScale || !yScale) return undefined;
236
257
  const xOf = channel(x ?? 0);
237
258
  const yOf = channel(y ?? 0);
238
259
  // A band's position is its start; a mark sits at its middle.
239
260
  const middle = (scale: typeof plot.scales.x) =>
240
261
  scale?.type === 'band' ? scale.fn.bandwidth() / 2 : 0;
241
262
  const [dx0, dy0] = [middle(plot.scales.x), middle(plot.scales.y)];
242
- return displayData.map((d) => ({
243
- px: Number(xScale(xOf(d) as never)) + dx0,
244
- py: Number(yScale(yOf(d) as never)) + dy0
245
- }));
263
+ return displayData.map((d) => {
264
+ const origin = originOf(d);
265
+ return {
266
+ px: Number(xScale(xOf(d) as never)) + dx0 + origin.dx,
267
+ py: Number(yScale(yOf(d) as never)) + dy0 + origin.dy
268
+ };
269
+ });
246
270
  });
247
271
 
248
272
  const lanes = $derived(
249
- declutter && points && bounds
273
+ declutter?.mode === 'lanes' && points && bounds
250
274
  ? Declutter.buildLanes({
251
275
  items: points.map((p, key) => ({ key, ...p })),
252
276
  dx,
@@ -259,8 +283,56 @@
259
283
  : undefined
260
284
  );
261
285
 
262
- /** Each row's anchor and offset, flipped where its text would cross the content box's edge. */
286
+ /** The rows whose label a shape drawn after its own row covers: their labels are left out. */
287
+ const covered = $derived.by(() => {
288
+ const covers = marker.covers;
289
+ if (!covers || !points) return undefined;
290
+ const shapes = displayData.map((d, i) => {
291
+ const shape = covers(d, plot.scales);
292
+ if (!shape) return undefined;
293
+ const { px, py } = points[i];
294
+ const { width, height, round } = shape;
295
+ return { box: new Box(px - width / 2, py - height / 2, width, height), round };
296
+ });
297
+ const labels = displayData.map((d, i) =>
298
+ Box.ofText(textOf(d), fontSize, cfg).beside(
299
+ { x: points[i].px + dx, y: points[i].py + dy },
300
+ { x: TEXT_SIDES[anchorAt(d)], y: LINE_SIDES[lineAnchorAt(d)] }
301
+ )
302
+ );
303
+ return Covering.hidden(labels, shapes);
304
+ });
305
+
306
+ /** Each row's spot around its point, `null` for a row with none free. */
307
+ const around = $derived.by(() => {
308
+ if (declutter?.mode !== 'around' || !points) return undefined;
309
+ const within = bounds ?? Box.size(plot.plotWidth, plot.plotHeight);
310
+ const spots = Declutter.around({
311
+ items: points.map((p, key) => ({
312
+ key,
313
+ ...p,
314
+ box: Box.ofText(textOf(displayData[key]), fontSize, cfg)
315
+ })),
316
+ preferred: { x: TEXT_SIDES[textAnchor], y: LINE_SIDES[lineAnchor] },
317
+ gap: Math.max(Math.abs(dx), Math.abs(dy)) || declutter.labelGap,
318
+ bounds: within
319
+ });
320
+ return displayData.map((_, i) => {
321
+ const spot = spots.get(i);
322
+ return spot
323
+ ? {
324
+ dx: spot.dx,
325
+ dy: spot.dy,
326
+ textAnchor: TEXT_ANCHORS[spot.anchor.x],
327
+ lineAnchor: LINE_ANCHORS[spot.anchor.y]
328
+ }
329
+ : null;
330
+ });
331
+ });
332
+
333
+ /** Each row's anchor and offset: on its spot around its point, else flipped where its text would cross the content box's edge. */
263
334
  const placements = $derived.by(() => {
335
+ if (around) return around;
264
336
  if (!marker.edgeAware || !points) return undefined;
265
337
  const within = bounds ?? Box.size(plot.plotWidth, plot.plotHeight);
266
338
  const anchor = { x: TEXT_SIDES[textAnchor], y: LINE_SIDES[lineAnchor] };
@@ -342,12 +414,13 @@
342
414
  {:else if (!declutter && !marker.edgeAware) || points}
343
415
  <!-- A decluttered or edge-aware label waits for both scales. -->
344
416
  {#each displayData as d, i (i)}
345
- {#if !fitted || fitted.get(d)}
417
+ {#if (!fitted || fitted.get(d)) && placements?.[i] !== null && !covered?.has(i)}
346
418
  {@const lane = lanes?.get(i)}
347
419
  {@const placement = placements?.[i]}
348
420
  {@const extraDy = lane?.extraDy ?? 0}
349
421
  {@const laneDir = lane?.aligned ? lane.dir : dir}
350
422
  {@const labelDx = lane?.aligned ? laneDir * lane.reach : dx}
423
+ {@const origin = originOf(d)}
351
424
  {#if declutter && points && (lane?.aligned || extraDy !== 0)}
352
425
  {@const { px, py } = points[i]}
353
426
  {@const laneX = lane?.aligned ? px + laneDir * lane.laneReach : px + labelDx}
@@ -370,18 +443,18 @@
370
443
  {#if placement}
371
444
  {@render label(
372
445
  rows,
373
- placement.dx,
374
- placement.dy + extraDy,
446
+ placement.dx + origin.dx,
447
+ placement.dy + extraDy + origin.dy,
375
448
  placement.textAnchor,
376
449
  placement.lineAnchor
377
450
  )}
378
451
  {:else}
379
452
  {@render label(
380
453
  rows,
381
- labelDx,
382
- dy + extraDy,
454
+ labelDx + origin.dx,
455
+ dy + extraDy + origin.dy,
383
456
  lane?.aligned && laneDir !== dir ? flipAnchor(anchorAt(d)) : anchorAt(d),
384
- lineAnchor
457
+ lineAnchorAt(d)
385
458
  )}
386
459
  {/if}
387
460
  {/snippet}
@@ -1,5 +1,5 @@
1
1
  import type { TextMarkerConfig } from '../../types/markers/common';
2
- import type { BoxChannels, BoxSizer } from '../utils/BoxLabel';
2
+ import type { BoxChannels, BoxSizer } from '../../types/markers/labels/box';
3
3
  type $$ComponentProps = {
4
4
  marker: TextMarkerConfig;
5
5
  box?: BoxChannels | BoxSizer;
@@ -12,7 +12,7 @@
12
12
  const events = MarkerEvents.use(() => marker);
13
13
 
14
14
  const plot = usePlot();
15
- const isHorizontal = $derived(plot.axes?.isHorizontal ?? false);
15
+ const isHorizontal = $derived(marker.isHorizontal ?? plot.axes?.isHorizontal ?? false);
16
16
  const TickMark = $derived(isHorizontal ? TickX : TickY);
17
17
 
18
18
  const cfg = $derived(getConfiguration());
@@ -19,7 +19,7 @@ export declare class MarkerChannels {
19
19
  * pivot) carry over; the domain is read from the marker's own rows.
20
20
  */
21
21
  static inheritScales(own: ChannelScalesConfig | undefined, plot: ChannelScalesConfig | undefined): ChannelScalesConfig | undefined;
22
- /** `markers`, each `dot` with no scales of its own carrying the plot's ({@link inheritScales}). */
22
+ /** `markers`, each of a kind that inherits them with no scales of its own carrying the plot's ({@link inheritScales}). */
23
23
  static inheritAll<TMarker extends {
24
24
  type: string;
25
25
  scales?: ChannelScalesConfig;
@@ -1,3 +1,4 @@
1
+ import { MarkerKinds } from '../MarkerKinds';
1
2
  import { resolveAccessor } from '../../utils/accessors';
2
3
  import { ColorRamp } from '../../utils/color';
3
4
  import { OpacityScale } from '../../utils/OpacityScale';
@@ -20,9 +21,9 @@ export class MarkerChannels {
20
21
  const numeric = Array.isArray(domain) && domain.every((v) => typeof v === 'number');
21
22
  return { ...plot, color: numeric ? color : plot.color };
22
23
  }
23
- /** `markers`, each `dot` with no scales of its own carrying the plot's ({@link inheritScales}). */
24
+ /** `markers`, each of a kind that inherits them with no scales of its own carrying the plot's ({@link inheritScales}). */
24
25
  static inheritAll(markers, plot) {
25
- return markers.map((marker) => marker.type === 'dot' && !marker.scales
26
+ return markers.map((marker) => MarkerKinds.inheritsScales(marker.type) && !marker.scales
26
27
  ? { ...marker, scales: MarkerChannels.inheritScales(undefined, plot) }
27
28
  : marker);
28
29
  }