@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
@@ -7,6 +7,11 @@ import { isGapValue } from '../../plots/utils/gaps';
7
7
  import { PlotAxes } from '../../phases/data/PlotAxes';
8
8
  import { NumberFormat } from '../../utils/NumberFormat';
9
9
  import { RowContext } from '../../phases/context/RowContext';
10
+ import { PointLayout } from '../../markers/labels/layouts/PointLayout';
11
+ /** Where a part's value sits on the row a part label copies. */
12
+ const PART_VALUE = Symbol('partValue');
13
+ /** The options of `styles.values` that place a label in its layout. */
14
+ const LAYOUT_KEYS = ['anchor', 'edge', 'side', 'offset', 'gap'];
10
15
  /**
11
16
  * A plot's value labels: the author's `styles.values` over the plot kind's
12
17
  * own defaults, how their text measures, where they are expected to sit
@@ -15,11 +20,20 @@ import { RowContext } from '../../phases/context/RowContext';
15
20
  */
16
21
  export class ValueLabels {
17
22
  show;
18
- anchor;
23
+ /** How the labels are laid out on their shapes: the author's `layout`, else the kind's. */
24
+ layout;
25
+ /** The options that place a label in its {@link layout}: the author's over the kind's own for it. */
26
+ placement;
27
+ /** The parts of each row labelled - a simple plot's one `value`, a box's `median`... - each with its own options. */
28
+ parts;
29
+ /** The series whose rows are labelled, when `styles.values.series` names some; every series else. */
30
+ series;
31
+ /** Whether the author gave the labels a text of their own, beyond the value. */
32
+ formatted;
19
33
  /**
20
34
  * A label's text for `value`: the author's `format`, else the kind's, else the value as the display fields ask.
21
35
  * `row` is the row it labels, whose context the format reads; `series` names its series where there is no row
22
- * (a label measured ahead of the rows).
36
+ * (a label measured ahead of the rows); `part`, which of the row's values it writes.
23
37
  */
24
38
  format;
25
39
  hoverIsolate;
@@ -35,15 +49,40 @@ export class ValueLabels {
35
49
  reach;
36
50
  #cfg;
37
51
  constructor(values, cfg, options = {}, reach) {
38
- this.show = values?.show ?? options.show ?? false;
39
- this.anchor = values?.anchor ?? options.anchor ?? 'outside';
52
+ this.layout = values?.layout ?? options.layout ?? 'point';
53
+ const own = Object.fromEntries(LAYOUT_KEYS.flatMap((key) => {
54
+ const value = values?.[key];
55
+ return value === undefined ? [] : [[key, value]];
56
+ }));
40
57
  // The value as the display fields ask (else as it is), which a plot kind's own text builds on.
41
58
  const number = NumberFormat.maybe(NumberFormat.displayOf(values), cfg) ?? ((v) => `${v}`);
42
- const own = values?.format ?? options.format;
43
- this.format = own
44
- ? (v, row, series) => own(v, number, ValueLabels.contextOf(row, series))
45
- : (v) => number(v);
59
+ const writer = (write) => (v, row, series, part) => write
60
+ ? write(v, number, { ...ValueLabels.contextOf(row, series), ...(part ? { part } : {}) })
61
+ : number(v);
62
+ this.format = writer(values?.format ?? options.format);
63
+ this.parts = ValueLabels.#parts(values?.parts, options.parts).map(([name, own_]) => {
64
+ const { format, ...placement } = own_;
65
+ return {
66
+ name,
67
+ placement: {
68
+ ...options.layouts?.[this.layout],
69
+ ...options.parts?.placements[name],
70
+ ...own,
71
+ ...placement
72
+ },
73
+ format: format ? writer(format) : this.format
74
+ };
75
+ });
76
+ this.placement = this.#part('value')?.placement ?? {
77
+ ...options.layouts?.[this.layout],
78
+ ...own
79
+ };
80
+ this.formatted =
81
+ values?.format !== undefined ||
82
+ !!values?.parts?.value?.format;
83
+ this.show = ValueLabels.shows(values, options.show) && this.parts.length > 0;
46
84
  this.hoverIsolate = values?.hoverIsolate ?? false;
