@fundar/data-chart-telling 1.0.0 → 1.1.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 (155) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +57 -18
  3. package/dist/configuration/config.svelte.js +13 -1
  4. package/dist/configuration/themes/index.d.ts +124 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +1 -0
  7. package/dist/layout/base/BasePlotLayout.svelte +18 -5
  8. package/dist/layout/hover/HoverSettings.d.ts +20 -6
  9. package/dist/layout/hover/HoverSettings.js +29 -4
  10. package/dist/layout/legend/LegendState.svelte.d.ts +5 -0
  11. package/dist/layout/legend/LegendState.svelte.js +4 -0
  12. package/dist/layout/plot/Plot.svelte +6 -3
  13. package/dist/layout/plot/PublicPlotContext.d.ts +5 -4
  14. package/dist/layout/plot/PublicPlotContext.js +7 -3
  15. package/dist/layout/plot/Slice.js +4 -2
  16. package/dist/layout/scales/EdgeTicks.d.ts +39 -0
  17. package/dist/layout/scales/EdgeTicks.js +82 -0
  18. package/dist/layout/scales/NiceTicks.d.ts +40 -0
  19. package/dist/layout/scales/NiceTicks.js +72 -0
  20. package/dist/layout/scales/ScalesLayout.svelte +64 -30
  21. package/dist/layout/scales/ScalesLayout.svelte.d.ts +5 -0
  22. package/dist/layout/scales/SveltePlotScale.d.ts +15 -0
  23. package/dist/layout/scales/SveltePlotScale.js +79 -0
  24. package/dist/layout/styles/ValueLabels.js +3 -2
  25. package/dist/layout/timeline/Timeline.svelte +5 -2
  26. package/dist/layout/timeline/Timeline.svelte.d.ts +2 -0
  27. package/dist/layout/timeline/TimelineLayout.svelte +38 -12
  28. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +7 -5
  29. package/dist/layout/timeline/TimelineRule.svelte +1 -0
  30. package/dist/layout/timeline/TimelineState.svelte.d.ts +13 -5
  31. package/dist/layout/timeline/TimelineState.svelte.js +42 -8
  32. package/dist/layout/timeline/TimelineTime.d.ts +18 -0
  33. package/dist/layout/timeline/TimelineTime.js +117 -0
  34. package/dist/layout/tooltip/Tooltip.svelte +6 -3
  35. package/dist/layout/tooltip/Tooltip.svelte.d.ts +2 -0
  36. package/dist/layout/tooltip/TooltipState.svelte.d.ts +8 -2
  37. package/dist/layout/tooltip/TooltipState.svelte.js +22 -3
  38. package/dist/markers/delta/DeltaMarker.svelte +9 -7
  39. package/dist/phases/Pipeline.js +3 -2
  40. package/dist/phases/data/DataPhase.d.ts +5 -11
  41. package/dist/phases/data/DataPhase.js +11 -8
  42. package/dist/phases/facet/FacetPanels.d.ts +54 -0
  43. package/dist/phases/facet/FacetPanels.js +95 -0
  44. package/dist/phases/facet/FacetPhase.d.ts +1 -42
  45. package/dist/phases/facet/FacetPhase.js +1 -87
  46. package/dist/phases/hover/HoverPhase.d.ts +6 -43
  47. package/dist/phases/hover/HoverPhase.js +19 -54
  48. package/dist/phases/mapping/MappingPhase.d.ts +1 -1
  49. package/dist/phases/mapping/MappingPhase.js +2 -4
  50. package/dist/phases/scales/ScalesPhase.d.ts +1 -3
  51. package/dist/phases/scales/ScalesPhase.js +17 -21
  52. package/dist/phases/sharedScales/SharedScalesPhase.d.ts +4 -1
  53. package/dist/phases/sharedScales/SharedScalesPhase.js +10 -7
  54. package/dist/phases/statistics/StatisticsPhase.js +3 -4
  55. package/dist/phases/tooltip/TooltipPhase.d.ts +4 -2
  56. package/dist/phases/tooltip/TooltipPhase.js +10 -3
  57. package/dist/phases/transforms/ProcessedPositions.d.ts +30 -0
  58. package/dist/phases/transforms/ProcessedPositions.js +74 -0
  59. package/dist/phases/transforms/TransformsPhase.d.ts +5 -42
  60. package/dist/phases/transforms/TransformsPhase.js +20 -57
  61. package/dist/plots/area/markers.js +1 -2
  62. package/dist/plots/bar/defaults.js +6 -10
  63. package/dist/plots/bar/markers.js +5 -4
  64. package/dist/plots/box/BoxSummaries.d.ts +19 -0
  65. package/dist/plots/box/BoxSummaries.js +101 -0
  66. package/dist/plots/box/Plot.svelte +42 -0
  67. package/dist/plots/box/Plot.svelte.d.ts +26 -0
  68. package/dist/plots/box/defaults.d.ts +12 -0
  69. package/dist/plots/box/defaults.js +34 -0
  70. package/dist/plots/box/markers.d.ts +12 -0
  71. package/dist/plots/box/markers.js +34 -0
  72. package/dist/plots/geo/phases/hover.js +5 -5
  73. package/dist/plots/geo/phases/markers.js +3 -4
  74. package/dist/plots/geo/slices.js +6 -3
  75. package/dist/plots/heatmap/markers.js +2 -1
  76. package/dist/plots/histogram/defaults.js +1 -1
  77. package/dist/plots/histogram/markers.js +4 -4
  78. package/dist/plots/line/markers.js +1 -2
  79. package/dist/plots/pyramid/defaults.js +3 -2
  80. package/dist/plots/pyramid/markers.js +3 -6
  81. package/dist/plots/scatter/defaults.js +3 -2
  82. package/dist/plots/scatter/markers.js +4 -3
  83. package/dist/types/configuration/styling.d.ts +24 -2
  84. package/dist/types/layout/composer.d.ts +5 -2
  85. package/dist/types/layout/scales.d.ts +16 -0
  86. package/dist/types/layout/timeline.d.ts +14 -5
  87. package/dist/types/phases/hover/phase.d.ts +6 -0
  88. package/dist/types/phases/state.d.ts +5 -2
  89. package/dist/types/phases/statistics/phase.d.ts +3 -1
  90. package/dist/types/phases/transforms/phase.d.ts +10 -0
  91. package/dist/types/plots/box/props.d.ts +10 -0
  92. package/dist/types/plots/box/props.js +1 -0
  93. package/dist/types/plots/box/state.d.ts +23 -0
  94. package/dist/types/plots/box/state.js +1 -0
  95. package/dist/types/plots/props.d.ts +2 -0
  96. package/docs/chrome.md +39 -8
  97. package/docs/composer.md +19 -3
  98. package/docs/concepts.md +1 -1
  99. package/docs/development.md +14 -5
  100. package/docs/facets.md +62 -8
  101. package/docs/getting-started.md +8 -9
  102. package/docs/legend.md +62 -15
  103. package/docs/markers.md +17 -6
  104. package/docs/plots.md +168 -42
  105. package/docs/scales.md +36 -19
  106. package/docs/state.md +1 -1
  107. package/docs/styles-and-segments.md +85 -31
  108. package/docs/theming.md +30 -19
  109. package/docs/timeline.md +62 -28
  110. package/docs/tooltip-and-hover.md +72 -24
  111. package/docs/transforms-and-statistics.md +47 -17
  112. package/examples/BarValues.svelte +2 -2
  113. package/examples/Box.svelte +7 -0
  114. package/examples/Chrome.svelte +17 -0
  115. package/examples/ChromeOrder.svelte +18 -0
  116. package/examples/Composer.svelte +23 -0
  117. package/examples/CustomTooltip.svelte +21 -14
  118. package/examples/Facets.svelte +3 -3
  119. package/examples/FacetsCarousel.svelte +14 -0
  120. package/examples/FacetsRowsCols.svelte +23 -0
  121. package/examples/FirstPlot.svelte +7 -0
  122. package/examples/Gaps.svelte +21 -0
  123. package/examples/GroupedBars.svelte +15 -0
  124. package/examples/Heatmap.svelte +7 -0
  125. package/examples/HeatmapSegments.svelte +26 -0
  126. package/examples/Histogram.svelte +14 -0
  127. package/examples/HorizontalBars.svelte +15 -0
  128. package/examples/LegendSections.svelte +27 -0
  129. package/examples/LegendToggle.svelte +3 -3
  130. package/examples/LineByRegion.svelte +18 -0
  131. package/examples/LineDates.svelte +15 -0
  132. package/examples/Markers.svelte +27 -0
  133. package/examples/MovingAverage.svelte +15 -0
  134. package/examples/Pyramid.svelte +7 -0
  135. package/examples/Regression.svelte +14 -0
  136. package/examples/ResponsiveLine.svelte +7 -7
  137. package/examples/Scales.svelte +26 -0
  138. package/examples/Scatter.svelte +16 -0
  139. package/examples/SegmentHighlight.svelte +22 -0
  140. package/examples/Segments.svelte +11 -3
  141. package/examples/SegmentsBySeries.svelte +25 -0
  142. package/examples/StackedArea.svelte +16 -0
  143. package/examples/StackedBars.svelte +15 -0
  144. package/examples/TimelinePlayer.svelte +8 -7
  145. package/examples/TimelineRule.svelte +19 -0
  146. package/examples/Tooltip.svelte +3 -3
  147. package/examples/ValueLabels.svelte +21 -0
  148. package/examples/data.ts +320 -8
  149. package/llms-full.txt +33 -5
  150. package/llms.txt +30 -2
  151. package/package.json +3 -2
  152. package/dist/layout/scales/buildScale.d.ts +0 -8
  153. package/dist/layout/scales/buildScale.js +0 -67
  154. package/dist/layout/scales/niceDomain.d.ts +0 -23
  155. package/dist/layout/scales/niceDomain.js +0 -59
