@opendata-ai/openchart-engine 8.0.0-rc.12 → 8.0.0-rc.3

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 (271) hide show
  1. package/dist/index.d.ts +0 -7
  2. package/dist/index.js +161 -579
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -3
  5. package/src/__test-fixtures__/specs.ts +182 -0
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +2339 -0
  7. package/src/__tests__/annotation-behavior.test.ts +721 -0
  8. package/src/__tests__/axes.test.ts +1579 -0
  9. package/src/__tests__/compile-animation.test.ts +161 -0
  10. package/src/__tests__/compile-chart.test.ts +1303 -0
  11. package/src/__tests__/compile-footnotes.test.ts +277 -0
  12. package/src/__tests__/compile-layer.test.ts +969 -0
  13. package/src/__tests__/compile-snapshot.test.ts +156 -0
  14. package/src/__tests__/compound-labels.test.ts +147 -0
  15. package/src/__tests__/dimensions.test.ts +918 -0
  16. package/src/__tests__/encoding-sugar.test.ts +390 -0
  17. package/src/__tests__/facet.test.ts +215 -0
  18. package/src/__tests__/helpers/invariants.ts +299 -0
  19. package/src/__tests__/known-bugs.test.ts +566 -0
  20. package/src/__tests__/layout-fuzz.test.ts +358 -0
  21. package/src/__tests__/layout-invariants.test.ts +535 -0
  22. package/src/__tests__/legend.test.ts +721 -0
  23. package/src/__tests__/mark-keys.test.ts +294 -0
  24. package/src/__tests__/ranking-recipes.test.ts +260 -0
  25. package/src/__tests__/rotation.test.ts +83 -0
  26. package/src/__tests__/scales.test.ts +323 -0
  27. package/src/__tests__/series-search.test.ts +92 -0
  28. package/src/__tests__/spec-sugar.test.ts +801 -0
  29. package/src/__tests__/you-draw-it.test.ts +160 -0
  30. package/src/annotations/__tests__/compute.test.ts +1672 -0
  31. package/src/annotations/__tests__/geometry.test.ts +66 -0
  32. package/src/annotations/__tests__/placement-fuzz.test.ts +256 -0
  33. package/src/annotations/__tests__/placement-invariants.test.ts +345 -0
  34. package/src/annotations/__tests__/placement.test.ts +316 -0
  35. package/src/annotations/__tests__/resolve-text.test.ts +788 -0
  36. package/src/annotations/__tests__/rich-text.test.ts +91 -0
  37. package/src/annotations/__tests__/thinning.test.ts +285 -0
  38. package/src/annotations/collisions.ts +326 -0
  39. package/src/annotations/compute.ts +333 -0
  40. package/src/annotations/constants.ts +162 -0
  41. package/src/annotations/geometry.ts +370 -0
  42. package/src/annotations/move.ts +135 -0
  43. package/src/annotations/placement.ts +636 -0
  44. package/src/annotations/position.ts +131 -0
  45. package/src/annotations/resolve-range.ts +117 -0
  46. package/src/annotations/resolve-refline.ts +157 -0
  47. package/src/annotations/resolve-text.ts +539 -0
  48. package/src/annotations/rich-text.ts +65 -0
  49. package/src/annotations/thinning.ts +190 -0
  50. package/src/barlist/__tests__/compile-barlist.test.ts +200 -0
  51. package/src/barlist/compile-barlist.ts +391 -0
  52. package/src/barlist/types.ts +28 -0
  53. package/src/charts/__tests__/post-process.test.ts +237 -0
  54. package/src/charts/__tests__/registry.test.ts +116 -0
  55. package/src/charts/__tests__/utils.test.ts +195 -0
  56. package/src/charts/_shared/__tests__/density-filter.test.ts +32 -0
  57. package/src/charts/_shared/density-filter.ts +26 -0
  58. package/src/charts/_shared/format-label-value.ts +16 -0
  59. package/src/charts/bar/__tests__/compute.test.ts +934 -0
  60. package/src/charts/bar/__tests__/gradient-orient.test.ts +127 -0
  61. package/src/charts/bar/__tests__/labels.test.ts +292 -0
  62. package/src/charts/bar/compute.ts +539 -0
  63. package/src/charts/bar/index.ts +60 -0
  64. package/src/charts/bar/labels.ts +311 -0
  65. package/src/charts/beeswarm/__tests__/compute.test.ts +280 -0
  66. package/src/charts/beeswarm/__tests__/dodge.test.ts +148 -0
  67. package/src/charts/beeswarm/compute.ts +203 -0
  68. package/src/charts/beeswarm/dodge.ts +155 -0
  69. package/src/charts/beeswarm/index.ts +32 -0
  70. package/src/charts/builtin.ts +75 -0
  71. package/src/charts/calendar/__tests__/compute.test.ts +417 -0
  72. package/src/charts/calendar/compute.ts +319 -0
  73. package/src/charts/calendar/index.ts +32 -0
  74. package/src/charts/column/__tests__/compute.test.ts +540 -0
  75. package/src/charts/column/__tests__/labels.test.ts +179 -0
  76. package/src/charts/column/compute.ts +493 -0
  77. package/src/charts/column/index.ts +53 -0
  78. package/src/charts/column/labels.ts +229 -0
  79. package/src/charts/dot/__tests__/compute.test.ts +380 -0
  80. package/src/charts/dot/__tests__/labels.test.ts +98 -0
  81. package/src/charts/dot/compute.ts +285 -0
  82. package/src/charts/dot/index.ts +62 -0
  83. package/src/charts/dot/labels.ts +121 -0
  84. package/src/charts/line/__tests__/compute.test.ts +1194 -0
  85. package/src/charts/line/__tests__/curves.test.ts +51 -0
  86. package/src/charts/line/__tests__/labels.test.ts +94 -0
  87. package/src/charts/line/__tests__/mark-options.test.ts +175 -0
  88. package/src/charts/line/area.ts +484 -0
  89. package/src/charts/line/compute.ts +356 -0
  90. package/src/charts/line/curves.ts +85 -0
  91. package/src/charts/line/index.ts +176 -0
  92. package/src/charts/line/labels.ts +177 -0
  93. package/src/charts/parliament/__tests__/compute.test.ts +411 -0
  94. package/src/charts/parliament/compute.ts +313 -0
  95. package/src/charts/parliament/index.ts +32 -0
  96. package/src/charts/pie/__tests__/compute.test.ts +384 -0
  97. package/src/charts/pie/__tests__/labels.test.ts +132 -0
  98. package/src/charts/pie/compute.ts +324 -0
  99. package/src/charts/pie/index.ts +49 -0
  100. package/src/charts/pie/labels.ts +143 -0
  101. package/src/charts/post-process.ts +229 -0
  102. package/src/charts/range/__tests__/compute.test.ts +486 -0
  103. package/src/charts/range/compute.ts +273 -0
  104. package/src/charts/range/index.ts +82 -0
  105. package/src/charts/range/labels.ts +169 -0
  106. package/src/charts/rect/__tests__/compute.test.ts +120 -0
  107. package/src/charts/rect/index.ts +127 -0
  108. package/src/charts/registry.ts +72 -0
  109. package/src/charts/rule/index.ts +139 -0
  110. package/src/charts/scatter/__tests__/compute.test.ts +488 -0
  111. package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
  112. package/src/charts/scatter/compute.ts +192 -0
  113. package/src/charts/scatter/index.ts +46 -0
  114. package/src/charts/scatter/trendline.ts +100 -0
  115. package/src/charts/text/__tests__/compute.test.ts +307 -0
  116. package/src/charts/text/index.ts +141 -0
  117. package/src/charts/tick/__tests__/compute.test.ts +75 -0
  118. package/src/charts/tick/index.ts +99 -0
  119. package/src/charts/utils.ts +170 -0
  120. package/src/charts/waffle/__tests__/compute.test.ts +305 -0
  121. package/src/charts/waffle/compute.ts +197 -0
  122. package/src/charts/waffle/index.ts +31 -0
  123. package/src/compile/__tests__/color-scale-range.test.ts +158 -0
  124. package/src/compile/__tests__/color-scale-reverse.test.ts +89 -0
  125. package/src/compile/__tests__/data-clip.test.ts +127 -0
  126. package/src/compile/__tests__/fill-patterns.test.ts +154 -0
  127. package/src/compile/__tests__/size-scale.test.ts +108 -0
  128. package/src/compile/__tests__/watermark-obstacle.test.ts +93 -0
  129. package/src/compile/color-scale-range.ts +101 -0
  130. package/src/compile/data-clip.ts +48 -0
  131. package/src/compile/fill-patterns.ts +74 -0
  132. package/src/compile/layer.ts +626 -0
  133. package/src/compile/size-scale.ts +141 -0
  134. package/src/compile/spec-sugar.ts +758 -0
  135. package/src/compile/watermark-obstacle.ts +54 -0
  136. package/src/compile/you-draw-it.ts +123 -0
  137. package/src/compile.ts +1450 -0
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +148 -0
  139. package/src/compiler/__tests__/animation.test.ts +177 -0
  140. package/src/compiler/__tests__/compile.test.ts +87 -0
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +19 -0
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +20 -0
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +22 -0
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +19 -0
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +14 -0
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +34 -0
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +22 -0
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +41 -0
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +38 -0
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +28 -0
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +28 -0
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +30 -0
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +29 -0
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +32 -0
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +25 -0
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +19 -0
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +19 -0
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +21 -0
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +23 -0
  160. package/src/compiler/__tests__/keys.test.ts +83 -0
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +103 -0
  162. package/src/compiler/__tests__/normalize.test.ts +399 -0
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +153 -0
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +86 -0
  165. package/src/compiler/__tests__/validate-highlight.test.ts +112 -0
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +89 -0
  167. package/src/compiler/__tests__/validate.test.ts +629 -0
  168. package/src/compiler/__tests__/vl-parity.test.ts +68 -0
  169. package/src/compiler/a11y-warnings.ts +110 -0
  170. package/src/compiler/animation.ts +142 -0
  171. package/src/compiler/index.ts +47 -0
  172. package/src/compiler/keys.ts +55 -0
  173. package/src/compiler/normalize.ts +734 -0
  174. package/src/compiler/sparkline-defaults.ts +138 -0
  175. package/src/compiler/types.ts +241 -0
  176. package/src/compiler/validate.ts +1871 -0
  177. package/src/endpoint-labels/__tests__/compute.test.ts +587 -0
  178. package/src/endpoint-labels/compute.ts +621 -0
  179. package/src/endpoint-labels/constants.ts +54 -0
  180. package/src/endpoint-labels/format.ts +60 -0
  181. package/src/endpoint-labels/predict.ts +139 -0
  182. package/src/format/__tests__/field-format.test.ts +164 -0
  183. package/src/format/field-format.ts +27 -0
  184. package/src/geo/__tests__/compile-map.test.ts +832 -0
  185. package/src/geo/compile-map.ts +1243 -0
  186. package/src/geo/join.ts +115 -0
  187. package/src/geo/projections.ts +45 -0
  188. package/src/geo/types.ts +26 -0
  189. package/src/graphs/__tests__/community.test.ts +228 -0
  190. package/src/graphs/__tests__/compile-graph.test.ts +396 -0
  191. package/src/graphs/__tests__/encoding.test.ts +415 -0
  192. package/src/graphs/community.ts +92 -0
  193. package/src/graphs/compile-graph.ts +329 -0
  194. package/src/graphs/encoding.ts +356 -0
  195. package/src/graphs/types.ts +106 -0
  196. package/src/index.ts +177 -0
  197. package/src/layout/__tests__/metrics.test.ts +97 -0
  198. package/src/layout/__tests__/scales-highlight.test.ts +192 -0
  199. package/src/layout/axes/rotation.ts +152 -0
  200. package/src/layout/axes/thinning.ts +98 -0
  201. package/src/layout/axes/ticks.ts +409 -0
  202. package/src/layout/axes.ts +757 -0
  203. package/src/layout/dimensions.ts +912 -0
  204. package/src/layout/facet.ts +91 -0
  205. package/src/layout/gridlines.ts +84 -0
  206. package/src/layout/metrics.ts +132 -0
  207. package/src/layout/plan.ts +631 -0
  208. package/src/layout/scales.ts +1053 -0
  209. package/src/layout/shared.ts +44 -0
  210. package/src/legend/__tests__/continuous.test.ts +318 -0
  211. package/src/legend/__tests__/redundancy.test.ts +108 -0
  212. package/src/legend/__tests__/size.test.ts +348 -0
  213. package/src/legend/__tests__/suppression.test.ts +382 -0
  214. package/src/legend/compute.ts +747 -0
  215. package/src/legend/continuous.ts +327 -0
  216. package/src/legend/size.ts +407 -0
  217. package/src/legend/suppression.ts +226 -0
  218. package/src/legend/wrap.ts +117 -0
  219. package/src/sankey/__tests__/compile-sankey.test.ts +458 -0
  220. package/src/sankey/__tests__/layout.test.ts +165 -0
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +114 -0
  222. package/src/sankey/__tests__/node-sort.test.ts +45 -0
  223. package/src/sankey/compile-sankey.ts +706 -0
  224. package/src/sankey/layout.ts +184 -0
  225. package/src/sankey/types.ts +41 -0
  226. package/src/tables/__tests__/bar-column.test.ts +147 -0
  227. package/src/tables/__tests__/category-colors.test.ts +230 -0
  228. package/src/tables/__tests__/compile-table.test.ts +236 -0
  229. package/src/tables/__tests__/format-cells.test.ts +143 -0
  230. package/src/tables/__tests__/heatmap.test.ts +148 -0
  231. package/src/tables/__tests__/pagination.test.ts +78 -0
  232. package/src/tables/__tests__/search.test.ts +94 -0
  233. package/src/tables/__tests__/sort.test.ts +107 -0
  234. package/src/tables/__tests__/sparkline.test.ts +122 -0
  235. package/src/tables/bar-column.ts +94 -0
  236. package/src/tables/category-colors.ts +78 -0
  237. package/src/tables/compile-table.ts +427 -0
  238. package/src/tables/format-cells.ts +115 -0
  239. package/src/tables/heatmap.ts +141 -0
  240. package/src/tables/pagination.ts +46 -0
  241. package/src/tables/search.ts +66 -0
  242. package/src/tables/sort.ts +69 -0
  243. package/src/tables/sparkline.ts +113 -0
  244. package/src/tables/utils.ts +16 -0
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +576 -0
  246. package/src/tilemap/compile-tilemap.ts +755 -0
  247. package/src/tilemap/layout.ts +172 -0
  248. package/src/tilemap/types.ts +33 -0
  249. package/src/tooltips/__tests__/compute.test.ts +582 -0
  250. package/src/tooltips/compute.ts +700 -0
  251. package/src/transforms/__tests__/aggregate.test.ts +159 -0
  252. package/src/transforms/__tests__/bin.test.ts +88 -0
  253. package/src/transforms/__tests__/calculate.test.ts +146 -0
  254. package/src/transforms/__tests__/conditional.test.ts +127 -0
  255. package/src/transforms/__tests__/filter-relative.test.ts +202 -0
  256. package/src/transforms/__tests__/filter.test.ts +59 -0
  257. package/src/transforms/__tests__/fold.test.ts +79 -0
  258. package/src/transforms/__tests__/index.test.ts +93 -0
  259. package/src/transforms/__tests__/predicates.test.ts +193 -0
  260. package/src/transforms/__tests__/timeunit.test.ts +129 -0
  261. package/src/transforms/__tests__/window.test.ts +286 -0
  262. package/src/transforms/aggregate.ts +131 -0
  263. package/src/transforms/bin.ts +87 -0
  264. package/src/transforms/calculate.ts +60 -0
  265. package/src/transforms/conditional.ts +46 -0
  266. package/src/transforms/filter.ts +122 -0
  267. package/src/transforms/fold.ts +49 -0
  268. package/src/transforms/index.ts +60 -0
  269. package/src/transforms/predicates.ts +123 -0
  270. package/src/transforms/timeunit.ts +88 -0
  271. package/src/transforms/window.ts +185 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Data-coordinate to pixel-coordinate resolution for annotations.