85
+ this.series = values?.series === undefined ? undefined : [values.series].flat();
47
86
  this.font = values?.fontStyle;
48
87
  this.fontSize = typeof this.font?.fontSize === 'number' ? this.font.fontSize : 12;
49
88
  this.strokeStyle = values?.strokeStyle;
@@ -52,15 +91,97 @@ export class ValueLabels {
52
91
  this.#cfg = cfg;
53
92
  this.reach = (this.show && reach?.(this)) || [];
54
93
  }
94
+ /** Whether the part named `name` is labelled: a plot writing its `value` on its own shapes asks first. */
95
+ writes(name) {
96
+ return this.#part(name) !== undefined;
97
+ }
98
+ /** The part named `name`, when it is labelled. */
99
+ #part(name) {
100
+ return this.parts.find((part) => part.name === name);
101
+ }
102
+ /** The parts labelled, each with its own options: the author's list or record, else the kind's default ones. */
103
+ static #parts(own, kind) {
104
+ if (own === undefined)
105
+ return (kind?.default ?? ['value']).map((name) => [name, {}]);
106
+ if (Array.isArray(own))
107
+ return own.map((name) => [name, {}]);
108
+ return Object.entries(own).flatMap(([name, option]) => option ? [[name, option === true ? {} : option]] : []);
109
+ }
110
+ /** Whether the labels draw: `show` as set, else when any other option of them is set, else the kind's default. */
111
+ static shows(values, fallback = false) {
112
+ if (values?.show !== undefined)
113
+ return values.show;
114
+ const set = Object.entries(values ?? {}).some(([key, value]) => key !== 'show' && value !== undefined);
115
+ return set || fallback;
116
+ }
55
117
  /** The context a label's `format` reads: its row's, with `series` when the row gives none. */
56
118
  static contextOf(row, series) {
57
119
  const context = RowContext.of(row);
58
120
  return context.series === undefined && series !== undefined ? { ...context, series } : context;
59
121
  }
122
+ /**
123
+ * The value a label writes for `row`, read off its context: its `value`, or one `part` of it on a plot whose rows
124
+ * have several. `undefined` when it is not a number.
125
+ */
126
+ static valueIn(row, part = 'value') {
127
+ const value = RowContext.of(row)[part];
128
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
129
+ }
130
+ /** A label's text for `row`: its context's `part` (its `value` by default), as that part's format writes it. */
131
+ text(row, part = 'value') {
132
+ const value = ValueLabels.valueIn(row, part);
133
+ const format = this.#part(part)?.format ?? this.format;
134
+ return value === undefined ? '' : format(value, row, undefined, part);
135
+ }
60
136
  /** A plot's value labels: `values` over `options`, the kind's own defaults, their reach read off `state`. */
