insomni-plot 0.1.0-alpha.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 (242) hide show
  1. package/LICENSE.md +674 -0
  2. package/README.md +81 -0
  3. package/dist/core.d.mts +340 -0
  4. package/dist/core.mjs +1047 -0
  5. package/dist/index.d.mts +3426 -0
  6. package/dist/index.mjs +12762 -0
  7. package/dist/interactions-DEFL_F4E.mjs +5395 -0
  8. package/dist/range-presets-CzECsu3V.d.mts +1523 -0
  9. package/package.json +34 -0
  10. package/src/annotations.d.ts +121 -0
  11. package/src/annotations.ts +438 -0
  12. package/src/axis.d.ts +184 -0
  13. package/src/axis.test.ts +131 -0
  14. package/src/axis.ts +765 -0
  15. package/src/colorbar.d.ts +69 -0
  16. package/src/colorbar.ts +294 -0
  17. package/src/colors.d.ts +57 -0
  18. package/src/colors.test.ts +28 -0
  19. package/src/colors.ts +486 -0
  20. package/src/core.ts +299 -0
  21. package/src/format.d.ts +54 -0
  22. package/src/format.ts +138 -0
  23. package/src/grammar/accessibility.d.ts +147 -0
  24. package/src/grammar/accessibility.test.ts +199 -0
  25. package/src/grammar/accessibility.ts +443 -0
  26. package/src/grammar/aes.d.ts +35 -0
  27. package/src/grammar/aes.test.ts +75 -0
  28. package/src/grammar/aes.ts +120 -0
  29. package/src/grammar/annotations.d.ts +86 -0
  30. package/src/grammar/annotations.test.ts +68 -0
  31. package/src/grammar/annotations.ts +336 -0
  32. package/src/grammar/attach-brush.d.ts +44 -0
  33. package/src/grammar/attach-brush.test.ts +214 -0
  34. package/src/grammar/attach-brush.ts +111 -0
  35. package/src/grammar/attach-presets.d.ts +33 -0
  36. package/src/grammar/attach-presets.test.ts +106 -0
  37. package/src/grammar/attach-presets.ts +215 -0
  38. package/src/grammar/chart.d.ts +952 -0
  39. package/src/grammar/chart.test.ts +118 -0
  40. package/src/grammar/chart.ts +1172 -0
  41. package/src/grammar/color-utils.d.ts +29 -0
  42. package/src/grammar/color-utils.test.ts +53 -0
  43. package/src/grammar/color-utils.ts +66 -0
  44. package/src/grammar/constants.d.ts +45 -0
  45. package/src/grammar/constants.ts +61 -0
  46. package/src/grammar/coord.d.ts +183 -0
  47. package/src/grammar/coord.test.ts +355 -0
  48. package/src/grammar/coord.ts +619 -0
  49. package/src/grammar/data/pivot.d.ts +57 -0
  50. package/src/grammar/data/pivot.ts +107 -0
  51. package/src/grammar/emphasis-driver.d.ts +69 -0
  52. package/src/grammar/emphasis-driver.test.ts +199 -0
  53. package/src/grammar/emphasis-driver.ts +205 -0
  54. package/src/grammar/equality.d.ts +3 -0
  55. package/src/grammar/equality.ts +40 -0
  56. package/src/grammar/facet.d.ts +63 -0
  57. package/src/grammar/facet.test.ts +60 -0
  58. package/src/grammar/facet.ts +175 -0
  59. package/src/grammar/geoms/_categorical.d.ts +94 -0
  60. package/src/grammar/geoms/_categorical.ts +0 -0
  61. package/src/grammar/geoms/_distribution.d.ts +52 -0
  62. package/src/grammar/geoms/_distribution.ts +125 -0
  63. package/src/grammar/geoms/_mark.d.ts +69 -0
  64. package/src/grammar/geoms/_mark.ts +136 -0
  65. package/src/grammar/geoms/_shape.d.ts +41 -0
  66. package/src/grammar/geoms/_shape.ts +74 -0
  67. package/src/grammar/geoms/aggregate.d.ts +95 -0
  68. package/src/grammar/geoms/aggregate.test.ts +554 -0
  69. package/src/grammar/geoms/aggregate.ts +840 -0
  70. package/src/grammar/geoms/area.d.ts +32 -0
  71. package/src/grammar/geoms/area.test.ts +165 -0
  72. package/src/grammar/geoms/area.ts +578 -0
  73. package/src/grammar/geoms/band.d.ts +27 -0
  74. package/src/grammar/geoms/band.test.ts +57 -0
  75. package/src/grammar/geoms/band.ts +126 -0
  76. package/src/grammar/geoms/bar.d.ts +56 -0
  77. package/src/grammar/geoms/bar.test.ts +367 -0
  78. package/src/grammar/geoms/bar.ts +1054 -0
  79. package/src/grammar/geoms/boxplot.d.ts +129 -0
  80. package/src/grammar/geoms/boxplot.test.ts +299 -0
  81. package/src/grammar/geoms/boxplot.ts +834 -0
  82. package/src/grammar/geoms/connected-scatter.d.ts +27 -0
  83. package/src/grammar/geoms/connected-scatter.test.ts +157 -0
  84. package/src/grammar/geoms/connected-scatter.ts +63 -0
  85. package/src/grammar/geoms/emphasis.d.ts +76 -0
  86. package/src/grammar/geoms/emphasis.test.ts +135 -0
  87. package/src/grammar/geoms/emphasis.ts +162 -0
  88. package/src/grammar/geoms/histogram.d.ts +75 -0
  89. package/src/grammar/geoms/histogram.test.ts +262 -0
  90. package/src/grammar/geoms/histogram.ts +740 -0
  91. package/src/grammar/geoms/index.d.ts +20 -0
  92. package/src/grammar/geoms/index.ts +77 -0
  93. package/src/grammar/geoms/interval.d.ts +31 -0
  94. package/src/grammar/geoms/interval.test.ts +154 -0
  95. package/src/grammar/geoms/interval.ts +342 -0
  96. package/src/grammar/geoms/line.d.ts +38 -0
  97. package/src/grammar/geoms/line.test.ts +247 -0
  98. package/src/grammar/geoms/line.ts +659 -0
  99. package/src/grammar/geoms/point.d.ts +57 -0
  100. package/src/grammar/geoms/point.test.ts +163 -0
  101. package/src/grammar/geoms/point.ts +545 -0
  102. package/src/grammar/geoms/polar.test.ts +216 -0
  103. package/src/grammar/geoms/ribbon.d.ts +21 -0
  104. package/src/grammar/geoms/ribbon.test.ts +170 -0
  105. package/src/grammar/geoms/ribbon.ts +87 -0
  106. package/src/grammar/geoms/ridgeline.d.ts +89 -0
  107. package/src/grammar/geoms/ridgeline.test.ts +247 -0
  108. package/src/grammar/geoms/ridgeline.ts +1164 -0
  109. package/src/grammar/geoms/rolling.d.ts +43 -0
  110. package/src/grammar/geoms/rolling.test.ts +217 -0
  111. package/src/grammar/geoms/rolling.ts +387 -0
  112. package/src/grammar/geoms/rug.d.ts +28 -0
  113. package/src/grammar/geoms/rug.test.ts +126 -0
  114. package/src/grammar/geoms/rug.ts +214 -0
  115. package/src/grammar/geoms/rule.d.ts +23 -0
  116. package/src/grammar/geoms/rule.test.ts +69 -0
  117. package/src/grammar/geoms/rule.ts +212 -0
  118. package/src/grammar/geoms/smooth.d.ts +54 -0
  119. package/src/grammar/geoms/smooth.test.ts +78 -0
  120. package/src/grammar/geoms/smooth.ts +337 -0
  121. package/src/grammar/geoms/text.d.ts +29 -0
  122. package/src/grammar/geoms/text.test.ts +64 -0
  123. package/src/grammar/geoms/text.ts +234 -0
  124. package/src/grammar/geoms/tile.d.ts +61 -0
  125. package/src/grammar/geoms/tile.test.ts +157 -0
  126. package/src/grammar/geoms/tile.ts +621 -0
  127. package/src/grammar/geoms/types.d.ts +319 -0
  128. package/src/grammar/geoms/types.ts +362 -0
  129. package/src/grammar/geoms/violin.d.ts +85 -0
  130. package/src/grammar/geoms/violin.test.ts +187 -0
  131. package/src/grammar/geoms/violin.ts +672 -0
  132. package/src/grammar/index.d.ts +22 -0
  133. package/src/grammar/index.ts +269 -0
  134. package/src/grammar/interactions/_disposable.d.ts +5 -0
  135. package/src/grammar/interactions/_disposable.ts +23 -0
  136. package/src/grammar/interactions/_z.d.ts +4 -0
  137. package/src/grammar/interactions/_z.ts +16 -0
  138. package/src/grammar/interactions/brush-selection.test.ts +262 -0
  139. package/src/grammar/interactions/brush.d.ts +63 -0
  140. package/src/grammar/interactions/brush.test.ts +483 -0
  141. package/src/grammar/interactions/brush.ts +452 -0
  142. package/src/grammar/interactions/crosshair.d.ts +19 -0
  143. package/src/grammar/interactions/crosshair.test.ts +127 -0
  144. package/src/grammar/interactions/crosshair.ts +76 -0
  145. package/src/grammar/interactions/hit-layer.d.ts +64 -0
  146. package/src/grammar/interactions/hit-layer.ts +246 -0
  147. package/src/grammar/interactions/legend.d.ts +19 -0
  148. package/src/grammar/interactions/legend.ts +101 -0
  149. package/src/grammar/interactions/menu.d.ts +93 -0
  150. package/src/grammar/interactions/menu.test.ts +373 -0
  151. package/src/grammar/interactions/menu.ts +342 -0
  152. package/src/grammar/interactions/selection.d.ts +25 -0
  153. package/src/grammar/interactions/selection.test.ts +289 -0
  154. package/src/grammar/interactions/selection.ts +142 -0
  155. package/src/grammar/interactions/series-readout.d.ts +91 -0
  156. package/src/grammar/interactions/series-readout.test.ts +668 -0
  157. package/src/grammar/interactions/series-readout.ts +422 -0
  158. package/src/grammar/interactions/series-snap.d.ts +70 -0
  159. package/src/grammar/interactions/series-snap.test.ts +214 -0
  160. package/src/grammar/interactions/series-snap.ts +218 -0
  161. package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
  162. package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
  163. package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
  164. package/src/grammar/interactions/tooltip.d.ts +140 -0
  165. package/src/grammar/interactions/tooltip.test.ts +406 -0
  166. package/src/grammar/interactions/tooltip.ts +622 -0
  167. package/src/grammar/interactions/transitions.d.ts +34 -0
  168. package/src/grammar/interactions/transitions.test.ts +172 -0
  169. package/src/grammar/interactions/transitions.ts +160 -0
  170. package/src/grammar/layout.d.ts +68 -0
  171. package/src/grammar/layout.ts +186 -0
  172. package/src/grammar/legend-merge.test.ts +332 -0
  173. package/src/grammar/mount.d.ts +78 -0
  174. package/src/grammar/mount.test.ts +479 -0
  175. package/src/grammar/mount.ts +2112 -0
  176. package/src/grammar/palettes.d.ts +54 -0
  177. package/src/grammar/palettes.test.ts +80 -0
  178. package/src/grammar/palettes.ts +167 -0
  179. package/src/grammar/pan-zoom.test.ts +398 -0
  180. package/src/grammar/phylo.d.ts +65 -0
  181. package/src/grammar/phylo.test.ts +59 -0
  182. package/src/grammar/phylo.ts +112 -0
  183. package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
  184. package/src/grammar/pipeline.d.ts +158 -0
  185. package/src/grammar/pipeline.test.ts +463 -0
  186. package/src/grammar/pipeline.ts +1233 -0
  187. package/src/grammar/profiling.d.ts +8 -0
  188. package/src/grammar/profiling.ts +24 -0
  189. package/src/grammar/scales.d.ts +188 -0
  190. package/src/grammar/scales.test.ts +181 -0
  191. package/src/grammar/scales.ts +800 -0
  192. package/src/grammar/svg.d.ts +3 -0
  193. package/src/grammar/svg.ts +39 -0
  194. package/src/grammar/theme.d.ts +261 -0
  195. package/src/grammar/theme.test.ts +105 -0
  196. package/src/grammar/theme.ts +490 -0
  197. package/src/heatmap/cpu.ts +109 -0
  198. package/src/heatmap/gpu.ts +565 -0
  199. package/src/heatmap/types.ts +177 -0
  200. package/src/heatmap.browser.test.ts +308 -0
  201. package/src/heatmap.test.ts +320 -0
  202. package/src/heatmap.ts +123 -0
  203. package/src/index.d.ts +1 -0
  204. package/src/index.ts +8 -0
  205. package/src/interactions.d.ts +48 -0
  206. package/src/interactions.test.ts +226 -0
  207. package/src/interactions.ts +394 -0
  208. package/src/layout/box.d.ts +48 -0
  209. package/src/layout/box.test.ts +107 -0
  210. package/src/layout/box.ts +143 -0
  211. package/src/legend.d.ts +115 -0
  212. package/src/legend.ts +422 -0
  213. package/src/marks/curve.d.ts +43 -0
  214. package/src/marks/curve.ts +244 -0
  215. package/src/marks/stack.d.ts +53 -0
  216. package/src/marks/stack.ts +184 -0
  217. package/src/marks.d.ts +273 -0
  218. package/src/marks.test.ts +541 -0
  219. package/src/marks.ts +1292 -0
  220. package/src/navigator.test.ts +174 -0
  221. package/src/navigator.ts +393 -0
  222. package/src/range-presets.d.ts +113 -0
  223. package/src/range-presets.test.ts +345 -0
  224. package/src/range-presets.ts +349 -0
  225. package/src/scales.d.ts +98 -0
  226. package/src/scales.test.ts +103 -0
  227. package/src/scales.ts +695 -0
  228. package/src/stats/index.d.ts +200 -0
  229. package/src/stats/index.test.ts +349 -0
  230. package/src/stats/index.ts +740 -0
  231. package/src/stats/regression.d.ts +38 -0
  232. package/src/stats/regression.test.ts +56 -0
  233. package/src/stats/regression.ts +396 -0
  234. package/src/stats/rolling-window.d.ts +55 -0
  235. package/src/stats/rolling-window.test.ts +237 -0
  236. package/src/stats/rolling-window.ts +256 -0
  237. package/src/test-setup.ts +19 -0
  238. package/src/viewport/axis-state.d.ts +72 -0
  239. package/src/viewport/axis-state.ts +476 -0
  240. package/src/viewport.d.ts +170 -0
  241. package/src/viewport.test.ts +363 -0
  242. package/src/viewport.ts +510 -0