3
+ */
4
+
5
+ import type { ScaleBand, ScaleLinear, ScalePoint, ScaleTime } from 'd3-scale';
6
+ import type { ResolvedScales } from '../layout/scales';
7
+
8
+ /**
9
+ * Interpolate a numeric value between sorted domain entries.
10
+ * Used when an annotation references a value not present in a categorical domain
11
+ * (e.g. "2008" on an axis with data points at "2007" and "2009").
12
+ * Returns null if domain values aren't numeric or the domain is too small.
13
+ */
14
+ export function interpolateInDomain(
15
+ numValue: number,
16
+ domain: string[],
17
+ positionOf: (entry: string) => number,
18
+ ): number | null {
19
+ if (domain.length < 2) return null;
20
+ const nums = domain.map(Number);
21
+ if (!nums.every(Number.isFinite)) return null;
22
+
23
+ // Sort by numeric value so bracket-finding works regardless of data order
24
+ const sorted = nums.map((n, i) => ({ n, i })).sort((a, b) => a.n - b.n);
25
+
26
+ // Find the two sorted neighbors that bracket this value
27
+ let lower = 0;
28
+ let upper = sorted.length - 1;
29
+ for (let i = 0; i < sorted.length; i++) {
30
+ if (sorted[i].n <= numValue) lower = i;
31
+ if (sorted[i].n >= numValue) {
32
+ upper = i;
33
+ break;
34
+ }
35
+ }
36
+
37
+ const lowerPos = positionOf(domain[sorted[lower].i]);
38
+ const upperPos = positionOf(domain[sorted[upper].i]);
39
+ if (lower === upper) return lowerPos;
40
+ const t = (numValue - sorted[lower].n) / (sorted[upper].n - sorted[lower].n);
41
+ return lowerPos + t * (upperPos - lowerPos);
42
+ }
43
+
44
+ /** Resolve a data value to a pixel position on a given axis. */
45
+ export function resolvePosition(
46
+ value: string | number,
47
+ scale: ResolvedScales['x'] | ResolvedScales['y'],
48
+ ): number | null {
49
+ if (!scale) return null;
50
+
51
+ const s = scale.scale;
52
+ const type = scale.type;
53
+
54
+ if (type === 'time') {
55
+ const date = new Date(String(value));
56
+ if (Number.isNaN(date.getTime())) return null;
57
+ return (s as ScaleTime<number, number>)(date);
58
+ }
59
+
60
+ if (type === 'linear' || type === 'log') {
61
+ const num = typeof value === 'number' ? value : Number(value);
62
+ if (!Number.isFinite(num)) return null;
63
+ return (s as ScaleLinear<number, number>)(num);
64
+ }
65
+
66
+ if (type === 'band') {
67
+ const bandScale = s as ScaleBand<string>;
68
+ const strValue = String(value);
69
+ const pos = bandScale(strValue);
70
+ if (pos !== undefined) return pos + (bandScale.bandwidth?.() ?? 0) / 2;
71
+
72
+ const bw = bandScale.bandwidth?.() ?? 0;
73
+ return interpolateInDomain(
74
+ Number(strValue),
75
+ bandScale.domain(),
76
+ (entry) => (bandScale(entry) ?? 0) + bw / 2,
77
+ );
78
+ }
79
+
80
+ // point or ordinal: try direct lookup, fall back to interpolation
81
+ const strValue = String(value);
82
+ const directResult = (s as (v: string) => number | undefined)(strValue);
83
+ if (directResult !== undefined) return directResult;
84
+
85
+ if (type === 'point' || type === 'ordinal') {
86
+ const domain = (s as { domain(): string[] }).domain();
87
+ return interpolateInDomain(
88
+ Number(strValue),
89
+ domain,
90
+ (entry) => (s as (v: string) => number)(entry) ?? 0,
91
+ );
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Like resolvePosition, but extends the result to the visual edge of the column
99
+ * for band and point scales. Used by range annotations to span complete columns.
100
+ *
101
+ * For point scales: shifts by ±step/2 using scalePoint.step()
102
+ * For band scales: shifts from center (what resolvePosition returns) to the band edge
103
+ * For all other scales: identical to resolvePosition
104
+ */
105
+ export function resolvePositionEdge(
106
+ value: string | number,
107
+ scale: ResolvedScales['x'] | ResolvedScales['y'],
108
+ edge: 'start' | 'end',
109
+ ): number | null {
110
+ const center = resolvePosition(value, scale);
111
+ if (center === null || !scale) return null;
112
+
113
+ const type = scale.type;
114
+
115
+ if (type === 'point') {
116
+ const s = scale.scale as ScalePoint<string>;
117
+ const halfStep = (s.step?.() ?? 0) / 2;
118
+ return edge === 'start' ? center - halfStep : center + halfStep;
119
+ }
120
+
121
+ if (type === 'band') {
122
+ // center = bandStart + bandwidth/2
123
+ // edge 'start' => bandStart = center - bandwidth/2
124
+ // edge 'end' => bandEnd = center + bandwidth/2
125
+ const s = scale.scale as ScaleBand<string>;
126
+ const halfBw = (s.bandwidth?.() ?? 0) / 2;
127
+ return edge === 'start' ? center - halfBw : center + halfBw;
128
+ }
129
+
130
+ return center;
131
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Range annotation resolver: creates a highlighted rectangular band
3
+ * between two data values on x or y axis.
4
+ */
5
+
6
+ import type {
7
+ RangeAnnotation,
8
+ Rect,
9
+ ResolvedAnnotation,
10
+ ResolvedLabel,
11
+ } from '@opendata-ai/openchart-core';
12
+ import type { ResolvedScales } from '../layout/scales';
13
+ import { DEFAULT_RANGE_FILL, DEFAULT_RANGE_OPACITY } from './constants';
14
+ import { applyOffset } from './geometry';
15
+ import { resolvePosition, resolvePositionEdge } from './position';
16
+ import { makeAnnotationLabelStyle } from './resolve-text';
17
+
18
+ export function resolveRangeAnnotation(
19
+ annotation: RangeAnnotation,
20
+ scales: ResolvedScales,
21
+ chartArea: Rect,
22
+ isDark: boolean,
23
+ fontFamily?: string,
24
+ ): ResolvedAnnotation | null {
25
+ let x = chartArea.x;
26
+ let y = chartArea.y;
27
+ let width = chartArea.width;
28
+ let height = chartArea.height;
29
+
30
+ // When extendToEdges is false, anchor at the data point's exact position
31
+ // (band/point center) instead of extending to the band/step edge. This insets
32
+ // the range from the axis so it starts at the first data point rather than
33
+ // flush against the axis guide. No effect on linear/time scales.
34
+ const extend = annotation.extendToEdges !== false;
35
+ const resolveEdge = (
36
+ value: string | number,
37
+ scale: typeof scales.x,
38
+ edge: 'start' | 'end',
39
+ ): number | null =>
40
+ extend ? resolvePositionEdge(value, scale, edge) : resolvePosition(value, scale);
41
+
42
+ // X-range (vertical band)
43
+ if (annotation.x1 !== undefined && annotation.x2 !== undefined) {
44
+ const x1px = resolveEdge(annotation.x1, scales.x, 'start');
45
+ const x2px = resolveEdge(annotation.x2, scales.x, 'end');
46
+ if (x1px === null || x2px === null) return null;
47
+
48
+ x = Math.min(x1px, x2px);
49
+ width = Math.abs(x2px - x1px);
50
+ }
51
+
52
+ // Y-range (horizontal band)
53
+ if (annotation.y1 !== undefined && annotation.y2 !== undefined) {
54
+ const y1px = resolveEdge(annotation.y1, scales.y, 'end');
55
+ const y2px = resolveEdge(annotation.y2, scales.y, 'start');
56
+ if (y1px === null || y2px === null) return null;
57
+
58
+ y = Math.min(y1px, y2px);
59
+ height = Math.abs(y2px - y1px);
60
+ }
61
+
62
+ const rect: Rect = { x, y, width, height };
63
+
64
+ // Label positioned within the range, with optional offset.
65
+ // labelAnchor controls horizontal placement:
66
+ // "top" (default): horizontally centered, text-anchor middle
67
+ // "left": left edge, text-anchor start
68
+ // "right": right edge, text-anchor end
69
+ // "bottom"/"auto": horizontally centered, text-anchor middle
70
+ let label: ResolvedLabel | undefined;
71
+ if (annotation.label) {
72
+ const anchor = annotation.labelAnchor ?? 'top';
73
+ const centered = anchor === 'top' || anchor === 'bottom' || anchor === 'auto';
74
+ const baseDx = centered ? 0 : anchor === 'right' ? -4 : 4;
75
+ const baseDy = 14;
76
+ const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
77
+
78
+ const style = makeAnnotationLabelStyle(
79
+ annotation.fontSize ?? 11,
80
+ annotation.fontWeight ?? 500,
81
+ undefined,
82
+ isDark,
83
+ fontFamily,
84
+ );
85
+ if (centered) {
86
+ style.textAnchor = 'middle';
87
+ } else if (anchor === 'right') {
88
+ style.textAnchor = 'end';
89
+ }
90
+
91
+ // Position label horizontally centered within the range band by default.
92
+ // For left/right anchors, position at the respective edge.
93
+ const baseX = centered ? x + width / 2 : anchor === 'right' ? x + width : x;
94
+
95
+ label = {
96
+ text: annotation.label,
97
+ x: baseX + labelDelta.dx,
98
+ y: y + labelDelta.dy,
99
+ style,
100
+ visible: true,
101
+ };
102
+ }
103
+
104
+ // In dark mode, boost range opacity slightly for better visibility
105
+ const defaultOpacity = isDark ? 0.2 : DEFAULT_RANGE_OPACITY;
106
+
107
+ return {
108
+ type: 'range',
109
+ id: annotation.id,
110
+ rect,
111
+ label,
112
+ fill: annotation.fill ?? DEFAULT_RANGE_FILL,
113
+ opacity: annotation.opacity ?? defaultOpacity,
114
+ stroke: annotation.stroke,
115
+ zIndex: annotation.zIndex,
116
+ };
117
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Reference line annotation resolver: creates a horizontal or vertical
3
+ * reference line at a data value with optional label.
4
+ */
5
+
6
+ import type {
7
+ Point,
8
+ Rect,
9
+ RefLineAnnotation,
10
+ ResolvedAnnotation,
11
+ ResolvedLabel,
12
+ } from '@opendata-ai/openchart-core';
13
+ import type { ResolvedScales } from '../layout/scales';
14
+ import { DARK_REFLINE_STROKE, DEFAULT_REFLINE_DASH, LIGHT_REFLINE_STROKE } from './constants';
15
+ import { applyOffset } from './geometry';
16
+ import { resolvePosition } from './position';
17
+ import { makeAnnotationLabelStyle } from './resolve-text';
18
+
19
+ export function resolveRefLineAnnotation(
20
+ annotation: RefLineAnnotation,
21
+ scales: ResolvedScales,
22
+ chartArea: Rect,
23
+ isDark: boolean,
24
+ fontFamily?: string,
25
+ ): ResolvedAnnotation | null {
26
+ let start: Point;
27
+ let end: Point;
28
+
29
+ if (annotation.y !== undefined) {
30
+ // Horizontal reference line
31
+ const yPx = resolvePosition(annotation.y, scales.y);
32
+ if (yPx === null) return null;
33
+
34
+ start = { x: chartArea.x, y: yPx };
35
+ end = { x: chartArea.x + chartArea.width, y: yPx };
36
+ } else if (annotation.x !== undefined) {
37
+ // Vertical reference line
38
+ const xPx = resolvePosition(annotation.x, scales.x);
39
+ if (xPx === null) return null;
40
+
41
+ start = { x: xPx, y: chartArea.y };
42
+ end = { x: xPx, y: chartArea.y + chartArea.height };
43
+ } else {
44
+ return null;
45
+ }
46
+
47
+ // Determine dash pattern: strokeDash array wins, then style string
48
+ let strokeDasharray: string | undefined;
49
+ if (annotation.strokeDash && annotation.strokeDash.length > 0) {
50
+ strokeDasharray = annotation.strokeDash.join(' ');
51
+ } else if (annotation.style === 'dashed' || annotation.style === undefined) {
52
+ strokeDasharray = DEFAULT_REFLINE_DASH;
53
+ } else if (annotation.style === 'dotted') {
54
+ strokeDasharray = '2 2';
55
+ }
56
+ // 'solid' gets no dasharray
57
+
58
+ // Label placement on reflines. labelAnchor controls position:
59
+ //
60
+ // Horizontal reflines (y set):
61
+ // "left": left end of line, above "right"/"top" (default): right end, above
62
+ // "bottom": right end of line, below
63
+ //
64
+ // Vertical reflines (x set):
65
+ // "right": label to the left of the line, near top
66
+ // "bottom": label to the right of the line, near bottom
67
+ // "left"/"top" (default): label to the right of the line, near top
68
+ let label: ResolvedLabel | undefined;
69
+ if (annotation.label) {
70
+ const isHorizontal = annotation.y !== undefined;
71
+ const anchor = annotation.labelAnchor ?? (isHorizontal ? 'top' : 'left');
72
+
73
+ let baseDx: number;
74
+ let baseDy: number;
75
+ let labelX: number;
76
+ let labelY: number;
77
+ let textAnchor: 'start' | 'middle' | 'end';
78
+
79
+ if (isHorizontal) {
80
+ if (anchor === 'left') {
81
+ baseDx = 4;
82
+ baseDy = -4;
83
+ labelX = start.x;
84
+ labelY = start.y;
85
+ textAnchor = 'start';
86
+ } else if (anchor === 'bottom') {
87
+ baseDx = -4;
88
+ baseDy = 14;
89
+ labelX = end.x;
90
+ labelY = end.y;
91
+ textAnchor = 'end';
92
+ } else {
93
+ // 'right', 'top' (default), 'auto'
94
+ baseDx = -4;
95
+ baseDy = -4;
96
+ labelX = end.x;
97
+ labelY = end.y;
98
+ textAnchor = 'end';
99
+ }
100
+ } else {
101
+ // Vertical refline
102
+ if (anchor === 'right') {
103
+ baseDx = -4;
104
+ baseDy = 14;
105
+ labelX = start.x;
106
+ labelY = start.y;
107
+ textAnchor = 'end';
108
+ } else if (anchor === 'bottom') {
109
+ baseDx = 4;
110
+ baseDy = -4;
111
+ labelX = start.x;
112
+ labelY = end.y;
113
+ textAnchor = 'start';
114
+ } else {
115
+ // 'left', 'top' (default), 'auto' — label to the right of the line, near top
116
+ baseDx = 4;
117
+ baseDy = 14;
118
+ labelX = start.x;
119
+ labelY = start.y;
120
+ textAnchor = 'start';
121
+ }
122
+ }
123
+
124
+ const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
125
+
126
+ const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
127
+ const style = makeAnnotationLabelStyle(
128
+ annotation.fontSize ?? 11,
129
+ annotation.fontWeight ?? 400,
130
+ annotation.stroke ?? defaultStroke,
131
+ isDark,
132
+ fontFamily,
133
+ );
134
+ style.textAnchor = textAnchor;
135
+
136
+ label = {
137
+ text: annotation.label,
138
+ x: labelX + labelDelta.dx,
139
+ y: labelY + labelDelta.dy,
140
+ style,
141
+ visible: true,
142
+ };
143
+ }
144
+
145
+ const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
146
+
147
+ return {
148
+ type: 'refline',
149
+ id: annotation.id,
150
+ line: { start, end },
151
+ label,
152
+ stroke: annotation.stroke ?? defaultStroke,
153
+ strokeDasharray,
154
+ strokeWidth: annotation.strokeWidth ?? 1,
155
+ zIndex: annotation.zIndex,
156
+ };
157
+ }