61
137
  static build(values, cfg, options, state) {
62
- const { reach } = options;
63
- return new ValueLabels(values, cfg, options, reach && ((labels) => reach(labels, state)));
138
+ const { reach, layout, layouts, parts, formatFor } = options;
139
+ return new ValueLabels(values, cfg, {
140
+ ...options,
141
+ layout: typeof layout === 'function' ? layout(state) : layout,
142
+ format: options.format ?? formatFor?.(state),
143
+ layouts: typeof layouts === 'function' ? layouts(state) : layouts,
144
+ parts: parts && {
145
+ ...parts,
146
+ placements: typeof parts.placements === 'function' ? parts.placements(state) : parts.placements
147
+ }
148
+ }, reach && ((labels) => reach(labels, state)));
149
+ }
150
+ /** Whether the labels sit past the tip of their shapes, where the plot's margins keep room for them. */
151
+ get pastTip() {
152
+ const { edge = 'tip', side = 'outside' } = this.placement;
153
+ return this.layout === 'path' && edge === 'tip' && side === 'outside';
154
+ }
155
+ /**
156
+ * The `label` a plot kind's shapes write their value in, in this plot's {@link layout}: `text`, over what every
157
+ * label shares ({@link hooks}). Inside its shape a label takes no `fill` of its own, so it is written in the color
158
+ * that reads best on the shape; past it, the plot's `fill`.
159
+ */
160
+ label(env, text, defaults = {}) {
161
+ const { fitMargins, styles: shared, ...events } = this.hooks(env);
162
+ const { font } = this;
163
+ const onShape = this.layout === 'box' || this.placement.side === 'inside';
164
+ const picked = this.layout === 'point' && defaults.labelled && new Set(defaults.labelled);
165
+ return {
166
+ ...events,
167
+ ...this.placement,
168
+ ...(picked ? { select: (row) => picked.has(row) } : {}),
169
+ layout: this.layout,
170
+ text,
171
+ fitMargins: onShape ? undefined : fitMargins,
172
+ styles: {
173
+ ...shared,
174
+ ...font,
175
+ // On its shape, a label unset reads in the color that contrasts with it; off it, in its own.
176
+ fill: onShape
177
+ ? this.fill
178
+ : (this.fill ??
179
+ (this.color === 'series' ? defaults.seriesColor : undefined) ??
180
+ defaults.fill),
181
+ fontSize: font?.fontSize ?? defaults.fontSize ?? 12,
182
+ fontWeight: font?.fontWeight ?? defaults.fontWeight
183
+ }
184
+ };
64
185
  }
65
186
  /** The configuration the labels were built under, for a plot kind's own label defaults. */