@@ -0,0 +1,20 @@
1
+ export { point, type PointChannels, type PointOptions } from "./point.ts";
2
+ export { line, type LineChannels, type LineOptions } from "./line.ts";
3
+ export { connectedScatter, type ConnectedScatterChannels, type ConnectedScatterOptions, } from "./connected-scatter.ts";
4
+ export { area, type AreaChannels, type AreaOptions, type AreaPosition } from "./area.ts";
5
+ export { bar, type BarChannels, type BarOptions, type BarPosition } from "./bar.ts";
6
+ export { histogram, type HistogramChannels, type HistogramMeasure, type HistogramOptions, type HistogramPosition, } from "./histogram.ts";
7
+ export { rug, type RugChannels, type RugOptions, type RugSide } from "./rug.ts";
8
+ export { rule, type RuleChannels, type RuleOptions } from "./rule.ts";
9
+ export { smooth, type SmoothChannels, type SmoothLabelOptions, type SmoothMethod, type SmoothOptions, } from "./smooth.ts";
10
+ export { statRolling, type StatRollingChannels, type StatRollingOptions } from "./rolling.ts";
11
+ export { ribbon, type RibbonChannels, type RibbonOptions } from "./ribbon.ts";
12
+ export { interval, type IntervalChannels, type IntervalOptions } from "./interval.ts";
13
+ export { band, type BandChannels, type BandOptions } from "./band.ts";
14
+ export { text, type TextChannels, type TextOptions } from "./text.ts";
15
+ export { boxplot, type BoxplotChannels, type BoxplotOptions, type PointsMode } from "./boxplot.ts";
16
+ export { violin, type ViolinChannels, type ViolinInner, type ViolinOptions, type ViolinScale, } from "./violin.ts";
17
+ export { ridgeline, type RidgelineChannels, type RidgelineFillMode, type RidgelineGeom, type RidgelineInner, type RidgelineOptions, type RidgelineScale, } from "./ridgeline.ts";
18
+ export { tile, type TileChannels, type TileNAOptions, type TileOptions } from "./tile.ts";
19
+ export { aggregate, type AggregateBinBy, type AggregateBinSize, type AggregateBinView, type AggregateBundleResult, type AggregateBundleSummary, type AggregateBundleSummaryKind, type AggregateChannels, type AggregateInnerGeom, type AggregateOptions, type AggregateSummary, type AggregateSummaryKind, type AutoBinSizeInfo, } from "./aggregate.ts";
20
+ export type { ChannelSpec, CompileContext, CompiledHitTest, Geom, GeomKind, HoveredHit, PositionScaleHint, PrepareRangeContext, RangeHints, ResolvedChannelMap, ScaleBundle, ScaleHints, } from "./types.ts";
@@ -0,0 +1,77 @@
1
+ export { point, type PointChannels, type PointOptions } from "./point.ts";
2
+ export { line, type LineChannels, type LineOptions } from "./line.ts";
3
+ export {
4
+ connectedScatter,
5
+ type ConnectedScatterChannels,
6
+ type ConnectedScatterOptions,
7
+ } from "./connected-scatter.ts";
8
+ export { area, type AreaChannels, type AreaOptions, type AreaPosition } from "./area.ts";
9
+ export { bar, type BarChannels, type BarOptions, type BarPosition } from "./bar.ts";
10
+ export {
11
+ histogram,
12
+ type HistogramChannels,
13
+ type HistogramMeasure,
14
+ type HistogramOptions,
15
+ type HistogramPosition,
16
+ } from "./histogram.ts";
17
+ export { rug, type RugChannels, type RugOptions, type RugSide } from "./rug.ts";
18
+ export { rule, type RuleChannels, type RuleOptions } from "./rule.ts";
19
+ export {
20
+ smooth,
21
+ type SmoothChannels,
22
+ type SmoothLabelOptions,
23
+ type SmoothMethod,
24
+ type SmoothOptions,
25
+ } from "./smooth.ts";
26
+ export { statRolling, type StatRollingChannels, type StatRollingOptions } from "./rolling.ts";
27
+ export { ribbon, type RibbonChannels, type RibbonOptions } from "./ribbon.ts";
28
+ export { interval, type IntervalChannels, type IntervalOptions } from "./interval.ts";
29
+ export { band, type BandChannels, type BandOptions } from "./band.ts";
30
+ export { text, type TextChannels, type TextOptions } from "./text.ts";
31
+ export { boxplot, type BoxplotChannels, type BoxplotOptions, type PointsMode } from "./boxplot.ts";
32
+ export {
33
+ violin,
34
+ type ViolinChannels,
35
+ type ViolinInner,
36
+ type ViolinOptions,
37
+ type ViolinScale,
38
+ } from "./violin.ts";
39
+ export {
40
+ ridgeline,
41
+ type RidgelineChannels,
42
+ type RidgelineFillMode,
43
+ type RidgelineGeom,
44
+ type RidgelineInner,
45
+ type RidgelineOptions,
46
+ type RidgelineScale,
47
+ } from "./ridgeline.ts";
48
+ export { tile, type TileChannels, type TileNAOptions, type TileOptions } from "./tile.ts";
49
+ export {
50
+ aggregate,
51
+ type AggregateBinBy,
52
+ type AggregateBinSize,
53
+ type AggregateBinView,
54
+ type AggregateBundleResult,
55
+ type AggregateBundleSummary,
56
+ type AggregateBundleSummaryKind,
57
+ type AggregateChannels,
58
+ type AggregateInnerGeom,
59
+ type AggregateOptions,
60
+ type AggregateSummary,
61
+ type AggregateSummaryKind,
62
+ type AutoBinSizeInfo,
63
+ } from "./aggregate.ts";
64
+ export type {
65
+ ChannelSpec,
66
+ CompileContext,
67
+ CompiledHitTest,
68
+ Geom,
69
+ GeomKind,
70
+ HoveredHit,
71
+ PositionScaleHint,
72
+ PrepareRangeContext,
73
+ RangeHints,
74
+ ResolvedChannelMap,
75
+ ScaleBundle,
76
+ ScaleHints,
77
+ } from "./types.ts";
@@ -0,0 +1,31 @@
1
+ import type { Aes } from "../aes.ts";
2
+ import { type ColorOrAccent } from "../color-utils.ts";
3
+ import type { Geom } from "./types.ts";
4
+ export interface IntervalChannels<T> {
5
+ /** Anchor for vertical intervals (required when `yMin`/`yMax` are bound). */
6
+ x?: Aes<T, number | Date>;
7
+ /** Anchor for horizontal intervals (required when `xMin`/`xMax` are bound). */
8
+ y?: Aes<T, number | Date>;
9
+ yMin?: Aes<T, number | Date>;
10
+ yMax?: Aes<T, number | Date>;
11
+ xMin?: Aes<T, number | Date>;
12
+ xMax?: Aes<T, number | Date>;
13
+ /** Optional categorical color channel — routed through the chart's color scale. */
14
+ color?: Aes<T, unknown>;
15
+ }
16
+ export interface IntervalOptions {
17
+ /**
18
+ * Stroke color. Accepts a literal {@link Color} or a theme accent key
19
+ * (`"positive" | "negative" | "warn" | "info"`); accent keys resolve through
20
+ * `theme.accents`.
21
+ */
22
+ stroke?: ColorOrAccent;
23
+ strokeWidth?: number;
24
+ /** Render perpendicular caps at each end. Default `true`. */
25
+ caps?: boolean;
26
+ /** Cap length in pixels (total, across both sides of the spine). Default `6`. */
27
+ capWidth?: number;
28
+ /** Display label for legend. */
29
+ label?: string;
30
+ }
31
+ export declare function interval<T>(channels: IntervalChannels<T>, options?: IntervalOptions): Geom<T>;
@@ -0,0 +1,154 @@
1
+ // @vitest-environment jsdom
2
+ import { createFrame } from "insomni";
3
+ import { describe, expect, test } from "vite-plus/test";
4
+
5
+ import { resolveAes } from "../aes.ts";
6
+ import { plot } from "../chart.ts";
7
+ import { buildPositionScale, type ScaleBundle } from "../scales.ts";
8
+ import { themeDefault } from "../theme.ts";
9
+ import { interval } from "./interval.ts";
10
+ import type { CompileContext } from "./types.ts";
11
+
12
+ interface Row {
13
+ t: number;
14
+ lo: number;
15
+ hi: number;
16
+ group?: string;
17
+ }
18
+
19
+ const rows: Row[] = [
20
+ { t: 0, lo: 1, hi: 3 },
21
+ { t: 1, lo: 2, hi: 5 },
22
+ { t: 2, lo: 0, hi: 4 },
23
+ ];
24
+
25
+ function makeCtx(data: readonly Row[]): CompileContext<Row> {
26
+ const xAes = resolveAes<Row, unknown>("t");
27
+ const yAes = resolveAes<Row, unknown>("hi");
28
+ // Pin domains so pixel math is predictable in tests.
29
+ const xScale = buildPositionScale(xAes, data, [0, 100], {
30
+ type: "linear",
31
+ domain: [0, 2],
32
+ });
33
+ const yScale = buildPositionScale(yAes, data, [200, 0], {
34
+ type: "linear",
35
+ domain: [0, 5],
36
+ });
37
+ const scales: ScaleBundle = { x: xScale, y: yScale };
38
+ return {
39
+ data,
40
+ scales,
41
+ plot: createFrame({ x: 50, y: 30, width: 100, height: 200 }),
42
+ theme: themeDefault,
43
+ atlas: undefined,
44
+ };
45
+ }
46
+
47
+ describe("interval geom — direction inference", () => {
48
+ test("throws when neither (yMin,yMax) nor (xMin,xMax) is provided", () => {
49
+ expect(() => interval<Row>({ x: "t" })).toThrow(/yMin.*yMax.*xMin.*xMax/);
50
+ });
51
+
52
+ test("throws when both pairs are provided", () => {
53
+ expect(() =>
54
+ interval<Row>({ x: "t", y: "t", yMin: "lo", yMax: "hi", xMin: "lo", xMax: "hi" }),
55
+ ).toThrow(/not both/);
56
+ });
57
+
58
+ test("throws when vertical pair lacks x anchor", () => {
59
+ expect(() => interval<Row>({ yMin: "lo", yMax: "hi" })).toThrow(/`x` is required/);
60
+ });
61
+
62
+ test("throws when horizontal pair lacks y anchor", () => {
63
+ expect(() => interval<Row>({ xMin: "lo", xMax: "hi" })).toThrow(/`y` is required/);
64
+ });
65
+ });
66
+
67
+ describe("interval geom — prepareDomain", () => {
68
+ test("vertical: extends y scale to union of yMin and yMax", () => {
69
+ const geom = interval<Row>({ x: "t", yMin: "lo", yMax: "hi" });
70
+ const hints = geom.prepareDomain!(rows);
71
+ expect(hints?.y?.extend).toEqual([0, 5]);
72
+ });
73
+
74
+ test("horizontal: extends x scale to union of xMin and xMax", () => {
75
+ const geom = interval<Row>({ y: "t", xMin: "lo", xMax: "hi" });
76
+ const hints = geom.prepareDomain!(rows);
77
+ expect(hints?.x?.extend).toEqual([0, 5]);
78
+ });
79
+
80
+ test("returns undefined when there is no finite data", () => {
81
+ const geom = interval<Row>({ x: "t", yMin: "lo", yMax: "hi" });
82
+ expect(geom.prepareDomain!([])).toBeUndefined();
83
+ });
84
+ });
85
+
86
+ describe("interval geom — compileHitTest", () => {
87
+ test("vertical: one hit per row at (x, midY) with [lo, hi] rect", () => {
88
+ const geom = interval<Row>({ x: "t", yMin: "lo", yMax: "hi" });
89
+ const hits = geom.compileHitTest!(makeCtx(rows))!;
90
+ expect(hits.geomKind).toBe("interval");
91
+ expect(hits.dataIndex.length).toBe(3);
92
+ expect(hits.rects).toBeDefined();
93
+ // x domain [0,2] → range [0,100]: t=0 → 0, plus plot.x(50) = 50.
94
+ expect(hits.positions[0]).toBeCloseTo(50, 3);
95
+ // y domain [0,5] → range [200,0]: lo=1 → 160, hi=3 → 80; midY=120, plus plot.y(30) = 150.
96
+ expect(hits.positions[1]).toBeCloseTo(150, 3);
97
+ // Rect[0]: x = 50 + 0 - 3 = 47; y = 30 + min(yLo,yHi) = 30 + 80 = 110.
98
+ expect(hits.rects![0]).toBeCloseTo(47, 3);
99
+ expect(hits.rects![1]).toBeCloseTo(110, 3);
100
+ });
101
+
102
+ test("horizontal: pos at midX, y anchor; rect spans [xMin, xMax]", () => {
103
+ const geom = interval<Row>({ y: "t", xMin: "lo", xMax: "hi" });
104
+ const hits = geom.compileHitTest!(makeCtx(rows))!;
105
+ expect(hits.geomKind).toBe("interval");
106
+ expect(hits.dataIndex.length).toBe(3);
107
+ expect(hits.rects).toBeDefined();
108
+ });
109
+
110
+ test("returns null on empty data", () => {
111
+ const geom = interval<Row>({ x: "t", yMin: "lo", yMax: "hi" });
112
+ expect(geom.compileHitTest!(makeCtx([]))).toBeNull();
113
+ });
114
+
115
+ test("skips rows with non-finite values", () => {
116
+ const partial: Row[] = [
117
+ { t: 0, lo: 1, hi: 3 },
118
+ { t: 1, lo: Number.NaN, hi: 5 },
119
+ ];
120
+ const geom = interval<Row>({ x: "t", yMin: "lo", yMax: "hi" });
121
+ const hits = geom.compileHitTest!(makeCtx(partial))!;
122
+ expect(hits.dataIndex.length).toBe(1);
123
+ expect(hits.dataIndex[0]).toBe(0);
124
+ });
125
+ });
126
+
127
+ describe("interval geom — end to end", () => {
128
+ // The v3 SVG renderer serializes stroked segments (pushLine/pushSegment) as
129
+ // <line> elements. Count those to capture the spine + cap segments.
130
+ function countStrokeLines(svg: SVGSVGElement): number {
131
+ return svg.querySelectorAll("line").length;
132
+ }
133
+
134
+ test("renders into a plot() and produces stroke segments per row", () => {
135
+ const svg = plot({ data: rows, width: 320, height: 200 })
136
+ .layer(interval({ x: "t", yMin: "lo", yMax: "hi" }))
137
+ .toSVG();
138
+ expect(svg).toBeInstanceOf(SVGSVGElement);
139
+ // 1 spine + 2 caps per row × 3 rows = 9 stroke segments.
140
+ expect(countStrokeLines(svg)).toBe(9);
141
+ });
142
+
143
+ test("caps: false omits cap segments", () => {
144
+ const svgWithCaps = plot({ data: rows, width: 320, height: 200 })
145
+ .layer(interval({ x: "t", yMin: "lo", yMax: "hi" }, { caps: true }))
146
+ .toSVG();
147
+ const svgNoCaps = plot({ data: rows, width: 320, height: 200 })
148
+ .layer(interval({ x: "t", yMin: "lo", yMax: "hi" }, { caps: false }))
149
+ .toSVG();
150
+ expect(svgWithCaps.querySelectorAll("line").length).toBeGreaterThan(
151
+ svgNoCaps.querySelectorAll("line").length,
152
+ );
153
+ });
154
+ });
@@ -0,0 +1,342 @@
1
+ // ---------------------------------------------------------------------------
2
+ // interval geom — directed [lo, hi] range anchored at a coordinate
3
+ // ---------------------------------------------------------------------------
4
+ // One primitive covers confidence intervals, error bars, candlestick wicks,
5
+ // gantt durations, daily highs/lows, prediction intervals, and IQR whiskers.
6
+ //
7
+ // Direction is inferred from which pair of channels is bound:
8
+ // - `yMin` + `yMax` → vertical interval, anchored at `x`
9
+ // - `xMin` + `xMax` → horizontal interval, anchored at `y`
10
+ //
11
+ // Pair this with `point()` when a center marker is wanted.
12
+
13
+ import type { Color, Layer } from "insomni";
14
+ import { lineSwatch } from "../../legend.ts";
15
+ import type { Aes, ResolvedAes } from "../aes.ts";
16
+ import { resolveAes } from "../aes.ts";
17
+ import { alphaize, resolveAccent, type ColorOrAccent } from "../color-utils.ts";
18
+ import type {
19
+ CompileContext,
20
+ CompiledHitTest,
21
+ Geom,
22
+ ResolvedChannelMap,
23
+ ScaleHints,
24
+ } from "./types.ts";
25
+ import { selectedIndicesFor, selectionActive, SELECTION_DIM_ALPHA } from "./_mark.ts";
26
+
27
+ export interface IntervalChannels<T> {
28
+ /** Anchor for vertical intervals (required when `yMin`/`yMax` are bound). */
29
+ x?: Aes<T, number | Date>;
30
+ /** Anchor for horizontal intervals (required when `xMin`/`xMax` are bound). */
31
+ y?: Aes<T, number | Date>;
32
+ yMin?: Aes<T, number | Date>;
33
+ yMax?: Aes<T, number | Date>;
34
+ xMin?: Aes<T, number | Date>;
35
+ xMax?: Aes<T, number | Date>;
36
+ /** Optional categorical color channel — routed through the chart's color scale. */
37
+ color?: Aes<T, unknown>;
38
+ }
39
+
40
+ export interface IntervalOptions {
41
+ /**
42
+ * Stroke color. Accepts a literal {@link Color} or a theme accent key
43
+ * (`"positive" | "negative" | "warn" | "info"`); accent keys resolve through
44
+ * `theme.accents`.
45
+ */
46
+ stroke?: ColorOrAccent;
47
+ strokeWidth?: number;
48
+ /** Render perpendicular caps at each end. Default `true`. */
49
+ caps?: boolean;
50
+ /** Cap length in pixels (total, across both sides of the spine). Default `6`. */
51
+ capWidth?: number;
52
+ /** Display label for legend. */
53
+ label?: string;
54
+ }
55
+
56
+ type Orientation = "vertical" | "horizontal";
57
+
58
+ function inferOrientation<T>(channels: IntervalChannels<T>): Orientation {
59
+ const hasYPair = channels.yMin !== undefined && channels.yMax !== undefined;
60
+ const hasXPair = channels.xMin !== undefined && channels.xMax !== undefined;
61
+ if (hasYPair && hasXPair) {
62
+ throw new Error(
63
+ "interval(): bind either (yMin, yMax) or (xMin, xMax) — not both. Direction is inferred from which pair is set.",
64
+ );
65
+ }
66
+ if (hasYPair) {
67
+ if (channels.x === undefined) {
68
+ throw new Error("interval(): `x` is required when `yMin`/`yMax` are bound (vertical).");
69
+ }
70
+ return "vertical";
71
+ }
72
+ if (hasXPair) {
73
+ if (channels.y === undefined) {
74
+ throw new Error("interval(): `y` is required when `xMin`/`xMax` are bound (horizontal).");
75
+ }
76
+ return "horizontal";
77
+ }
78
+ throw new Error(
79
+ "interval(): one of (yMin, yMax) or (xMin, xMax) must be provided to define the range.",
80
+ );
81
+ }
82
+
83
+ function extentOf<T>(
84
+ aLo: ResolvedAes<T, unknown>,
85
+ aHi: ResolvedAes<T, unknown>,
86
+ data: readonly T[],
87
+ ): readonly [number, number] | undefined {
88
+ let lo = Number.POSITIVE_INFINITY;
89
+ let hi = Number.NEGATIVE_INFINITY;
90
+ for (let i = 0; i < data.length; i++) {
91
+ const d = data[i]!;
92
+ const a = aLo.fn(d, i);
93
+ const b = aHi.fn(d, i);
94
+ const an = a instanceof Date ? a.getTime() : (a as number);
95
+ const bn = b instanceof Date ? b.getTime() : (b as number);
96
+ if (Number.isFinite(an)) {
97
+ if (an < lo) lo = an;
98
+ if (an > hi) hi = an;
99
+ }
100
+ if (Number.isFinite(bn)) {
101
+ if (bn < lo) lo = bn;
102
+ if (bn > hi) hi = bn;
103
+ }
104
+ }
105
+ return Number.isFinite(lo) && Number.isFinite(hi) ? [lo, hi] : undefined;
106
+ }
107
+
108
+ export function interval<T>(channels: IntervalChannels<T>, options: IntervalOptions = {}): Geom<T> {
109
+ const orientation = inferOrientation(channels);
110
+ const isVertical = orientation === "vertical";
111
+
112
+ // Register one end of the range as the "value" channel so the pipeline
113
+ // picks it up for color/domain inference; prepareDomain extends it with
114
+ // the other end. (Mirrors `ribbon`'s registration of y1 + extend via y0.)
115
+ const registeredChannels = isVertical
116
+ ? { x: channels.x, y: channels.yMax, color: channels.color }
117
+ : { x: channels.xMax, y: channels.y, color: channels.color };
118
+
119
+ return {
120
+ kind: "interval",
121
+ channels: registeredChannels,
122
+ label: options.label,
123
+ legendSwatch: (color, theme) =>
124
+ lineSwatch({
125
+ stroke: color,
126
+ strokeWidth: options.strokeWidth ?? theme.marks.ruleStrokeWidth,
127
+ }),
128
+ prepareDomain(data) {
129
+ if (isVertical) {
130
+ const loAes = resolveAes<T, unknown>(channels.yMin as Aes<T, unknown>);
131
+ const hiAes = resolveAes<T, unknown>(channels.yMax as Aes<T, unknown>);
132
+ const ext = extentOf(loAes, hiAes, data);
133
+ if (!ext) return undefined;
134
+ const hints: ScaleHints = { y: { extend: ext } };
135
+ return hints;
136
+ }
137
+ const loAes = resolveAes<T, unknown>(channels.xMin as Aes<T, unknown>);
138
+ const hiAes = resolveAes<T, unknown>(channels.xMax as Aes<T, unknown>);
139
+ const ext = extentOf(loAes, hiAes, data);
140
+ if (!ext) return undefined;
141
+ const hints: ScaleHints = { x: { extend: ext } };
142
+ return hints;
143
+ },
144
+ compile(ctx: CompileContext<T>) {
145
+ const { data, scales, plot, theme } = ctx;
146
+
147
+ const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
148
+ const colorScale = scales.color?.fn;
149
+ const baseStroke: Color =
150
+ resolveAccent(options.stroke, theme) ?? theme.palettes.categorical(0);
151
+ const strokeWidth = options.strokeWidth ?? theme.marks.ruleStrokeWidth;
152
+ const drawCaps = options.caps ?? true;
153
+ const capHalf = (options.capWidth ?? 6) / 2;
154
+
155
+ const hidden = ctx.hidden;
156
+ const selected = selectedIndicesFor(ctx, "interval");
157
+ const dimWhenSelected = selectionActive(ctx);
158
+
159
+ const ox = plot.topLeft.x;
160
+ const oy = plot.topLeft.y;
161
+
162
+ interface Segment {
163
+ x1: number;
164
+ y1: number;
165
+ x2: number;
166
+ y2: number;
167
+ stroke: Color;
168
+ }
169
+ const spines: Segment[] = [];
170
+ const caps: Segment[] = [];
171
+
172
+ if (isVertical) {
173
+ const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
174
+ const loAes = resolveAes<T, unknown>(channels.yMin as Aes<T, unknown>);
175
+ const hiAes = resolveAes<T, unknown>(channels.yMax as Aes<T, unknown>);
176
+ const xFn = scales.x.fn;
177
+ const yFn = scales.y.fn;
178
+ for (let i = 0; i < data.length; i++) {
179
+ const d = data[i]!;
180
+ if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) continue;
181
+ const xPx = xFn(xAes.fn(d, i) as never);
182
+ const yLo = yFn(loAes.fn(d, i) as never);
183
+ const yHi = yFn(hiAes.fn(d, i) as never);
184
+ if (!Number.isFinite(xPx) || !Number.isFinite(yLo) || !Number.isFinite(yHi)) continue;
185
+ let stroke: Color = colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseStroke;
186
+ if (dimWhenSelected && (!selected || !selected.has(i))) {
187
+ stroke = alphaize(stroke, SELECTION_DIM_ALPHA);
188
+ }
189
+ spines.push({ x1: xPx, y1: yLo, x2: xPx, y2: yHi, stroke });
190
+ if (drawCaps && capHalf > 0) {
191
+ caps.push({ x1: xPx - capHalf, y1: yLo, x2: xPx + capHalf, y2: yLo, stroke });
192
+ caps.push({ x1: xPx - capHalf, y1: yHi, x2: xPx + capHalf, y2: yHi, stroke });
193
+ }
194
+ }
195
+ } else {
196
+ const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
197
+ const loAes = resolveAes<T, unknown>(channels.xMin as Aes<T, unknown>);
198
+ const hiAes = resolveAes<T, unknown>(channels.xMax as Aes<T, unknown>);
199
+ const xFn = scales.x.fn;
200
+ const yFn = scales.y.fn;
201
+ for (let i = 0; i < data.length; i++) {
202
+ const d = data[i]!;
203
+ if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) continue;
204
+ const yPx = yFn(yAes.fn(d, i) as never);
205
+ const xLo = xFn(loAes.fn(d, i) as never);
206
+ const xHi = xFn(hiAes.fn(d, i) as never);
207
+ if (!Number.isFinite(yPx) || !Number.isFinite(xLo) || !Number.isFinite(xHi)) continue;
208
+ let stroke: Color = colorAes && colorScale ? colorScale(colorAes.fn(d, i)) : baseStroke;
209
+ if (dimWhenSelected && (!selected || !selected.has(i))) {
210
+ stroke = alphaize(stroke, SELECTION_DIM_ALPHA);
211
+ }
212
+ spines.push({ x1: xLo, y1: yPx, x2: xHi, y2: yPx, stroke });
213
+ if (drawCaps && capHalf > 0) {
214
+ caps.push({ x1: xLo, y1: yPx - capHalf, x2: xLo, y2: yPx + capHalf, stroke });
215
+ caps.push({ x1: xHi, y1: yPx - capHalf, x2: xHi, y2: yPx + capHalf, stroke });
216
+ }
217
+ }
218
+ }
219
+
220
+ const builder = {
221
+ length: spines.length + caps.length,
222
+ addTo(layer: Layer): Layer {
223
+ for (const s of spines) {
224
+ layer.pushLine({
225
+ x1: ox + s.x1,
226
+ y1: oy + s.y1,
227
+ x2: ox + s.x2,
228
+ y2: oy + s.y2,
229
+ color: s.stroke,
230
+ width: strokeWidth,
231
+ });
232
+ }
233
+ for (const s of caps) {
234
+ layer.pushLine({
235
+ x1: ox + s.x1,
236
+ y1: oy + s.y1,
237
+ x2: ox + s.x2,
238
+ y2: oy + s.y2,
239
+ color: s.stroke,
240
+ width: strokeWidth,
241
+ });
242
+ }
243
+ return layer;
244
+ },
245
+ };
246
+
247
+ return [builder];
248
+ },
249
+ compileHitTest(ctx: CompileContext<T>): CompiledHitTest<T> | null {
250
+ const { data, scales, plot } = ctx;
251
+ const colorAes = channels.color ? resolveAes<T, unknown>(channels.color) : undefined;
252
+ const hidden = ctx.hidden;
253
+ const capHalf = (options.capWidth ?? 6) / 2;
254
+ const ox = plot.topLeft.x;
255
+ const oy = plot.topLeft.y;
256
+
257
+ // One hit per row at the spine midpoint; the rect covers [lo, hi] on
258
+ // the value axis × the cap span on the anchor axis. The hit-layer
259
+ // prefers rects when present, so the cursor anywhere along the spine
260
+ // selects the interval.
261
+ const positions = new Float32Array(data.length * 2);
262
+ const rects = new Float32Array(data.length * 4);
263
+ const dataIndex = new Int32Array(data.length);
264
+ let n = 0;
265
+
266
+ const channelsMap: ResolvedChannelMap<T> = isVertical
267
+ ? {
268
+ x: resolveAes<T, unknown>(channels.x as Aes<T, unknown>),
269
+ y: resolveAes<T, unknown>(channels.yMax as Aes<T, unknown>),
270
+ color: colorAes,
271
+ }
272
+ : {
273
+ x: resolveAes<T, unknown>(channels.xMax as Aes<T, unknown>),
274
+ y: resolveAes<T, unknown>(channels.y as Aes<T, unknown>),
275
+ color: colorAes,
276
+ };
277
+
278
+ if (isVertical) {
279
+ const xAes = resolveAes<T, unknown>(channels.x as Aes<T, unknown>);
280
+ const loAes = resolveAes<T, unknown>(channels.yMin as Aes<T, unknown>);
281
+ const hiAes = resolveAes<T, unknown>(channels.yMax as Aes<T, unknown>);
282
+ const xFn = scales.x.fn;
283
+ const yFn = scales.y.fn;
284
+ for (let i = 0; i < data.length; i++) {
285
+ const d = data[i]!;
286
+ if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) continue;
287
+ const xPx = xFn(xAes.fn(d, i) as never);
288
+ const yLo = yFn(loAes.fn(d, i) as never);
289
+ const yHi = yFn(hiAes.fn(d, i) as never);
290
+ if (!Number.isFinite(xPx) || !Number.isFinite(yLo) || !Number.isFinite(yHi)) continue;
291
+ const yTop = Math.min(yLo, yHi);
292
+ const yBot = Math.max(yLo, yHi);
293
+ positions[n * 2] = ox + xPx;
294
+ positions[n * 2 + 1] = oy + (yTop + yBot) / 2;
295
+ rects[n * 4] = ox + xPx - capHalf;
296
+ rects[n * 4 + 1] = oy + yTop;
297
+ rects[n * 4 + 2] = Math.max(capHalf * 2, 4);
298
+ rects[n * 4 + 3] = Math.max(yBot - yTop, 1);
299
+ dataIndex[n] = i;
300
+ n++;
301
+ }
302
+ } else {
303
+ const yAes = resolveAes<T, unknown>(channels.y as Aes<T, unknown>);
304
+ const loAes = resolveAes<T, unknown>(channels.xMin as Aes<T, unknown>);
305
+ const hiAes = resolveAes<T, unknown>(channels.xMax as Aes<T, unknown>);
306
+ const xFn = scales.x.fn;
307
+ const yFn = scales.y.fn;
308
+ for (let i = 0; i < data.length; i++) {
309
+ const d = data[i]!;
310
+ if (hidden && colorAes && hidden.has(String(colorAes.fn(d, i)))) continue;
311
+ const yPx = yFn(yAes.fn(d, i) as never);
312
+ const xLo = xFn(loAes.fn(d, i) as never);
313
+ const xHi = xFn(hiAes.fn(d, i) as never);
314
+ if (!Number.isFinite(yPx) || !Number.isFinite(xLo) || !Number.isFinite(xHi)) continue;
315
+ const xLeft = Math.min(xLo, xHi);
316
+ const xRight = Math.max(xLo, xHi);
317
+ positions[n * 2] = ox + (xLeft + xRight) / 2;
318
+ positions[n * 2 + 1] = oy + yPx;
319
+ rects[n * 4] = ox + xLeft;
320
+ rects[n * 4 + 1] = oy + yPx - capHalf;
321
+ rects[n * 4 + 2] = Math.max(xRight - xLeft, 1);
322
+ rects[n * 4 + 3] = Math.max(capHalf * 2, 4);
323
+ dataIndex[n] = i;
324
+ n++;
325
+ }
326
+ }
327
+
328
+ if (n === 0) return null;
329
+
330
+ return {
331
+ geomKind: "interval",
332
+ label: options.label,
333
+ positions: positions.subarray(0, n * 2),
334
+ rects: rects.subarray(0, n * 4),
335
+ dataIndex: dataIndex.subarray(0, n),
336
+ pickRadius: Math.max(8, capHalf * 2 + 4),
337
+ channels: channelsMap,
338
+ data,
339
+ };
340
+ },
341
+ };
342
+ }
@@ -0,0 +1,38 @@
1
+ import { type Color } from "insomni";
2
+ import { type LineCurve, type LineDashStyle } from "../../marks.ts";
3
+ import type { Aes } from "../aes.ts";
4
+ import type { Geom } from "./types.ts";
5
+ export interface LineChannels<T> {
6
+ x: Aes<T, number | Date>;
7
+ y: Aes<T, number | Date>;
8
+ /** Categorical color channel splits the line into one stroke per category. */
9
+ color?: Aes<T, unknown>;
10
+ /**
11
+ * Optional ordering aesthetic. When present, rows are connected in ascending
12
+ * order (globally, or within each color-grouped series).
13
+ */
14
+ order?: Aes<T, number | Date>;
15
+ }
16
+ export interface LineOptions {
17
+ stroke?: Color;
18
+ strokeWidth?: number;
19
+ curve?: LineCurve;
20
+ curveSamples?: number;
21
+ dashPattern?: readonly number[];
22
+ /**
23
+ * Categorical dash treatment. `dashPattern` takes precedence when both
24
+ * are supplied. See {@link LineDashStyle}.
25
+ */
26
+ dashStyle?: LineDashStyle;
27
+ label?: string;
28
+ /**
29
+ * When true, hover hit-tests resolve to the nearest vertex *by x* — the
30
+ * cursor's vertical position doesn't influence which datum is picked, so
31
+ * the user can hover anywhere along the line. Default `false` (per-vertex
32
+ * Euclidean pick within `pickRadius`). For multi-line charts this means
33
+ * the cursor's x picks one vertex per series; the topmost series wins
34
+ * via PointCloud zIndex order.
35
+ */
36
+ nearestX?: boolean;
37
+ }
38
+ export declare function line<T>(channels: LineChannels<T>, options?: LineOptions): Geom<T>;