@@ -1,7 +1,8 @@
1
- import { resolveAccessor } from '../../plots/utils/accessors';
1
+ import { ProcessedPositions } from '../transforms/ProcessedPositions';
2
2
  import { isGapValue } from '../../plots/utils/gaps';
3
3
  import { MarkerAxisValues } from './MarkerAxisValues';
4
4
  import { PlotAxes } from '../data/PlotAxes';
5
+ import { NiceTicks } from '../../layout/scales/NiceTicks';
5
6
  import { DISCRETE_SCALE_TYPES } from '../../types/layout/scales';
6
7
  const AXES = ['x', 'y'];
7
8
  const POSITION_CHANNELS = {
@@ -86,15 +87,22 @@ export class ScalesPhase {
86
87
  const aggregated = (processedData?.transforms.length ?? 0) > 0 && zero.has(axis);
87
88
  if (!derived.length && !ranged && !aggregated)
88
89
  continue;
89
- const own = numeric(ScalesPhase.processedValues(processedData, axis));
90
+ const own = numeric(ProcessedPositions.values(processedData, axis));
90
91
  if (!own)
91
92
  continue;
92
93
  const values = [...own, ...derived, ...(zero.has(axis) ? [0] : [])];
93
- scales[axis] = {
94
- ...scales[axis],
95
- domain: [Math.min(...values), Math.max(...values)],
96
- nice: scales[axis]?.nice ?? true
97
- };
94
+ const lo = Math.min(...values);
95
+ const hi = Math.max(...values);
96
+ const author = scales[axis];
97
+ // Niced here, with a tolerance, so parts that add up to a whole land on its tick.
98
+ const niced = author?.nice !== false && author?.ticks == null && hi > lo;
99
+ scales[axis] = niced
100
+ ? {
101
+ ...author,
102
+ domain: NiceTicks.fit(state, axis, lo, hi, author?.niceTolerance),
103
+ nice: false
104
+ }
105
+ : { ...author, domain: [lo, hi], nice: author?.nice ?? true };
98
106
  }
99
107
  const next = { ...state, scales };
100
108
  return options.extend ? options.extend(next, ctx) : next;
@@ -116,22 +124,10 @@ export class ScalesPhase {
116
124
  }
117
125
  }