66
187
  get config() {
@@ -82,9 +203,9 @@ export class ValueLabels {
82
203
  textBox(text) {
83
204
  return Box.ofText(text, this.fontSize, this.#cfg);
84
205
  }
85
- /** The labels' gap (px) from their mark along `axis`: their own `dx`/`dy`, else `fallback`. */
86
- offset(axis, fallback = 6) {
87
- const own = axis === 'x' ? this.font?.dx : this.font?.dy;
206
+ /** The labels' gap (px) from their mark: the layout's `gap`, else `fallback`. */
207
+ offset(fallback = 6) {
208
+ const own = this.placement.gap;
88
209
  return Math.abs(typeof own === 'number' ? own : fallback);
89
210
  }
90
211
  /**
@@ -129,15 +250,6 @@ export class ValueLabels {
129
250
  return [lowest, highest].flatMap((value) => value === undefined ? [] : [{ series: s, value }]);
130
251
  });
131
252
  }
132
- /**
133
- * The author's own `textAnchor`, if any. `'outside'` is a hover marker's
134
- * per-point anchor; a label's own `anchor` already places it, so here it
135
- * counts as unset.
136
- */
137
- get textAnchor() {
138
- const own = this.font?.textAnchor;
139
- return own === 'outside' ? undefined : own;
140
- }
141
253
  /**
142
254
  * What every label marker shares: the plot's margins grow to fit it, it
143
255
  * dims with its row's series, and with `hoverIsolate`, hovering it
@@ -161,64 +273,6 @@ export class ValueLabels {
161
273
  onpointerleave: isolate ? () => interaction.isolate(null) : undefined
162
274
  };
163
275
  }
164
- /**
165
- * Where in a bar a label goes, from the `anchor` of `styles.values`: its `label` anchor, and whether it is beyond
166
- * the bar. `negative` tells a bar that runs to the left (or down) from its base from one that runs right (or up),
167
- * which `'tip'`, `'base'` and `'outside'` follow; the others are the edges of the page. A `stacked` bar's label is
168
- * always in the middle. An anchor the shapes do not read (`'start'`, `'end'`) is taken for `'outside'`, the default.
169
- */
170
- static boxAnchor(anchor, shape) {
171
- if (shape.stacked || anchor === 'middle')
172
- return { anchor: 'middle' };
173
- // A place in the shape as it is drawn on the page, which no sign moves.
174
- if (anchor !== 'outside' &&
175
- anchor !== 'start' &&
176
- anchor !== 'end' &&
177
- anchor !== 'tip' &&
178
- anchor !== 'base') {
179
- return { anchor };
180
- }
181
- const [tip, base] = shape.isHorizontal
182
- ? shape.negative
183
- ? ['left', 'right']
184
- : ['right', 'left']
185
- : shape.negative
186
- ? ['bottom', 'top']
187
- : ['top', 'bottom'];
188
- if (anchor === 'base')
189
- return { anchor: base };
190
- return anchor === 'tip' ? { anchor: tip } : { anchor: tip, outside: true };
191
- }
192
- /** Whether a label at `anchor` goes to one side or the other of a bar with the sign of its value. */
193
- static followsSign(anchor) {
194
- return anchor === 'outside' || anchor === 'tip' || anchor === 'base';
195
- }
196
- /**
197
- * The `label` the shapes of a plot kind write their value in: this plot's `styles.values` over the `defaults` the kind
198
- * sets, with what every label shares ({@link hooks}) - its dimming and isolating of a series. A label inside its shape
199
- * never reaches out of the plot, so only one `outside` has the margins grow to hold it; and it takes the default
200
- * `fill` only `outside`, for inside it is written in the color that contrasts with its shape.
201
- */
202
- shapeLabel(env, text, defaults = {}) {
203
- const { fitMargins, styles: shared, ...events } = this.hooks(env);
204
- const { font } = this;
205
- return {
206
- ...events,
207
- text,
208
- anchor: defaults.anchor,
209
- outside: defaults.outside,
210
- gap: defaults.gap,
211
- fitMargins: defaults.outside ? fitMargins : undefined,
212
- styles: {
213
- ...shared,
214
- ...font,
215
- // Inside its shape, a label unset is written in the color that reads best on it.
216
- fill: font?.fill ?? (defaults.outside ? defaults.fill : undefined),
217
- fontSize: font?.fontSize ?? defaults.fontSize ?? 12,
218
- fontWeight: font?.fontWeight ?? defaults.fontWeight
219
- }
220
- };
221
- }
222
276
  /**
223
277
  * A processed row's own value, read through its series' own value channel
224
278
  * (`y`, or `x` on a horizontal plot) - what a label shows, even where a
@@ -234,30 +288,147 @@ export class ValueLabels {
234
288
  return (d) => bySeries.get(String(d[SERIES_ID_KEY]))?.(d);
235
289
  }
236
290
  /**
237
- * A series-over-`x` plot's (line, area) labels: one `'text'` marker over
238
- * the processed rows, each visible series' last row with a value
239
- * (`filter` + `selectLast`, per series), at its top (a stacked row's
240
- * `y2`), labelled with its own value and placed by `anchor`. Labels that
241
- * would overlap fan out into a shared column (`declutter`), colored by the
242
- * segment the row falls in. A plot that labels each end of its series apart
243
- * passes the `rows` and `anchor` of one end, and `lane` to route its labels
244
- * out to a lane whether or not they overlap (`flip: false` keeps one on the
245
- * left in the margin instead of flipping it over its points).
291
+ * A series-over-`x` plot's (line, area) labels, for its line or area markers: each labelled row (by default its
292
+ * series' last) with its own value, on the `side` of it its placement asks, colored by the segment it falls in.
293
+ * Labels that would overlap fan out into a shared column. A plot that labels each end of its series apart passes
294
+ * the `rows` and `side` of one end, and `lane` to route them out to a lane whether or not they overlap.
246
295
  */
247
- atSeriesEnds(state, end = {}) {
296
+ atSeriesEnds(state, env, end = {}) {
248
297
  if (end.rows)
249
- return this.#seriesEnds(state, end);
298
+ return this.#seriesEnds(state, env, end);
250
299
  // A label on a series' first point would sit on its own line: it goes out to a lane instead.
251
300
  const firsts = ValueLabels.#firstRows(state);
252
301
  const labelled = state.labelled ?? [];
253
302
  return [
254
- ...this.#seriesEnds(state, { rows: labelled.filter((row) => !firsts.has(row)) }),
255
- ...this.#seriesEnds(state, {
303
+ ...this.#seriesEnds(state, env, { rows: labelled.filter((row) => !firsts.has(row)) }),
304
+ ...this.#seriesEnds(state, env, {
256
305
  rows: labelled.filter((row) => firsts.has(row)),
257
306
  lane: { elbowGap: ValueLabels.FIRST_LANE_GAP }
258
307
  })
259
308
  ];
260
309
  }
310
+ /**
311
+ * The labels of a plot whose rows have several values (a box's quartiles, a bullet's target): one `text` marker per
312
+ * part labelled, each of `rows` written at that part's value in its context - one label per value, for a part
313
+ * holding several (a box's outliers). `at` places a label at a value: its point, and the shape around it its
314
+ * `anchor` reads.
315
+ */
316
+ partMarkers(env, rows, at, defaults = {}) {
317
+ const { styles: shared, ...hooks } = this.hooks(env);
318
+ const { font } = this;
319
+ // `series` labels the rows of those series only.
320
+ const named = this.series && new Set(this.series);
321
+ const labelled = named ? rows.filter((row) => named.has(String(row[SERIES_KEY]))) : rows;
322
+ return this.parts.flatMap((part) => {
323
+ if (defaults.skip?.includes(part.name))
324
+ return [];
325
+ const placed = labelled.flatMap((row) => {
326
+ const own = RowContext.of(row)[part.name];
327
+ return (Array.isArray(own) ? own : [own])
328
+ .filter((value) => typeof value === 'number' && Number.isFinite(value))
329
+ .map((value) => ({ ...row, [PART_VALUE]: value }));
330
+ });
331
+ const label = {
332
+ ...hooks,
333
+ ...part.placement,
334
+ layout: 'point',
335
+ text: (row) => part.format(Number(row[PART_VALUE]), row, undefined, part.name),
336
+ styles: {
337
+ ...shared,
338
+ ...font,
339
+ fill: this.fill ??
340
+ (this.color === 'series' ? defaults.seriesColor : undefined) ??
341
+ 'currentColor',
342
+ fontSize: font?.fontSize ?? 11
343
+ }
344
+ };
345
+ const marker = placed.length > 0 &&
346
+ PointLayout.build(label, at(PART_VALUE), placed, { gap: this.#cfg.text.label.gap });
347
+ return marker ? [marker] : [];
348
+ });
349
+ }
350
+ /**
351
+ * Where a part label of a row sits on a plot of categories: at the row's `category` (a field or a function of
352
+ * it), level with the part's value, across the category's band - which an `anchor` reads, to write it beside.
353
+ */
354
+ static bandPoint(state, category,
355
+ /** How wide the shape is across its band, in pixels, when it does not fill it: a violin's inner box. */
356
+ across,
357
+ /** A row's slot of its band, when its series share it side by side. */
358
+ slot) {
359
+ const valueAxis = PlotAxes.valueOf(state);
360
+ const categoryAxis = PlotAxes.categoryOf(state);
361
+ return (value) => ({
362
+ [categoryAxis]: category,
363
+ [valueAxis]: value,
364
+ shift: slot &&
365
+ ((row, scales) => {
366
+ const own = slot(row);
367
+ const band = Number(scales[categoryAxis]?.fn?.bandwidth?.() ?? 0);
368
+ if (!own || !band)
369
+ return undefined;
370
+ const share = band / own.count;
371
+ const offset = share * own.index + share / 2 - band / 2;
372
+ return categoryAxis === 'x' ? { dx: offset, dy: 0 } : { dx: 0, dy: offset };
373
+ }),
374
+ size: (_row, scales) => {
375
+ const band = across ?? Number(scales[categoryAxis]?.fn?.bandwidth?.() ?? 0);
376
+ return categoryAxis === 'x' ? { width: band, height: 0 } : { width: 0, height: band };
377
+ }
378
+ });
379
+ }
380
+ /** The placement that writes each of `parts` beside its category's band: to its right, or above sideways bands. */
381
+ static besideBand(state, parts) {
382
+ const beside = { anchor: state.axes?.isHorizontal ? 'top' : 'right' };
383
+ return Object.fromEntries(parts.map((part) => [part, beside]));
384
+ }
385
+ /** A series-over-`x` plot's labels in its {@link layout}: at its series' ends, or along its lines. */
386
+ forSeries(state, env) {
387
+ return this.layout === 'path' ? this.alongSeries(state, env) : this.atSeriesEnds(state, env);
388
+ }
389
+ /** The side of its point a `'point'` label is written on: its placement's, else after it. */
390
+ get side() {
391
+ return this.placement.side ?? 'after';
392
+ }
393
+ /**
394
+ * A series-over-`x` plot's `'path'` labels, for its line or area markers: each series' name - or, with a `format`,
395
+ * the text it writes for the series' first row - along its line, colored by the series.
396
+ */
397
+ alongSeries(state, env) {
398
+ const { font } = this;
399
+ const nameOf = (d) => String(d[SERIES_KEY]);
400
+ const { styles: shared, ...hooks } = this.hooks(env);
401
+ return [
402
+ {
403
+ ...hooks,
404
+ ...this.placement,
405
+ layout: 'path',
406
+ text: (d) => (this.formatted ? this.text(d) : nameOf(d)),
407
+ styles: {
408
+ ...shared,
409
+ ...font,
410
+ fill: this.fill ?? ((d) => this.#colorOf(state, d) ?? 'currentColor'),
411
+ fontSize: font?.fontSize ?? 12
412
+ }
413
+ }
414
+ ];
415
+ }
416
+ /** A series row's color: the segment it falls in, else its series'. */
417
+ #colorOf(state, d) {
418
+ const id = String(d[SERIES_ID_KEY]);
419
+ const group = state.scopedGroups?.find((g) => (g.id ?? g.name) === id);
420
+ const segment = group?.segments.find((seg) => seg.match(d));
421
+ return (segment?.color ??
422
+ group?.segments[0]?.color ??
423
+ group?.color ??
424
+ state.colorForSeries?.(String(d[SERIES_KEY])));
425
+ }
426
+ /** `markers` with `labels` on each one of `type`: the shapes of a series carry its labels. */
427
+ static onMarkers(markers, type, labels) {
428
+ if (!labels?.length)
429
+ return markers;
430
+ return markers.map((marker) => (marker.type === type ? { ...marker, label: labels } : marker));
431
+ }
261
432
  /** How far (px) a lane takes the label of a series' first point from it - clear of the line that leaves it. */
262
433
  static FIRST_LANE_GAP = 28;
263
434
  /** The first row of every series that has more than one. */
@@ -272,50 +443,39 @@ export class ValueLabels {
272
443
  }
273
444
  return new Set([...first].filter(([id]) => (counts.get(id) ?? 0) > 1).map(([, row]) => row));
274
445
  }
275
- #seriesEnds(state, end) {
276
- const processed = state.processedData;
277
- if (!processed)
446
+ #seriesEnds(state, env, end) {
447
+ const rows = new Set(end.rows ?? state.labelled ?? []);
448
+ if (rows.size === 0)
278
449
  return [];
279
- const { font, format } = this;
280
- const anchor = end.anchor ?? this.anchor;
281
- const position = ProcessedPositions.of(processed);
282
- const topOf = (d) => position.y(d);
283
- const nameOf = (d) => String(d[SERIES_KEY]);
284
- const valueOf = ValueLabels.valueOf(state);
285
- const colorOf = (d) => {
286
- const id = String(d[SERIES_ID_KEY]);
287
- const group = state.scopedGroups?.find((g) => (g.id ?? g.name) === id);
288
- const segment = group?.segments.find((seg) => seg.match(d));
289
- return (segment?.color ??
290
- group?.segments[0]?.color ??
291
- group?.color ??
292
- state.colorForSeries?.(nameOf(d)));
293
- };
294
- const marker = {
295
- type: 'text',
296
- // The rows the "transforms" phase picked for the labels: by default each series' last.
297
- data: end.rows ?? state.labelled ?? [],
298
- x: (processed.channels.x ?? position.x),
299
- y: (processed.channels.y2 ?? processed.channels.y ?? topOf),
300
- grouping: SERIES_ID_KEY,
301
- text: (d) => format(Number(valueOf(d)), d, nameOf(d)),
302
- styles: {
303
- ...font,
304
- fill: this.fill ?? ((d) => colorOf(d) ?? 'currentColor'),
305
- fontSize: font?.fontSize ?? 12,
306
- dx: font?.dx ?? (anchor === 'start' ? 6 : anchor === 'end' ? -6 : 0),
307
- dy: font?.dy ?? (anchor === 'outside' ? -8 : 0),
308
- textAnchor: this.textAnchor ?? (anchor === 'start' || anchor === 'end' ? anchor : 'middle'),
309
- lineAnchor: font?.lineAnchor ?? (anchor === 'outside' ? 'bottom' : 'middle')
310
- },
311
- declutter: {
312
- connector: this.strokeStyle,
313
- aligned: !!end.lane && (anchor === 'start' || anchor === 'end'),
314
- elbowGap: typeof end.lane === 'object' ? end.lane.elbowGap : undefined,
315
- flip: end.flip
450
+ const { font } = this;
451
+ const side = end.side ?? this.side;
452
+ const at = PointLayout.sideOf(side, this.placement.gap);
453
+ const { styles: shared, ...hooks } = this.hooks(env);
454
+ return [
455
+ {
456
+ ...hooks,
457
+ layout: 'point',
458
+ select: (row) => rows.has(row),
459
+ grouping: SERIES_ID_KEY,
460
+ text: (d) => this.text(d),
461
+ styles: {
462
+ ...shared,
463
+ ...font,
464
+ fill: this.fill ?? ((d) => this.#colorOf(state, d) ?? 'currentColor'),
465
+ fontSize: font?.fontSize ?? 12,
466
+ dx: at.dx,
467
+ dy: at.dy,
468
+ textAnchor: at.textAnchor,
469
+ lineAnchor: at.lineAnchor
470
+ },
471
+ declutter: {
472
+ connector: this.strokeStyle,
473
+ aligned: !!end.lane && (side === 'after' || side === 'before'),
474
+ elbowGap: typeof end.lane === 'object' ? end.lane.elbowGap : undefined,
475
+ flip: end.flip
476
+ }
316
477
  }
317
- };
318
- return marker.data?.length ? [marker] : [];
478
+ ];
319
479
  }
320
480
  /** The rows of a series-over-`x` plot that cannot be labelled: a gap, or a stack with no top. For the "transforms" phase's `labels.skip`. */
321
481
  static skipGaps(state) {
@@ -331,13 +491,11 @@ export class ValueLabels {
331
491
  * domain when the scales set one, so it holds across a timeline; else off
332
492
  * every value.
333
493
  */
334
- reachAtSeriesEnds(state, anchor = this.anchor) {
494
+ reachAtSeriesEnds(state, pointSide = this.side) {
335
495
  const series = state.resolvedSeries ?? [];
336
- const { font, format } = this;
337
- const dx = typeof font?.dx === 'number' ? font.dx : anchor === 'start' ? 6 : anchor === 'end' ? -6 : 0;
338
- const dy = typeof font?.dy === 'number' ? font.dy : anchor === 'outside' ? -8 : 0;
339
- const textAnchor = this.textAnchor ?? (anchor === 'start' ? 'start' : anchor === 'end' ? 'end' : 'middle');
340
- const lineAnchor = font?.lineAnchor ?? (anchor === 'outside' ? 'bottom' : 'middle');
496
+ const { format } = this;
497
+ const written = PointLayout.sideOf(pointSide, this.placement.gap);
498
+ const { dx, dy, textAnchor, lineAnchor } = written;
341
499
  const side = {
342
500
  x: textAnchor === 'start' ? 'after' : textAnchor === 'end' ? 'before' : 'middle',
343
501
  y: lineAnchor === 'top' ? 'after' : lineAnchor === 'bottom' ? 'before' : 'middle'