@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
@@ -0,0 +1,101 @@
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
2
+ import { quantile } from 'd3-array';
3
+ import { SERIES_ID_KEY, SERIES_KEY } from '../../phases/transforms/TransformsPhase';
4
+ import { PANEL_KEY } from '../../phases/facet/FacetPanels';
5
+ import { PlotAxes } from '../../phases/data/PlotAxes';
6
+ import { TooltipState } from '../../layout/tooltip/TooltipState.svelte';
7
+ import { isGapValue } from '../utils/gaps';
8
+ /** The whiskers reach this many interquartile ranges past the box, as svelteplot draws them. */
9
+ const WHISKER_IQR = 1.5;
10
+ /** A summary value as a tooltip shows it: at most two decimals. */
11
+ const round = (value) => String(Math.round(value * 100) / 100);
12
+ /**
13
+ * The boxes a box plot draws, one summary row per series, category and facet
14
+ * panel - the same quartiles and whiskers svelteplot's `BoxX`/`BoxY` compute,
15
+ * so its hover and tooltip read a box rather than every value inside it.
16
+ */
17
+ export class BoxSummaries {
18
+ /** One summary per box, in the order the processed rows first reach it. */
19
+ static of(state) {
20
+ const processed = state.processedData;
21
+ if (!processed?.data.length)
22
+ return [];
23
+ const valueAxis = PlotAxes.valueOf(state);
24
+ const categoryAxis = PlotAxes.categoryOf(state);
25
+ const position = ProcessedPositions.of(processed, valueAxis);
26
+ const valueOf = position[valueAxis];
27
+ const categoryOf = position[categoryAxis];
28
+ const categoryField = processed.channels[categoryAxis];
29
+ const boxes = new Map();
30
+ for (const row of processed.data) {
31
+ const value = valueOf(row);
32
+ if (isGapValue(value))
33
+ continue;
34
+ const n = Number(value);
35
+ if (!Number.isFinite(n))
36
+ continue;
37
+ const key = JSON.stringify([
38
+ row[SERIES_ID_KEY],
39
+ row[PANEL_KEY] ?? null,
40
+ String(categoryOf(row))
41
+ ]);
42
+ const box = boxes.get(key);
43
+ if (box)
44
+ box.values.push(n);
45
+ else
46
+ boxes.set(key, { first: row, values: [n] });
47
+ }
48
+ return [...boxes.values()].map(({ first, values }) => {
49
+ const sorted = values.sort((a, b) => a - b);
50
+ const p25 = quantile(sorted, 0.25);
51
+ const median = quantile(sorted, 0.5);
52
+ const p75 = quantile(sorted, 0.75);
53
+ const reach = (p75 - p25) * WHISKER_IQR;
54
+ const inside = sorted.filter((v) => v >= p25 - reach && v <= p75 + reach);
55
+ const category = categoryOf(first);
56
+ return {
57
+ ...(typeof categoryField === 'string' ? { [categoryField]: category } : {}),
58
+ [SERIES_KEY]: first[SERIES_KEY],
59
+ [SERIES_ID_KEY]: first[SERIES_ID_KEY],
60
+ ...(first[PANEL_KEY] !== undefined ? { [PANEL_KEY]: first[PANEL_KEY] } : {}),
61
+ category,
62
+ min: inside[0] ?? p25,
63
+ p25,
64
+ median,
65
+ p75,
66
+ max: inside.at(-1) ?? p75,
67
+ count: sorted.length,
68
+ outliers: sorted.filter((v) => v < p25 - reach || v > p75 + reach)
69
+ };
70
+ });
71
+ }
72
+ /** Where a box's summary sits for hover: at its category, level with its median. */
73
+ static position(state) {
74
+ const valueAxis = PlotAxes.valueOf(state);
75
+ const at = {
76
+ [valueAxis]: (row) => row.median,
77
+ [PlotAxes.other(valueAxis)]: (row) => row.category
78
+ };
79
+ return at;
80
+ }
81
+ /**
82
+ * How a hovered box reads by default: its series, when the plot groups its
83
+ * rows; its category; then its five numbers, highest first, and how many
84
+ * values fall past the whiskers.
85
+ */
86
+ static format(state) {
87
+ const grouped = state.channels?.grouping !== undefined;
88
+ return (row) => {
89
+ const lines = grouped ? [String(row[SERIES_KEY])] : [];
90
+ lines.push(TooltipState.value(row.category));
91
+ lines.push(`max: ${round(row.max)}`);
92
+ lines.push(`p75: ${round(row.p75)}`);
93
+ lines.push(`median: ${round(row.median)}`);
94
+ lines.push(`p25: ${round(row.p25)}`);
95
+ lines.push(`min: ${round(row.min)}`);
96
+ if (row.outliers.length)
97
+ lines.push(`outliers: ${row.outliers.length}`);
98
+ return lines.join('\n');
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,42 @@
1
+ <script lang="ts" generics="TData extends Record<string, unknown>">
2
+ import Plot from '../../layout/plot/Plot.svelte';
3
+ import { buildBoxDefaults } from './defaults';
4
+ import type { BoxPlotProps, BoxPlotCoreProps } from '../../types/plots/box/props';
5
+
6
+ /**
7
+ * A box plot: the distribution of each series' values per category - quartiles, whiskers and outliers - one box per category, upright, or sideways when the categories run along `y`. Draws from pre-built `series` or flat `data` rows.
8
+ * Optional chrome (`title`, `subtitle`, `caption`, `header`, `footer`,
9
+ * `legend`) frames it; `timeline` and `facet` slice its `data` rows.
10
+ * `margins` is bindable: it reports the margin the plot settled on.
11
+ */
12
+ let {
13
+ title,
14
+ subtitle,
15
+ caption,
16
+ header,
17
+ footer,
18
+ legend,
19
+ order,
20
+ timeline,
21
+ facet,
22
+ tooltip,
23
+ margins = $bindable(),
24
+ ...core
25
+ }: BoxPlotProps<TData> = $props();
26
+ </script>
27
+
28
+ <Plot
29
+ core={core as BoxPlotCoreProps<TData>}
30
+ defaults={buildBoxDefaults}
31
+ bind:margins
32
+ {title}
33
+ {subtitle}
34
+ {caption}
35
+ {header}
36
+ {footer}
37
+ {legend}
38
+ {order}
39
+ {timeline}
40
+ {facet}
41
+ {tooltip}
42
+ />
@@ -0,0 +1,26 @@
1
+ import Plot from '../../layout/plot/Plot.svelte';
2
+ import type { BoxPlotProps } from '../../types/plots/box/props';
3
+ declare function $$render<TData extends Record<string, unknown>>(): {
4
+ props: BoxPlotProps<TData>;
5
+ exports: {};
6
+ bindings: "margins";
7
+ slots: {};
8
+ events: {};
9
+ };
10
+ declare class __sveltets_Render<TData extends Record<string, unknown>> {
11
+ props(): ReturnType<typeof $$render<TData>>['props'];
12
+ events(): ReturnType<typeof $$render<TData>>['events'];
13
+ slots(): ReturnType<typeof $$render<TData>>['slots'];
14
+ bindings(): "margins";
15
+ exports(): {};
16
+ }
17
+ interface $$IsomorphicComponent {
18
+ new <TData extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
19
+ $$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
20
+ } & ReturnType<__sveltets_Render<TData>['exports']>;
21
+ <TData extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
22
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
23
+ }
24
+ declare const Plot: $$IsomorphicComponent;
25
+ type Plot<TData extends Record<string, unknown>> = InstanceType<typeof Plot<TData>>;
26
+ export default Plot;
@@ -0,0 +1,12 @@
1
+ import type { AxisPlotDefaults } from '../../types/phases';
2
+ import type { BoxPlotState, BoxPlotCtx } from '../../types/plots/box/state';
3
+ /**
4
+ * BoxPlot on the shared pipeline: one box per category of each series along
5
+ * one category axis - `x`, or `y` for horizontal boxes, as the "data" phase
6
+ * reads it off the values, in the order the data gives them - with its own
7
+ * band padding and markers. The value
8
+ * axis fits the values, without reaching for zero. Its plot defaults to:
9
+ * hover along the category axis, over one summary per box rather than every
10
+ * value inside it.
11
+ */
12
+ export declare function buildBoxDefaults<TData extends Record<string, unknown>>(): AxisPlotDefaults<BoxPlotState<TData>, BoxPlotCtx>;
@@ -0,0 +1,34 @@
1
+ import { buildMarks } from './markers';
2
+ import { BoxSummaries } from './BoxSummaries';
3
+ import { TooltipState } from '../../layout/tooltip/TooltipState.svelte';
4
+ /**
5
+ * BoxPlot on the shared pipeline: one box per category of each series along
6
+ * one category axis - `x`, or `y` for horizontal boxes, as the "data" phase
7
+ * reads it off the values, in the order the data gives them - with its own
8
+ * band padding and markers. The value
9
+ * axis fits the values, without reaching for zero. Its plot defaults to:
10
+ * hover along the category axis, over one summary per box rather than every
11
+ * value inside it.
12
+ */
13
+ export function buildBoxDefaults() {
14
+ return {
15
+ data: {
16
+ discreteAxes: 1,
17
+ segments: { color: (style) => style?.fill, connect: false }
18
+ },
19
+ scales: {
20
+ bandOrder: 'data',
21
+ defaults: (_state, ctx) => ({
22
+ category: { paddingInner: ctx.cfg.box.paddingInner, paddingOuter: ctx.cfg.box.paddingOuter }
23
+ })
24
+ },
25
+ markers: { marks: buildMarks },
26
+ hover: {
27
+ rows: BoxSummaries.of,
28
+ position: BoxSummaries.position,
29
+ label: () => ((row) => String(row.median)),
30
+ format: (state) => BoxSummaries.format(state),
31
+ sort: () => TooltipState.descending((row) => row.median)
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,12 @@
1
+ import type { BoxPlotState, BoxPlotCtx } from '../../types/plots/box/state';
2
+ import type { AxisBasedMarkersConfig } from '../../types/markers/common';
3
+ /**
4
+ * BoxPlot's "markers" phase: one `box` per visual segment of each series,
5
+ * one box per category of its rows, filled with its series' color (or its
6
+ * segment's own `fill`). The markers the "statistics" phase already put in
7
+ * `state.markers` come after, so they render on top.
8
+ *
9
+ * A series toggled off in the legend (style mode), or dimmed by hovering
10
+ * another one, takes the legend's disabled fill.
11
+ */
12
+ export declare function buildMarks<TData extends Record<string, unknown>>(state: BoxPlotState<TData>, ctx: BoxPlotCtx): AxisBasedMarkersConfig[];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * BoxPlot's "markers" phase: one `box` per visual segment of each series,
3
+ * one box per category of its rows, filled with its series' color (or its
4
+ * segment's own `fill`). The markers the "statistics" phase already put in
5
+ * `state.markers` come after, so they render on top.
6
+ *
7
+ * A series toggled off in the legend (style mode), or dimmed by hovering
8
+ * another one, takes the legend's disabled fill.
9
+ */
10
+ export function buildMarks(state, ctx) {
11
+ const { channels } = state.processedData;
12
+ const { cfg } = ctx;
13
+ return (state.processedGroups ?? []).flatMap((group) => {
14
+ const name = group.series.name;
15
+ const disabled = ctx.interaction?.fillFor(name);
16
+ return group.visualSegments.map((seg) => {
17
+ const { stroke, strokeWidth, strokeOpacity, borderRadius } = seg.styles;
18
+ const outline = { stroke, strokeWidth, strokeOpacity, borderRadius };
19
+ return {
20
+ type: 'box',
21
+ series: name,
22
+ data: seg.data,
23
+ x: channels.x,
24
+ y: channels.y,
25
+ bar: {
26
+ // The box's own outline, from its segment; unset fields stay svelteplot's.
27
+ ...Object.fromEntries(Object.entries(outline).filter(([, v]) => v !== undefined)),
28
+ fill: disabled?.fill ?? seg.styles.fill ?? state.colorForSeries(name),
29
+ fillOpacity: disabled?.fillOpacity ?? seg.styles.fillOpacity ?? cfg.box.fillOpacity
30
+ }
31
+ };
32
+ });
33
+ });
34
+ }
@@ -1,4 +1,5 @@
1
- import { HoverPhase } from '../../../phases/hover/HoverPhase';
1
+ import { LegendState } from '../../../layout/legend/LegendState.svelte';
2
+ import { HoverSettings } from '../../../layout/hover/HoverSettings';
2
3
  /** Whether a pointer event is over an `inset` marker's panel, which has its own hover. */
3
4
  export function isOverInset(e) {
4
5
  return e.target?.closest?.('.dct-geo-inset') != null;
@@ -14,10 +15,10 @@ export function buildHover(state, ctx) {
14
15
  const features = state.features;
15
16
  const label = (f) => f.__id;
16
17
  const points = features
17
- .filter((f) => !range || !color || HoverPhase.inRange(color(f), range))
18
+ .filter((f) => !range || !color || LegendState.inRange(color(f), range))
18
19
  .map((f) => ({ x: f.__cx, y: f.__cy, row: f, label: label(f) }));
19
20
  const hover = {
20
- ...HoverPhase.buildHoverConfig({
21
+ ...HoverSettings.buildConfig({
21
22
  tooltip: tooltip,
22
23
  format: state.tooltipFormat,
23
24
  markers: authoredMarkers,
@@ -27,8 +28,7 @@ export function buildHover(state, ctx) {
27
28
  // Match the feature under the cursor, not the nearest centroid.
28
29
  pointMatching: 'contains'
29
30
  }),
30
- // A map has no axis to draw a crosshair rule against - 'punctual' here
31
- // only means "exact XY hover match", unlike heatmap's own use of it.
31
+ // A map has no axes for a crosshair: 'punctual' only means an exact match.
32
32
  strategy: 'punctual',
33
33
  crosshair: false,
34
34
  exclude: isOverInset
@@ -1,4 +1,4 @@
1
- import { HoverPhase } from '../../../phases/hover/HoverPhase';
1
+ import { LegendState } from '../../../layout/legend/LegendState.svelte';
2
2
  import { resolveFeatureStyle } from '../../utils/geoSegments';
3
3
  import { DisabledStyle } from '../../../layout/legend/DisabledStyle';
4
4
  /**
@@ -24,8 +24,7 @@ export function buildMarkers(state, ctx) {
24
24
  function rangeFilteredStyle(base, f) {
25
25
  if (range == null || !color)
26
26
  return base;
27
- const v = color(f);
28
- if (v == null || (v >= range.min && v <= range.max))
27
+ if (LegendState.inRange(color(f), range))
29
28
  return base;
30
29
  return { ...base, ...DisabledStyle.fill(range.disabledStyle) };
31
30
  }
@@ -51,7 +50,7 @@ export function buildMarkers(state, ctx) {
51
50
  }
52
51
  // What the pointer can match: the features inside the legend's range, by id.
53
52
  const hoverable = new Map(features
54
- .filter((f) => !range || !color || HoverPhase.inRange(color(f), range))
53
+ .filter((f) => !range || !color || LegendState.inRange(color(f), range))
55
54
  .map((f) => [f.__id, f]));
56
55
  const featureMarkers = styleGroups.map((group) => ({
57
56
  type: 'geo',
@@ -1,3 +1,4 @@
1
+ import { TimelineTime } from '../../layout/timeline/TimelineTime';
1
2
  import { isFacetWrap } from '../../layout/facet/config';
2
3
  const SLICE = Symbol('dct-geo-slice');
3
4
  /** A slicing field's value on a feature (from its `properties`) or on a marker's own row. */
@@ -32,9 +33,11 @@ export function buildGeoSlicing(args) {
32
33
  : readField(fields[dimension], row);
33
34
  let combos = [{}];
34
35
  for (const dimension of Object.keys(fields)) {
35
- const values = [
36
- ...new Set(rows.map((row) => readField(fields[dimension], row)).filter((v) => v != null))
37
- ];
36
+ const read = rows.map((row) => readField(fields[dimension], row)).filter((v) => v != null);
37
+ // Dates on the same step are one value.
38
+ const time = new TimelineTime(timeline?.dateFormat);
39
+ const keyOf = (value) => dimension === 'time' ? String(time.toTime(value) ?? value) : value;
40
+ const values = [...new Map(read.map((value) => [keyOf(value), value])).values()];
38
41
  combos = combos.flatMap((combo) => values.map((value) => ({ ...combo, [dimension]: value })));
39
42
  }
40
43
  return {
@@ -1,3 +1,4 @@
1
+ import { LegendState } from '../../layout/legend/LegendState.svelte';
1
2
  import { DisabledStyle } from '../../layout/legend/DisabledStyle';
2
3
  import { resolveAccessor } from '../utils/accessors';
3
4
  /** A processed channel as a per-row function. */
@@ -98,7 +99,7 @@ export function buildMarks(state, ctx) {
98
99
  return map;
99
100
  })();
100
101
  function rangeFilteredStyle(base, value) {
101
- if (range == null || (value >= range.min && value <= range.max))
102
+ if (range == null || LegendState.inRange(value, range))
102
103
  return base;
103
104
  return { ...base, ...DisabledStyle.fill(range.disabledStyle) };
104
105
  }
@@ -32,7 +32,7 @@ function mergeBin(own, cfg, horizontal) {
32
32
  */
33
33
  export function buildHistogramDefaults() {
34
34
  return {
35
- data: { segments: { color: (style) => style?.fill } },
35
+ data: { segments: { color: (style) => style?.fill, connect: false } },
36
36
  transforms: {
37
37
  transforms: (state, ctx) => mergeBin(state.transforms ?? [], ctx.cfg, state.axes?.isHorizontal ?? false)
38
38
  },
@@ -1,4 +1,5 @@
1
- import { SERIES_KEY, TransformsPhase } from '../../phases/transforms/TransformsPhase';
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
2
+ import { SERIES_KEY } from '../../phases/transforms/TransformsPhase';
2
3
  import { Transforms } from '../../phases/transforms/Transforms';
3
4
  import { resolveAccessor } from '../utils/accessors';
4
5
  import { PlotAxes } from '../../phases/data/PlotAxes';
@@ -18,7 +19,7 @@ export function buildValueLabels(labels, state, ctx) {
18
19
  const value = PlotAxes.valueOf(state);
19
20
  const binned = PlotAxes.other(value);
20
21
  const horizontal = value === 'x';
21
- const position = TransformsPhase.processedPosition(processed, value);
22
+ const position = ProcessedPositions.of(processed, value);
22
23
  const read = (channel) => channel === undefined
23
24
  ? undefined
24
25
  : resolveAccessor(channel);
@@ -69,8 +70,7 @@ export function buildValueLabels(labels, state, ctx) {
69
70
  export function buildMarks(state, ctx) {
70
71
  const { channels, options, transforms } = state.processedData;
71
72
  const fillOpacity = ctx.cfg.histogram.fillOpacity;
72
- // Bins are discrete, so adjacent segments share no boundary bar to bridge.
73
- const rects = TransformsPhase.buildProcessedGroups(state, false).flatMap((group) => {
73
+ const rects = (state.processedGroups ?? []).flatMap((group) => {
74
74
  const disabled = ctx.interaction?.fillFor(group.series.name);
75
75
  return group.visualSegments.map((seg) => Transforms.finish({
76
76
  type: 'rect',
@@ -1,5 +1,4 @@
1
1
  import { getConfiguration } from '../../configuration/config.svelte';
2
- import { TransformsPhase } from '../../phases/transforms/TransformsPhase';
3
2
  import { resolveGapRuns, resolveGapsForSeries } from '../utils/gaps';
4
3
  /**
5
4
  * Every `'line'` (+ `'dot'`, where a segment's style asks for them) marker
@@ -182,7 +181,7 @@ export function buildMarks(state, ctx) {
182
181
  const { colorForSeries, gaps } = state;
183
182
  const disabledStrokeFor = (name) => ctx.interaction?.strokeFor(name);
184
183
  const disabledDotsFor = (name) => ctx.interaction?.dotsFor(name);
185
- const groups = TransformsPhase.buildProcessedGroups(state);
184
+ const groups = state.processedGroups ?? [];
186
185
  const lineMarkers = buildLineMarkers({
187
186
  groupedSeries: groups,
188
187
  colorFor: colorForSeries,
@@ -1,9 +1,10 @@
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
1
2
  import { PlotAxes } from '../../phases/data/PlotAxes';
2
3
  import { buildMarks, buildValueLabels } from './markers';
3
4
  import { ValueLabels } from '../../layout/styles/ValueLabels';
4
5
  import { seriesIdOf } from '../../utils/grouping';
5
6
  import { isGapValue } from '../utils/gaps';
6
- import { SERIES_ID_KEY, TransformsPhase } from '../../phases/transforms/TransformsPhase';
7
+ import { SERIES_ID_KEY } from '../../phases/transforms/TransformsPhase';
7
8
  const byName = (a, b) => a.name.localeCompare(b.name);
8
9
  /**
9
10
  * PyramidPlot on the shared pipeline. Its `x`/`y` are the visual axes: the one
@@ -64,7 +65,7 @@ export function buildPyramidDefaults() {
64
65
  // side - and its category, and reads as its magnitude.
65
66
  position: (state) => {
66
67
  const value = PlotAxes.valueOf(state);
67
- const place = TransformsPhase.processedPosition(state.processedData, value);
68
+ const place = ProcessedPositions.of(state.processedData, value);
68
69
  const valueOf = ValueLabels.valueOf(state);
69
70
  const sideOf = new Map((state.resolvedSeries ?? []).map((s) => [seriesIdOf(s), s.side]));
70
71
  const signed = (row) => {
@@ -1,5 +1,5 @@
1
1
  import { resolveAccessor } from '../utils/accessors';
2
- import { SERIES_ID_KEY, TransformsPhase } from '../../phases/transforms/TransformsPhase';
2
+ import { SERIES_ID_KEY } from '../../phases/transforms/TransformsPhase';
3
3
  import { seriesIdOf } from '../../utils/grouping';
4
4
  import { PlotAxes } from '../../phases/data/PlotAxes';
5
5
  import { getConfiguration } from '../../configuration/config.svelte';
@@ -23,7 +23,7 @@ export function buildValueLabels(labels, state, ctx) {
23
23
  const categoryAxis = PlotAxes.categoryOf(state);
24
24
  const valueAxis = PlotAxes.valueOf(state);
25
25
  const isHorizontal = state.axes?.isHorizontal ?? true;
26
- return TransformsPhase.buildProcessedGroups(state, false).flatMap(({ series }) => {
26
+ return (state.processedGroups ?? []).flatMap(({ series }) => {
27
27
  const id = seriesIdOf(series);
28
28
  const side = state.resolvedSeries.find((s) => seriesIdOf(s) === id).side;
29
29
  const isLeft = side === 'left';
@@ -93,10 +93,7 @@ export function buildMarks(state, ctx) {
93
93
  [`${valueAxis}2`]: to
94
94
  });
95
95
  // The real bars draw the rows the "transforms" phase left, read through its channels.
96
- const processedGroups = new Map(TransformsPhase.buildProcessedGroups(state, false).map((g) => [
97
- seriesIdOf(g.series),
98
- g
99
- ]));
96
+ const processedGroups = new Map((state.processedGroups ?? []).map((g) => [seriesIdOf(g.series), g]));
100
97
  const markers = [];
101
98
  for (const group of groupedSeries) {
102
99
  // Forwarded raw (a field key or function, whatever the series' category is)
@@ -1,5 +1,5 @@
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
1
2
  import { buildMarks, buildValueLabels } from './markers';
2
- import { TransformsPhase } from '../../phases/transforms/TransformsPhase';
3
3
  import { Margins } from '../../layout/margins/Margins';
4
4
  import { MarkerAxisValues } from '../../phases/scales/MarkerAxisValues';
5
5
  /** Stand-in max render radius for a channel-bound `r` (svelteplot's own auto sqrt scale). */
@@ -28,7 +28,7 @@ function paddedDomain(values, radiusPx, sizePx) {
28
28
  function fitDomains(state, ctx) {
29
29
  const { scales, channels, groupedSeries } = state;
30
30
  const processed = state.processedData;
31
- const position = TransformsPhase.processedPosition(processed);
31
+ const position = ProcessedPositions.of(processed);
32
32
  const extra = {
33
33
  x: MarkerAxisValues.of(state.markers ?? [], 'x'),
34
34
  y: MarkerAxisValues.of(state.markers ?? [], 'y')
@@ -66,6 +66,7 @@ export function buildScatterDefaults() {
66
66
  connect: false
67
67
  }
68
68
  },
69
+ transforms: { groups: 'runs' },
69
70
  scales: { fit: fitDomains },
70
71
  markers: { marks: buildMarks, valueLabels: buildValueLabels },
71
72
  hover: {
@@ -1,4 +1,5 @@
1
- import { SERIES_KEY, TransformsPhase } from '../../phases/transforms/TransformsPhase';
1
+ import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
2
+ import { SERIES_KEY } from '../../phases/transforms/TransformsPhase';
2
3
  import { ValueLabels } from '../../layout/styles/ValueLabels';
3
4
  /**
4
5
  * ScatterPlot's `styles.values` as one `'text'` marker over the same
@@ -11,7 +12,7 @@ export function buildValueLabels(labels, state, ctx) {
11
12
  if (!processedData?.data.length)
12
13
  return [];
13
14
  const { font, format } = labels;
14
- const position = TransformsPhase.processedPosition(processedData);
15
+ const position = ProcessedPositions.of(processedData);
15
16
  const nameOf = (d) => String(d[SERIES_KEY]);
16
17
  const valueOf = ValueLabels.valueOf(state);
17
18
  return [
@@ -89,7 +90,7 @@ export function buildMarks(state, ctx) {
89
90
  }
90
91
  : undefined;
91
92
  const markers = [];
92
- TransformsPhase.buildProcessedRuns(state).forEach((group) => {
93
+ (state.processedGroups ?? []).forEach((group) => {
93
94
  const defaultColor = colorForSeries(group.series.name);
94
95
  const disabledDots = interaction?.dotsFor(group.series.name);
95
96
  for (const seg of group.visualSegments) {
@@ -1,6 +1,7 @@
1
1
  import type { StrokeStyle, DotStyle, FontStyle, AreaStyle } from '../plots/styling';
2
- import type { SymbolType } from '../plots/constants';
2
+ import type { LineAnchor, SymbolType, TextAnchor } from '../plots/constants';
3
3
  import type { PlotChromeEntry } from '../plots/props';
4
+ import type { EdgeTicksOptions } from '../layout/scales';
4
5
  export type TextStyle = {
5
6
  /** font-family */
6
7
  font: string;
@@ -139,8 +140,11 @@ export type PlotStylesConfig = {
139
140
  yTickPadding: number;
140
141
  grid: boolean;
141
142
  locale: string;
142
- /** Compact units abbreviated (`'short'`, the default: `1 M`) or spelled out (`'long'`: `1 millón`). Axes and size legends. */
143
143
  compactDisplay: 'short' | 'long';
144
+ edgeTicks: {
145
+ x: EdgeTicksOptions;
146
+ y: EdgeTicksOptions;
147
+ };
144
148
  };
145
149
  tooltip: {
146
150
  background: string;
@@ -239,6 +243,15 @@ export type PlotStylesConfig = {
239
243
  /** Fill opacity of every bar, so a grouped histogram's bars behind another group's still show. */
240
244
  fillOpacity: number;
241
245
  };
246
+ /** Default box-plot styling. */
247
+ box: {
248
+ /** Fill opacity of every box that sets none of its own. */
249
+ fillOpacity: number;
250
+ /** Fallback gap (0-1) between the boxes of neighbouring categories, when a plot doesn't set its own `scales.x/y.paddingInner`. */
251
+ paddingInner: number;
252
+ /** Fallback gap (0-1) before the first and after the last box, when a plot doesn't set its own `scales.x/y.paddingOuter`. */
253
+ paddingOuter: number;
254
+ };
242
255
  /** Default scatter point geometry. */
243
256
  scatter: {
244
257
  dotRadius: number;
@@ -324,6 +337,15 @@ export type PlotStylesConfig = {
324
337
  font: FontStyle;
325
338
  area: AreaStyle;
326
339
  };
340
+ /** The `delta` marker's label offset and anchor, per axis the delta is taken on. A marker's own `text` wins. */
341
+ delta: {
342
+ text: Record<'x' | 'y', {
343
+ dx: number;
344
+ dy: number;
345
+ lineAnchor: LineAnchor;
346
+ textAnchor: TextAnchor;
347
+ }>;
348
+ };
327
349
  facet: {
328
350
  /** Position badge shown in `carousel`/`paginated` facet modes. */
329
351
  indicator: {
@@ -1,5 +1,5 @@
1
1
  import type { HoverStrategy } from './hover';
2
- import type { Orientation, PlayerConfig, TimeValue, TimelineCallbacks, TimelineType } from './timeline';
2
+ import type { Orientation, PlayerConfig, TimeInput, TimelineCallbacks, TimelineType } from './timeline';
3
3
  /**
4
4
  * How a `PlotComposer` arranges its panels: `areas` names the grid cells
5
5
  * (one string per row, `.` for empty), else panels flow into `columns`;
@@ -20,7 +20,10 @@ export type ComposerTags = 'A' | 'a' | '1' | 'I' | 'i' | string[];
20
20
  /** One timeline for every panel: plots with a `timeline` follow its `progress` over `values`. */
21
21
  export type ComposerTimelineConfig = TimelineCallbacks & {
22
22
  progress?: number;
23
- values: TimeValue[];
23
+ /** The steps: numbers, `Date`s or date strings. */
24
+ values: TimeInput[];
25
+ /** How a non-ISO date string in `values` reads. */
26
+ dateFormat?: string;
24
27
  /** How it draws. Defaults to `'player'`. */
25
28
  type?: TimelineType;
26
29
  /** The player bar's own options, when `type` is `'player'`. */
@@ -76,6 +76,18 @@ export type DivergingScaleType = (typeof DIVERGING_SCALE_TYPES)[number];
76
76
  /** Scale types that bucket a numeric domain into a fixed set of output values instead of interpolating. */
77
77
  export declare const BINNED_SCALE_TYPES: readonly ["threshold", "quantile", "quantize"];
78
78
  export type BinnedScaleType = (typeof BINNED_SCALE_TYPES)[number];
79
+ /**
80
+ * What an axis does when the tick at the data's first or last value would
81
+ * crowd the nice tick next to it: `'drop-nice'` keeps the data's tick and
82
+ * drops the nice one, `'drop-edge'` keeps the nice tick and drops the data's,
83
+ * `'extend'` widens the axis to the next nice tick past the data.
84
+ */
85
+ export type EdgeTickCollision = 'drop-nice' | 'drop-edge' | 'extend';
86
+ /** Whether an axis adds ticks at the data's first and last values, and what it does when one crowds a nice tick. */
87
+ export type EdgeTicksOptions = {
88
+ enabled: boolean;
89
+ collision: EdgeTickCollision;
90
+ };
79
91
  /**
80
92
  * Scale overrides for one plot axis - svelteplot's `ScaleOptions` and
81
93
  * `XScaleOptions`, plus this package's own fields. `TData` is the row type
@@ -94,6 +106,10 @@ export type AxisScale<TData = Record<string, unknown>> = {
94
106
  interval?: string | number;
95
107
  clamp?: boolean;
96
108
  nice?: boolean;
109
+ niceTolerance?: number;
110
+ edgeTicks?: boolean | {
111
+ collision?: EdgeTickCollision;
112
+ };
97
113
  zero?: boolean;
98
114
  round?: boolean;
99
115
  percent?: boolean;