118
126
  if (this.keepsDataOrder && !band.domain) {
119
- band.domain = [...new Set(ScalesPhase.processedValues(processed, axis))];
127
+ band.domain = [...new Set(ProcessedPositions.values(processed, axis))];
120
128
  }
121
129
  return band;
122
130
  }
123
- /** Every value the plot's own processed rows sit at on one axis. */
124
- static processedValues(processed, axis) {
125
- if (!processed)
126
- return [];
127
- return POSITION_CHANNELS[axis].flatMap((name) => {
128
- const channel = processed.channels[name];
129
- if (channel === undefined)
130
- return [];
131
- const read = resolveAccessor(channel);
132
- return processed.data.map((row) => read(row));
133
- });
134
- }
135
131
  /** The axes `refs` name, by name or by the role they play in this state. */
136
132
  static #axesOf(refs, state) {
137
133
  return (refs ?? []).map((ref) => ref === 'value'
@@ -147,7 +143,7 @@ export class ScalesPhase {
147
143
  }
148
144
  /** How far an axis reaches from zero: the largest magnitude its rows, and the gaps filled in between them, sit at. */
149
145
  static #reachOf(state, axis) {
150
- const rows = ScalesPhase.processedValues(state.processedData, axis)
146
+ const rows = ProcessedPositions.values(state.processedData, axis)
151
147
  .filter((v) => !isGapValue(v))
152
148
  .map((v) => Math.abs(Number(v)));
153
149
  const filled = [...(state.gapFillMaps?.values() ?? [])].flatMap((fills) => [...fills.values()].map((fill) => Math.abs(fill.value)));
@@ -13,6 +13,9 @@ type SharedScalesState = FacetStageState & {
13
13
  axes?: PlotAxes;
14
14
  processedData?: ProcessedData;
15
15
  markers?: object[];
16
+ scales?: Partial<Record<AxisChannelName, {
17
+ niceTolerance?: number;
18
+ }>>;
16
19
  sharedScales?: Record<string, AxisBasedScalesConfig<never>>;
17
20
  };
18
21
  /**
@@ -38,7 +41,7 @@ export declare class SharedScalesPhase<TState extends SharedScalesState, TCtx> i
38
41
  * count derived from each panel's height, which over-rounds short panels and
39
42
  * can disagree between panels of different heights.
40
43
  */
41
- static buildSharedAxisScale(slices: SharedAxisValues[], axis: AxisChannelName): SharedScale;
44
+ static buildSharedAxisScale(slices: SharedAxisValues[], axis: AxisChannelName, niceTolerance?: number): SharedScale;
42
45
  /**
43
46
  * Where one panel's rows and markers sit on each axis, read off a pipeline
44
47
  * run over every panel - or, without a `panel`, where all of them sit.
@@ -1,7 +1,8 @@
1
- import { nice as niceExtent } from 'd3-array';
1
+ import { ProcessedPositions } from '../transforms/ProcessedPositions';
2
+ import { NiceTicks } from '../../layout/scales/NiceTicks';
2
3
  import { ScalesPhase } from '../scales/ScalesPhase';
3
4
  import { Pipeline } from '../Pipeline';
4
- import { PANEL_KEY, FacetPhase } from '../facet/FacetPhase';
5
+ import { PANEL_KEY, FacetPanels } from '../facet/FacetPanels';
5
6
  import { isFacetWrap } from '../../layout/facet/config';
6
7
  import { MarkerAxisValues } from '../scales/MarkerAxisValues';
7
8
  /** Natural ascending order, as svelteplot sorts a band domain. */
@@ -49,7 +50,7 @@ export class SharedScalesPhase {
49
50
  /** The channel's domain over `members` panels, or every row without them. */
50
51
  const domainOver = (channel, members) => {
51
52
  if (fromPhase.has(channel)) {
52
- const scoped = members ? FacetPhase.narrowToPanels(state, members) : state;
53
+ const scoped = members ? FacetPanels.narrowToPanels(state, members) : state;
53
54
  const scales = this.#kindScales.run(scoped, ctx).scales;
54
55
  const domain = scales?.[channel]?.domain;
55
56
  return domain === undefined ? undefined : { domain };
@@ -59,7 +60,7 @@ export class SharedScalesPhase {
59
60
  const values = members
60
61
  ? members.map((panel) => SharedScalesPhase.buildPanelAxisValues(state, panel, layout))
61
62
  : [SharedScalesPhase.buildPanelAxisValues(state, undefined, layout)];
62
- return SharedScalesPhase.buildSharedAxisScale(values, channel);
63
+ return SharedScalesPhase.buildSharedAxisScale(values, channel, state.scales?.[channel]?.niceTolerance);
63
64
  };
64
65
  const cache = new Map();
65
66
  const scalesOf = (place) => {
@@ -103,7 +104,7 @@ export class SharedScalesPhase {
103
104
  * count derived from each panel's height, which over-rounds short panels and
104
105
  * can disagree between panels of different heights.
105
106
  */
106
- static buildSharedAxisScale(slices, axis) {
107
+ static buildSharedAxisScale(slices, axis, niceTolerance) {
107
108
  const values = slices
108
109
  .flatMap((slice) => slice[axis])
109
110
  .filter((v) => v != null && v !== '' && !(typeof v === 'number' && !Number.isFinite(v)));
@@ -119,7 +120,9 @@ export class SharedScalesPhase {
119
120
  const numbers = slices.some((slice) => slice.zero.includes(axis))
120
121
  ? [...values, 0]
121
122
  : values;
122
- return { domain: niceExtent(Math.min(...numbers), Math.max(...numbers), 10), nice: false };
123
+ const lo = Math.min(...numbers);
124
+ const hi = Math.max(...numbers);
125
+ return { domain: NiceTicks.domain(lo, hi, 10, niceTolerance), nice: false };
123
126
  }
124
127
  if (values.every((v) => v instanceof Date)) {
125
128
  const times = values.map((d) => +d);
@@ -139,7 +142,7 @@ export class SharedScalesPhase {
139
142
  };
140
143
  const markers = (state.markers ?? []).filter((marker) => inPanel(marker[PANEL_KEY]));
141
144
  const values = (axis) => [
142
- ...ScalesPhase.processedValues(processed, axis),
145
+ ...ProcessedPositions.values(processed, axis),
143
146
  ...MarkerAxisValues.of(markers, axis)
144
147
  ];
145
148
  return {
@@ -1,7 +1,6 @@
1
1
  import { AuthorMarkers } from './AuthorMarkers';
2
2
  import { Statistics } from './Statistics';
3
- import { PANEL_KEY, FacetPhase } from '../facet/FacetPhase';
4
- import { TransformsPhase } from '../transforms/TransformsPhase';
3
+ import { PANEL_KEY, FacetPanels } from '../facet/FacetPanels';
5
4
  import { PlotAxes } from '../data/PlotAxes';
6
5
  /** Marker kinds that draw from a channel pair, so they take the plot's `x`/`y` when the author leaves one out. */
7
6
  const POSITIONED_KINDS = new Set(['line', 'area', 'dot']);
@@ -25,7 +24,7 @@ export class StatisticsPhase {
25
24
  run = (state) => {
26
25
  if (!state.panels)
27
26
  return { ...state, markers: this.#resolve(state) };
28
- const markers = state.panels.flatMap((panel) => this.#resolve(FacetPhase.narrowToPanel(state, panel.key)).map((marker) => ({ ...marker, [PANEL_KEY]: panel.key })));
27
+ const markers = state.panels.flatMap((panel) => this.#resolve(FacetPanels.narrowToPanel(state, panel.key)).map((marker) => ({ ...marker, [PANEL_KEY]: panel.key })));
29
28
  return { ...state, markers };
30
29
  };
31
30
  #resolve(state) {
@@ -51,7 +50,7 @@ export class StatisticsPhase {
51
50
  // A series setting no value of its own (a histogram's) measures its processed rows: its bins.
52
51
  const valueAxis = PlotAxes.valueOf(state);
53
52
  const measured = state.processedData && resolvedSeries.some((s) => s[valueAxis] === undefined)
54
- ? TransformsPhase.buildProcessedGroups(state).map((group) => group.series)
53
+ ? (state.processedGroups ?? []).map((group) => group.series)
55
54
  : resolvedSeries;
56
55
  return [
57
56
  ...Statistics.buildForPlot({ series: measured, statistics, colorFor: colorForSeries }),
@@ -2,8 +2,10 @@ import type { AxisPlotState, PhaseStage } from '../../types/phases';
2
2
  type AnyAxisState = AxisPlotState<any, any, any, any>;
3
3
  /**
4
4
  * The "tooltip" phase every axis-based plot shares: how a hovered row reads,
5
- * as `tooltipFormat` - the same default on every plot, {@link TooltipState.default}.
6
- * It runs after the scales, which say which axis holds categories.
5
+ * as `tooltipFormat` - the same default on every plot, {@link TooltipState.default} -
6
+ * and the order its rows read in, as `tooltipSort`: highest value first, by
7
+ * each row's own value (a stacked row's share, not its running total). It
8
+ * runs after the scales, which say which axis holds categories.
7
9
  */
8
10
  export declare class TooltipPhase<TState extends AnyAxisState> implements PhaseStage<TState> {
9
11
  readonly phase: "tooltip";
@@ -1,10 +1,17 @@
1
1
  import { TooltipState } from '../../layout/tooltip/TooltipState.svelte';
2
+ import { ValueLabels } from '../../layout/styles/ValueLabels';
2
3
  /**
3
4
  * The "tooltip" phase every axis-based plot shares: how a hovered row reads,
4
- * as `tooltipFormat` - the same default on every plot, {@link TooltipState.default}.
5
- * It runs after the scales, which say which axis holds categories.
5
+ * as `tooltipFormat` - the same default on every plot, {@link TooltipState.default} -
6
+ * and the order its rows read in, as `tooltipSort`: highest value first, by
7
+ * each row's own value (a stacked row's share, not its running total). It
8
+ * runs after the scales, which say which axis holds categories.
6
9
  */
7
10
  export class TooltipPhase {
8
11
  phase = 'tooltip';
9
- run = (state) => ({ ...state, tooltipFormat: TooltipState.default(state) });
12
+ run = (state) => ({
13
+ ...state,
14
+ tooltipFormat: TooltipState.default(state),
15
+ tooltipSort: TooltipState.descending(ValueLabels.valueOf(state))
16
+ });
10
17
  }
@@ -0,0 +1,30 @@
1
+ import type { AxisChannelName } from '../../types/layout/mapping';
2
+ import type { ProcessedData, ProcessedRow } from '../../types/phases';
3
+ /**
4
+ * Where the "transforms" phase's processed rows sit, read off its output
5
+ * (`state.processedData`) - by whichever channels the transforms left: a
6
+ * row's own value, a stack's `1`-`2` span, a bin's edges. Every later phase
7
+ * reads positions through it instead of telling those channels apart itself.
8
+ */
9
+ export declare class ProcessedPositions {
10
+ /**
11
+ * Where a processed row sits: the top of its value (`y2`, or `y` - `x2`/`x`
12
+ * when `valueAxis` is `x`) and, on the other axis, its own position (or the
13
+ * middle of its `1`-`2` span, a bin's centre).
14
+ */
15
+ static of(processed: ProcessedData, valueAxis?: 'x' | 'y'): {
16
+ x: (row: ProcessedRow) => unknown;
17
+ y: (row: ProcessedRow) => unknown;
18
+ };
19
+ /**
20
+ * A processed row's span along `valueAxis`: from its `1` channel (a stack's
21
+ * bottom) or, with none, from `baseline`, to its top. `undefined` when the
22
+ * row has no value there. `stacked` tells a stack from plain values.
23
+ */
24
+ static span(processed: ProcessedData, valueAxis: 'x' | 'y', baseline?: number): {
25
+ stacked: boolean;
26
+ of: (row: ProcessedRow) => [number, number] | undefined;
27
+ };
28
+ /** Every value the processed rows sit at on one axis, through each of its channels. */
29
+ static values(processed: ProcessedData | undefined, axis: AxisChannelName): unknown[];
30
+ }
@@ -0,0 +1,74 @@
1
+ import { resolveAccessor } from '../../plots/utils/accessors';
2
+ import { isGapValue } from '../../plots/utils/gaps';
3
+ const POSITION_CHANNELS = {
4
+ x: ['x', 'x1', 'x2'],
5
+ y: ['y', 'y1', 'y2']
6
+ };
7
+ /**
8
+ * Where the "transforms" phase's processed rows sit, read off its output
9
+ * (`state.processedData`) - by whichever channels the transforms left: a
10
+ * row's own value, a stack's `1`-`2` span, a bin's edges. Every later phase
11
+ * reads positions through it instead of telling those channels apart itself.
12
+ */
13
+ export class ProcessedPositions {
14
+ /**
15
+ * Where a processed row sits: the top of its value (`y2`, or `y` - `x2`/`x`
16
+ * when `valueAxis` is `x`) and, on the other axis, its own position (or the
17
+ * middle of its `1`-`2` span, a bin's centre).
18
+ */
19
+ static of(processed, valueAxis = 'y') {
20
+ const { channels } = processed;
21
+ const read = (channel) => resolveAccessor(channel);
22
+ const along = (axis) => {
23
+ const own = channels[axis];
24
+ const from = channels[`${axis}1`];
25
+ const to = channels[`${axis}2`];
26
+ if (axis === valueAxis) {
27
+ const top = to ?? own;
28
+ return top !== undefined ? read(top) : () => undefined;
29
+ }
30
+ if (own !== undefined)
31
+ return read(own);
32
+ if (from === undefined || to === undefined)
33
+ return () => undefined;
34
+ return (row) => (Number(read(from)(row)) + Number(read(to)(row))) / 2;
35
+ };
36
+ return { x: along('x'), y: along('y') };
37
+ }
38
+ /**
39
+ * A processed row's span along `valueAxis`: from its `1` channel (a stack's
40
+ * bottom) or, with none, from `baseline`, to its top. `undefined` when the
41
+ * row has no value there. `stacked` tells a stack from plain values.
42
+ */
43
+ static span(processed, valueAxis, baseline = 0) {
44
+ const from = processed.channels[`${valueAxis}1`];
45
+ const stacked = from !== undefined && processed.channels[`${valueAxis}2`] !== undefined;
46
+ const top = ProcessedPositions.of(processed, valueAxis)[valueAxis];
47
+ const bottom = stacked
48
+ ? resolveAccessor(from)
49
+ : () => baseline;
50
+ return {
51
+ stacked,
52
+ of: (row) => {
53
+ const end = top(row);
54
+ const start = bottom(row);
55
+ if (isGapValue(end) || isGapValue(start))
56
+ return undefined;
57
+ const span = [Number(start), Number(end)];
58
+ return Number.isFinite(span[0]) && Number.isFinite(span[1]) ? span : undefined;
59
+ }
60
+ };
61
+ }
62
+ /** Every value the processed rows sit at on one axis, through each of its channels. */
63
+ static values(processed, axis) {
64
+ if (!processed)
65
+ return [];
66
+ return POSITION_CHANNELS[axis].flatMap((name) => {
67
+ const channel = processed.channels[name];
68
+ if (channel === undefined)
69
+ return [];
70
+ const read = resolveAccessor(channel);
71
+ return processed.data.map((row) => read(row));
72
+ });
73
+ }
74
+ }
@@ -1,7 +1,4 @@
1
- import type { Series } from '../../types/plots/data/common';
2
- import type { SegmentsConfig } from '../../types/plots/props';
3
- import type { VisualGroup } from '../../types/plots/segments/config';
4
- import type { PhaseStage, ProcessedData, ProcessedRow, TransformsStageOptions, TransformsStageState } from '../../types/phases';
1
+ import type { PhaseStage, TransformsStageOptions, TransformsStageState } from '../../types/phases';
5
2
  /** The field each processed row carries its series name under, for its color, legend and labels. */
6
3
  export declare const SERIES_KEY = "__series";
7
4
  /** The field each processed row carries its series id under - the transforms' `z`, so a stack or bin keeps series apart. */
@@ -14,7 +11,8 @@ export declare const GAP_KIND = "__gapKind";
14
11
  * The "transforms" phase every axis-based plot shares: runs the plot-level
15
12
  * transforms once over every visible series' rows together, so a stack sees
16
13
  * every series and a bin keeps each series apart (both group by series), and
17
- * leaves the result in `state.processedData` for the plot's own marks. With
14
+ * leaves the result in `state.processedData` for the plot's own marks - and
15
+ * in `state.processedGroups`, split back into the visual groups they draw. With
18
16
  * facet panels, the transforms run once per panel's rows, so a stack never
19
17
  * combines two panels, and every row they return keeps its panel. A gap that
20
18
  * `styles.gaps` fills is read at its interpolated value, so it takes its place
@@ -23,42 +21,7 @@ export declare const GAP_KIND = "__gapKind";
23
21
  export declare class TransformsPhase<TState extends TransformsStageState, TCtx = unknown> implements PhaseStage<TState, TCtx> {
24
22
  #private;
25
23
  readonly phase: "transforms";
26
- constructor(options?: TransformsStageOptions<TState, TCtx>);
24
+ /** `connect`: whether a series' visual segments bridge into each other, as the kind's "data" phase splits them. */
25
+ constructor(options?: TransformsStageOptions<TState, TCtx>, connect?: boolean);
27
26
  run: (state: TState, ctx: TCtx) => TState;
28
- /**
29
- * The processed rows split back into one group per visible series, each cut
30
- * into its styled visual segments by `segments`, matched on the processed `x`.
31
- * Each series reads its rows through its value axis: a stack's top, a bin's start.
32
- */
33
- static buildProcessedGroups<TStyle>(state: TransformsStageState & {
34
- segments?: SegmentsConfig<TStyle>;
35
- }, connect?: boolean): VisualGroup<Series<ProcessedRow>, TStyle>[];
36
- /**
37
- * The processed rows in their own order, split into runs of consecutive rows
38
- * from the same visible series, each cut into its styled visual segments like
39
- * {@link TransformsPhase.buildProcessedGroups}. Drawn in turn, the runs keep the rows' order
40
- * across series, so a transform that reorders rows (`sort`, `reverse`,
41
- * `shuffle`) decides which rows paint on top. Unreordered, each series is one run.
42
- */
43
- static buildProcessedRuns<TStyle>(state: TransformsStageState & {
44
- segments?: SegmentsConfig<TStyle>;
45
- }): VisualGroup<Series<ProcessedRow>, TStyle>[];
46
- /**
47
- * Where a processed row sits: the top of its value (`y2`, or `y` - `x2`/`x`
48
- * when `valueAxis` is `x`) and, on the other axis, its own position (or the
49
- * middle of its `1`-`2` span, a bin's centre).
50
- */
51
- static processedPosition(processed: ProcessedData, valueAxis?: 'x' | 'y'): {
52
- x: (row: ProcessedRow) => unknown;
53
- y: (row: ProcessedRow) => unknown;
54
- };
55
- /**
56
- * A processed row's span along `valueAxis`: from its `1` channel (a stack's
57
- * bottom) or, with none, from `baseline`, to its top. `undefined` when the
58
- * row has no value there. `stacked` tells a stack from plain values.
59
- */
60
- static processedSpan(processed: ProcessedData, valueAxis: 'x' | 'y', baseline?: number): {
61
- stacked: boolean;
62
- of: (row: ProcessedRow) => [number, number] | undefined;
63
- };
64
27
  }
@@ -1,8 +1,7 @@
1
1
  import { Transforms } from './Transforms';
2
2
  import { seriesIdOf } from '../../utils/grouping';
3
- import { PANEL_KEY } from '../facet/FacetPhase';
3
+ import { PANEL_KEY } from '../facet/FacetPanels';
4
4
  import { resolveAccessor } from '../../plots/utils/accessors';
5
- import { isGapValue } from '../../plots/utils/gaps';
6
5
  import { buildGroupedSeries } from '../../plots/utils/segments';
7
6
  import { PlotAxes } from '../data/PlotAxes';
8
7
  /** The field each processed row carries its series name under, for its color, legend and labels. */
@@ -20,7 +19,8 @@ const NOT_OPTIONS = new Set(['data', 'z', 'grouping', 'transforms', 'sort', 'fil
20
19
  * The "transforms" phase every axis-based plot shares: runs the plot-level
21
20
  * transforms once over every visible series' rows together, so a stack sees
22
21
  * every series and a bin keeps each series apart (both group by series), and
23
- * leaves the result in `state.processedData` for the plot's own marks. With
22
+ * leaves the result in `state.processedData` for the plot's own marks - and
23
+ * in `state.processedGroups`, split back into the visual groups they draw. With
24
24
  * facet panels, the transforms run once per panel's rows, so a stack never
25
25
  * combines two panels, and every row they return keeps its panel. A gap that
26
26
  * `styles.gaps` fills is read at its interpolated value, so it takes its place
@@ -29,8 +29,11 @@ const NOT_OPTIONS = new Set(['data', 'z', 'grouping', 'transforms', 'sort', 'fil
29
29
  export class TransformsPhase {
30
30
  phase = 'transforms';
31
31
  #options;
32
- constructor(options = {}) {
32
+ #connect;
33
+ /** `connect`: whether a series' visual segments bridge into each other, as the kind's "data" phase splits them. */
34
+ constructor(options = {}, connect = true) {
33
35
  this.#options = options;
36
+ this.#connect = connect;
34
37
  }
35
38
  run = (state, ctx) => {
36
39
  const options = this.#options;
@@ -62,7 +65,7 @@ export class TransformsPhase {
62
65
  if (!transforms.length) {
63
66
  const channels = Object.fromEntries(Object.entries({ x, y }).filter(([, channel]) => channel !== undefined));
64
67
  const processedData = { data, channels, options: {}, transforms };
65
- return { ...state, processedData };
68
+ return this.#withGroups({ ...state, processedData });
66
69
  }
67
70
  const out = TransformsPhase.#runByPanel(data, (rows) => Transforms.resolve({ data: rows, x, y, grouping: SERIES_ID_KEY, transforms }, rows));
68
71
  const channels = {};
@@ -89,8 +92,15 @@ export class TransformsPhase {
89
92
  options: markOptions,
90
93
  transforms
91
94
  };
92
- return { ...state, processedData };
95
+ return this.#withGroups({ ...state, processedData });
93
96
  };
97
+ /** `state` with its processed rows split into the visual groups the kind's marks draw (`processedGroups`). */
98
+ #withGroups(state) {
99
+ const processedGroups = this.#options.groups === 'runs'
100
+ ? TransformsPhase.#buildRuns(state)
101
+ : TransformsPhase.#buildGroups(state, this.#connect);
102
+ return { ...state, processedGroups };
103
+ }
94
104
  /** What a gap-filled row carries: its interpolated value and the kind of zone that filled it. */
95
105
  static #fillTags(fill) {
96
106
  return fill ? { [GAP_VALUE]: fill.value, [GAP_KIND]: fill.kind } : {};
@@ -100,7 +110,7 @@ export class TransformsPhase {
100
110
  * into its styled visual segments by `segments`, matched on the processed `x`.
101
111
  * Each series reads its rows through its value axis: a stack's top, a bin's start.
102
112
  */
103
- static buildProcessedGroups(state, connect = true) {
113
+ static #buildGroups(state, connect) {
104
114
  const { data } = state.processedData;
105
115
  const series = (state.resolvedSeries ?? []).map((s) => TransformsPhase.#processedSeries(state, s, data.filter((row) => row[SERIES_ID_KEY] === seriesIdOf(s))));
106
116
  return buildGroupedSeries(series, state.segments, connect);
@@ -108,11 +118,11 @@ export class TransformsPhase {
108
118
  /**
109
119
  * The processed rows in their own order, split into runs of consecutive rows
110
120
  * from the same visible series, each cut into its styled visual segments like
111
- * {@link TransformsPhase.buildProcessedGroups}. Drawn in turn, the runs keep the rows' order
121
+ * {@link TransformsPhase.#buildGroups}. Drawn in turn, the runs keep the rows' order
112
122
  * across series, so a transform that reorders rows (`sort`, `reverse`,
113
123
  * `shuffle`) decides which rows paint on top. Unreordered, each series is one run.
114
124
  */
115
- static buildProcessedRuns(state) {
125
+ static #buildRuns(state) {
116
126
  const byId = new Map((state.resolvedSeries ?? []).map((s) => [seriesIdOf(s), s]));
117
127
  const runs = [];
118
128
  for (const row of state.processedData.data) {
@@ -127,54 +137,6 @@ export class TransformsPhase {
127
137
  }
128
138
  return buildGroupedSeries(runs, state.segments, false);
129
139
  }
130
- /**
131
- * Where a processed row sits: the top of its value (`y2`, or `y` - `x2`/`x`
132
- * when `valueAxis` is `x`) and, on the other axis, its own position (or the
133
- * middle of its `1`-`2` span, a bin's centre).
134
- */
135
- static processedPosition(processed, valueAxis = 'y') {
136
- const { channels } = processed;
137
- const read = (channel) => resolveAccessor(channel);
138
- const along = (axis) => {
139
- const own = channels[axis];
140
- const from = channels[`${axis}1`];
141
- const to = channels[`${axis}2`];
142
- if (axis === valueAxis) {
143
- const top = to ?? own;
144
- return top !== undefined ? read(top) : () => undefined;
145
- }
146
- if (own !== undefined)
147
- return read(own);
148
- if (from === undefined || to === undefined)
149
- return () => undefined;
150
- return (row) => (Number(read(from)(row)) + Number(read(to)(row))) / 2;
151
- };
152
- return { x: along('x'), y: along('y') };
153
- }
154
- /**
155
- * A processed row's span along `valueAxis`: from its `1` channel (a stack's
156
- * bottom) or, with none, from `baseline`, to its top. `undefined` when the
157
- * row has no value there. `stacked` tells a stack from plain values.
158
- */
159
- static processedSpan(processed, valueAxis, baseline = 0) {
160
- const from = processed.channels[`${valueAxis}1`];
161
- const stacked = from !== undefined && processed.channels[`${valueAxis}2`] !== undefined;
162
- const top = TransformsPhase.processedPosition(processed, valueAxis)[valueAxis];
163
- const bottom = stacked
164
- ? resolveAccessor(from)
165
- : () => baseline;
166
- return {
167
- stacked,
168
- of: (row) => {
169
- const end = top(row);
170
- const start = bottom(row);
171
- if (isGapValue(end) || isGapValue(start))
172
- return undefined;
173
- const span = [Number(start), Number(end)];
174
- return Number.isFinite(span[0]) && Number.isFinite(span[1]) ? span : undefined;
175
- }
176
- };
177
- }
178
140
  /**
179
141
  * One series accessor shared by every series as-is, so a field key keeps
180
142
  * feeding svelteplot's axis-title inference; a per-series function otherwise.
@@ -234,6 +196,7 @@ export class TransformsPhase {
234
196
  return {
235
197
  name: source.name,
236
198
  ...(source.id !== undefined ? { id: source.id } : {}),
199
+ ...(source.panel !== undefined ? { panel: source.panel } : {}),
237
200
  x: read('x'),
238
201
  y: read('y'),
239
202
  data
@@ -1,4 +1,3 @@
1
- import { TransformsPhase } from '../../phases/transforms/TransformsPhase';
2
1
  import { Transforms } from '../../phases/transforms/Transforms';
3
2
  /**
4
3
  * AreaPlot's "markers" phase: one `area` per visual segment of each series,
@@ -27,7 +26,7 @@ export function buildMarks(state, ctx) {
27
26
  };
28
27
  // The dim channel: each area reads its own series name, looked up in `opacityScale`.
29
28
  const dimByName = (name) => dimmed ? { opacity: () => name, scales: { opacity: opacityScale } } : {};
30
- const areas = TransformsPhase.buildProcessedGroups(state).flatMap((group) => group.visualSegments.map((seg) => Transforms.finish({
29
+ const areas = (state.processedGroups ?? []).flatMap((group) => group.visualSegments.map((seg) => Transforms.finish({
31
30
  type: 'area',
32
31
  series: group.series.name,
33
32
  data: seg.data,
@@ -1,7 +1,7 @@
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
1
2
  import { buildMarks, buildValueLabels, buildHitAreas, buildBarGeometry } from './markers';
2
- import { niceDomain, naturalTickCount } from '../../layout/scales/niceDomain';
3
- import { Margins } from '../../layout/margins/Margins';
4
- import { SERIES_KEY, TransformsPhase } from '../../phases/transforms/TransformsPhase';
3
+ import { NiceTicks } from '../../layout/scales/NiceTicks';
4
+ import { SERIES_KEY } from '../../phases/transforms/TransformsPhase';
5
5
  import { PlotAxes } from '../../phases/data/PlotAxes';
6
6
  /**
7
7
  * The domain a bar plot fits to its own bars on its value axis: from zero to
@@ -9,14 +9,13 @@ import { PlotAxes } from '../../phases/data/PlotAxes';
9
9
  * author pins the axis, and when there is no span to fit.
10
10
  */
11
11
  function fitValueDomain(state) {
12
- const isHorizontal = state.axes?.isHorizontal ?? false;
13
12
  const axis = PlotAxes.valueOf(state);
14
13
  const scale = state.scales[axis];
15
14
  if (scale?.domain != null || scale?.ticks != null || scale?.nice != null)
16
15
  return {};
17
16
  // Every bar's span, as the "transforms" phase left it - its baseline is
18
17
  // unknown until the domain is, so a plain bar reads as reaching from zero.
19
- const { of: spanOf } = TransformsPhase.processedSpan(state.processedData, axis);
18
+ const { of: spanOf } = ProcessedPositions.span(state.processedData, axis);
20
19
  let lo = 0;
21
20
  let hi = 0;
22
21
  for (const row of state.processedData?.data ?? []) {
@@ -28,10 +27,7 @@ function fitValueDomain(state) {
28
27
  }
29
28
  if (hi <= lo)
30
29
  return {};
31
- const spacing = isHorizontal ? 80 : 50;
32
- const box = Margins.AXIS_ESTIMATE.contentBox(state.width, state.height);
33
- const count = naturalTickCount(isHorizontal ? box.width : box.height, spacing, 2);
34
- return { [axis]: { domain: niceDomain(lo, hi, count) } };
30
+ return { [axis]: { domain: NiceTicks.fit(state, axis, lo, hi, scale?.niceTolerance) } };
35
31
  }
36
32
  /**
37
33
  * BarPlot on the shared pipeline: discrete bars with gap fills along one
@@ -59,7 +55,7 @@ export function buildBarDefaults() {
59
55
  if (labels.anchor !== 'outside')
60
56
  return [];
61
57
  const axis = PlotAxes.valueOf(state);
62
- if (TransformsPhase.processedSpan(state.processedData, axis).stacked)
58
+ if (ProcessedPositions.span(state.processedData, axis).stacked)
63
59
  return [];
64
60
  if (!state.axes?.isHorizontal) {
65
61
  const at = { y: -labels.offset('y') };
@@ -1,5 +1,6 @@
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
1
2
  import { getConfiguration } from '../../configuration/config.svelte';
2
- import { GAP_KIND, GAP_VALUE, SERIES_ID_KEY, TransformsPhase } from '../../phases/transforms/TransformsPhase';
3
+ import { GAP_KIND, GAP_VALUE, SERIES_ID_KEY } from '../../phases/transforms/TransformsPhase';
3
4
  import { seriesIdOf } from '../../utils/grouping';
4
5
  import { resolveGapAreaStyle } from '../utils/gaps';
5
6
  import { PlotAxes } from '../../phases/data/PlotAxes';
@@ -24,8 +25,8 @@ export function buildBarGeometry(state, seriesPadding) {
24
25
  const [min, max] = [Math.min(lo, hi), Math.max(lo, hi)];
25
26
  return min > 0 ? min : max < 0 ? max : 0;
26
27
  })();
27
- const { stacked, of: spanOf } = TransformsPhase.processedSpan(state.processedData, valueAxis, baseline);
28
- const position = TransformsPhase.processedPosition(state.processedData, valueAxis);
28
+ const { stacked, of: spanOf } = ProcessedPositions.span(state.processedData, valueAxis, baseline);
29
+ const position = ProcessedPositions.of(state.processedData, valueAxis);
29
30
  const categoryOf = (row) => position[categoryAxis](row);
30
31
  const names = [...new Set((state.groupedSeries ?? []).map((g) => g.series.name))];
31
32
  // A lone series, or a stack, fills its whole band: there's nothing to set side by side.
@@ -197,7 +198,7 @@ export function buildMarks(state, ctx) {
197
198
  }
198
199
  const isReal = (row) => row[GAP_VALUE] === undefined && geometry.spanOf(row) !== undefined;
199
200
  const segmentMarkers = [];
200
- TransformsPhase.buildProcessedGroups(state, false).forEach((group) => {
201
+ (state.processedGroups ?? []).forEach((group) => {
201
202
  const name = group.series.name;
202
203
  const slice = geometry.sliceOf(name);
203
204
  const defaultColor = colorForSeries(name);
@@ -0,0 +1,19 @@
1
+ import type { HoverPosition } from '../../types/phases';
2
+ import type { BoxPlotState, BoxSummaryRow } from '../../types/plots/box/state';
3
+ /**
4
+ * The boxes a box plot draws, one summary row per series, category and facet
5
+ * panel - the same quartiles and whiskers svelteplot's `BoxX`/`BoxY` compute,
6
+ * so its hover and tooltip read a box rather than every value inside it.
7
+ */
8
+ export declare class BoxSummaries {
9
+ /** One summary per box, in the order the processed rows first reach it. */
10
+ static of<TData extends Record<string, unknown>>(state: BoxPlotState<TData>): BoxSummaryRow[];
11
+ /** Where a box's summary sits for hover: at its category, level with its median. */
12
+ static position<TData extends Record<string, unknown>>(state: BoxPlotState<TData>): HoverPosition;
13
+ /**
14
+ * How a hovered box reads by default: its series, when the plot groups its
15
+ * rows; its category; then its five numbers, highest first, and how many
16
+ * values fall past the whiskers.
17
+ */
18
+ static format<TData extends Record<string, unknown>>(state: BoxPlotState<TData>): (row: BoxSummaryRow) => string;
19
+ }