@fjader/svelte 0.2.2 → 0.2.4

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 (148) hide show
  1. package/README.md +2 -0
  2. package/dist/components/axes/AxisBottom.svelte +23 -6
  3. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  4. package/dist/components/axes/AxisLeft.svelte +77 -8
  5. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  6. package/dist/components/axes/BandAxisTicks.svelte +64 -25
  7. package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
  8. package/dist/components/axes/axis-title.d.ts +30 -0
  9. package/dist/components/axes/axis-title.js +77 -0
  10. package/dist/components/charts/ChartCell.svelte +1 -1
  11. package/dist/components/charts/ChartContainer.svelte +12 -3
  12. package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
  13. package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
  14. package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
  15. package/dist/components/marks/AreaMarkX.svelte +21 -6
  16. package/dist/components/marks/AreaMarkY.svelte +21 -6
  17. package/dist/components/marks/BarMarkX.svelte +16 -1
  18. package/dist/components/marks/BarMarkY.svelte +16 -1
  19. package/dist/components/marks/DotsMark.svelte +5 -0
  20. package/dist/components/marks/LineMark.svelte +8 -1
  21. package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
  22. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
  23. package/dist/components/marks/RectMark.svelte +5 -0
  24. package/dist/components/marks/RectXMark.svelte +5 -0
  25. package/dist/components/marks/RectYMark.svelte +5 -0
  26. package/dist/components/marks/RuleMarkX.svelte +12 -2
  27. package/dist/components/marks/RuleMarkY.svelte +12 -2
  28. package/dist/components/marks/TextMark.svelte +76 -13
  29. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
  30. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
  31. package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
  32. package/dist/components/marks/lifecycle/domain-queries.js +22 -15
  33. package/dist/components/marks/lifecycle/domain-registration.js +2 -6
  34. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
  35. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  36. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  37. package/dist/components/marks/lifecycle/other-split.js +112 -0
  38. package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
  39. package/dist/components/marks/lifecycle/query-builder.js +25 -5
  40. package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
  41. package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
  42. package/dist/components/marks/utils/area-outline.d.ts +24 -0
  43. package/dist/components/marks/utils/area-outline.js +10 -0
  44. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  45. package/dist/components/marks/utils/reactive-config.js +9 -0
  46. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  47. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  48. package/dist/components/rendering/Areas.svelte +30 -6
  49. package/dist/components/rendering/Areas.svelte.d.ts +2 -0
  50. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  51. package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
  52. package/dist/components/rendering/Dots.svelte +24 -3
  53. package/dist/components/rendering/Dots.svelte.d.ts +2 -0
  54. package/dist/components/rendering/GridLine.svelte +2 -1
  55. package/dist/components/rendering/Lines.svelte +49 -7
  56. package/dist/components/rendering/Lines.svelte.d.ts +9 -0
  57. package/dist/components/rendering/ParallelLines.svelte +26 -7
  58. package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
  59. package/dist/components/rendering/Rects.svelte +129 -20
  60. package/dist/components/rendering/Rects.svelte.d.ts +9 -0
  61. package/dist/components/rendering/RuleX.svelte +20 -5
  62. package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
  63. package/dist/components/rendering/RuleY.svelte +20 -5
  64. package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
  65. package/dist/components/rendering/activity-reads.d.ts +111 -0
  66. package/dist/components/rendering/activity-reads.js +154 -0
  67. package/dist/components/rendering/uniform-sizes.d.ts +26 -0
  68. package/dist/components/rendering/uniform-sizes.js +28 -0
  69. package/dist/config/ChartConfig.svelte.d.ts +5 -1
  70. package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
  71. package/dist/gpu/ColorScaleShaderLib.js +56 -5
  72. package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
  73. package/dist/gpu/GPUCoordinator.svelte.js +68 -32
  74. package/dist/gpu/MorphedChannels.d.ts +6 -32
  75. package/dist/gpu/MorphedChannels.js +45 -3
  76. package/dist/gpu/ResolveShader.d.ts +2 -2
  77. package/dist/gpu/ResolveShader.js +2 -2
  78. package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
  79. package/dist/gpu/ScaleTemplateSystem.js +111 -11
  80. package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
  81. package/dist/gpu/SizeScaleShaderLib.js +4 -4
  82. package/dist/gpu/identity-slot-packing.d.ts +157 -40
  83. package/dist/gpu/identity-slot-packing.js +262 -62
  84. package/dist/gpu/tracked-scales.d.ts +14 -0
  85. package/dist/gpu/tracked-scales.js +18 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.js +4 -0
  88. package/dist/interactors/BandLabelToggle.svelte +5 -9
  89. package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
  90. package/dist/interactors/DerivedInterval1D.svelte +26 -54
  91. package/dist/interactors/Interval1D.svelte +8 -2
  92. package/dist/interactors/Interval2D.svelte +8 -2
  93. package/dist/interactors/ParallelAxisSource.svelte +14 -5
  94. package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
  95. package/dist/interactors/ParallelBrushes.svelte +8 -9
  96. package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
  97. package/dist/interactors/Toggle.svelte +48 -38
  98. package/dist/interactors/Toggle.svelte.d.ts +4 -3
  99. package/dist/interactors/Tooltip.svelte +140 -42
  100. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  101. package/dist/interactors/TooltipManager.svelte.js +40 -10
  102. package/dist/interactors/other-tooltip.d.ts +16 -0
  103. package/dist/interactors/other-tooltip.js +22 -0
  104. package/dist/interactors/relay-clauses.d.ts +23 -0
  105. package/dist/interactors/relay-clauses.js +60 -0
  106. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
  107. package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
  108. package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
  109. package/dist/query-engine/FieldActivity.svelte.js +378 -0
  110. package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
  111. package/dist/query-engine/FilterGroup.svelte.js +223 -124
  112. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
  113. package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
  114. package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
  115. package/dist/query-engine/MarkHandle.svelte.js +12 -2
  116. package/dist/query-engine/QueryEngine.d.ts +52 -16
  117. package/dist/query-engine/QueryEngine.js +116 -34
  118. package/dist/query-engine/categorical-identity.d.ts +114 -27
  119. package/dist/query-engine/categorical-identity.js +225 -51
  120. package/dist/query-engine/index.d.ts +2 -1
  121. package/dist/query-engine/index.js +1 -0
  122. package/dist/query-engine/preagg/Preagg.d.ts +1 -1
  123. package/dist/query-engine/preagg/Preagg.js +11 -9
  124. package/dist/query-engine/preagg/plan.d.ts +14 -5
  125. package/dist/query-engine/preagg/plan.js +44 -18
  126. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  127. package/dist/query-engine/toggle-predicate.js +41 -2
  128. package/dist/scales/ActivityTable.d.ts +103 -0
  129. package/dist/scales/ActivityTable.js +220 -0
  130. package/dist/scales/BandScaleState.svelte.d.ts +41 -15
  131. package/dist/scales/BandScaleState.svelte.js +78 -36
  132. package/dist/scales/ChartScales.svelte.d.ts +2 -2
  133. package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
  134. package/dist/scales/ColorScaleState.svelte.js +118 -24
  135. package/dist/scales/DomainResolver.d.ts +2 -0
  136. package/dist/scales/DomainResolver.js +15 -8
  137. package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
  138. package/dist/scales/ScaleRegistry.svelte.js +22 -1
  139. package/dist/scales/TimeScaleState.svelte.js +7 -1
  140. package/dist/spec/index.js +1 -0
  141. package/dist/spec/types.d.ts +4 -0
  142. package/dist/theme.css +5 -4
  143. package/dist/transforms/TransformCompiler.d.ts +37 -2
  144. package/dist/transforms/TransformCompiler.js +44 -1
  145. package/dist/transforms/index.d.ts +27 -0
  146. package/dist/transforms/index.js +127 -19
  147. package/dist/types.d.ts +71 -0
  148. package/package.json +3 -2
package/README.md CHANGED
@@ -16,6 +16,8 @@ pip install fjader # Python: marimo and Jupyter
16
16
  pip install "fjader[streamlit]" # Python: Streamlit
17
17
  ```
18
18
 
19
+ `@uwdata/mosaic-sql` is a peer dependency (0.34 or 0.35), so your app and fjader share one copy. Two copies would not recognise each other's expressions: an aggregate such as `sum(...)` written with one would be misread by the other.
20
+
19
21
  fjader needs Chrome 149 or later, or another Chromium browser on that version. It relies on two recent features other browsers don't ship yet: WebGPU's `immediate_address_space`, through which every chart passes its scales to the shaders (so every zoom and pan uses it), and the `Temporal` API, used for calendar offsets. There is no fallback renderer.
20
22
 
21
23
  Three things to know before using it in a deployed app:
@@ -8,9 +8,24 @@
8
8
  interface Props {
9
9
  ticksNumber?: number;
10
10
  tickFormat?: ((value: unknown) => string) | null;
11
+ /**
12
+ * Draw the scale's label as the axis title (default). `false` draws none; the
13
+ * label still names the scale, so charts sharing it still link their zoom.
14
+ */
15
+ title?: boolean;
16
+ /**
17
+ * Where along the axis the title sits: `'center'` (default), or at the plot's
18
+ * `'start'` (left) or `'end'` (right) edge.
19
+ */
20
+ titleAnchor?: 'start' | 'center' | 'end';
11
21
  }
12
22
 
13
- let { ticksNumber = 5, tickFormat = null }: Props = $props();
23
+ let {
24
+ ticksNumber = 5,
25
+ tickFormat = null,
26
+ title = true,
27
+ titleAnchor = 'center'
28
+ }: Props = $props();
14
29
  const c = getContext<ChartContext>('chart');
15
30
 
16
31
  // Propagate custom format to scale state once it exists (for MarginLabel, tooltips, etc.)
@@ -47,10 +62,6 @@
47
62
  {tickFormatter(tick)}
48
63
  </text>
49
64
  {/each}
50
-
51
- <text text-anchor="middle" x={c.innerWidth / 2} y={28}>
52
- {c.xLabel}
53
- </text>
54
65
  {:else if c.scaleTypes.x === 'band' && c.xScale}
55
66
  <BandAxisTicks
56
67
  scale={c.xScaleState as BandScaleState}
@@ -60,8 +71,14 @@
60
71
  tickLength={5}
61
72
  labelGap={16}
62
73
  />
74
+ {/if}
63
75
 
64
- <text text-anchor="middle" x={c.innerWidth / 2} y={28}>
76
+ {#if title && c.xScale}
77
+ <text
78
+ text-anchor={titleAnchor === 'start' ? 'start' : titleAnchor === 'end' ? 'end' : 'middle'}
79
+ x={titleAnchor === 'start' ? 0 : titleAnchor === 'end' ? c.innerWidth : c.innerWidth / 2}
80
+ y={28}
81
+ >
65
82
  {c.xLabel}
66
83
  </text>
67
84
  {/if}
@@ -1,6 +1,16 @@
1
1
  interface Props {
2
2
  ticksNumber?: number;
3
3
  tickFormat?: ((value: unknown) => string) | null;
4
+ /**
5
+ * Draw the scale's label as the axis title (default). `false` draws none; the
6
+ * label still names the scale, so charts sharing it still link their zoom.
7
+ */
8
+ title?: boolean;
9
+ /**
10
+ * Where along the axis the title sits: `'center'` (default), or at the plot's
11
+ * `'start'` (left) or `'end'` (right) edge.
12
+ */
13
+ titleAnchor?: 'start' | 'center' | 'end';
4
14
  }
5
15
  declare const AxisBottom: import("svelte").Component<Props, {}, "">;
6
16
  type AxisBottom = ReturnType<typeof AxisBottom>;
@@ -4,13 +4,38 @@
4
4
  import type { BandScaleState } from '../../scales/BandScaleState.svelte';
5
5
  import BandAxisTicks from './BandAxisTicks.svelte';
6
6
  import type { ContinuousScaleState } from '../../types';
7
+ import { categoryLabel } from '../../query-engine/categorical-identity';
8
+ import { clearTitleOffset, sameTicks, textWidth } from './axis-title';
7
9
 
8
10
  interface Props {
9
11
  ticksNumber?: number;
10
12
  tickFormat?: ((value: unknown) => string) | null;
13
+ /**
14
+ * Draw the scale's label as the axis title (default). `false` draws none; the
15
+ * label still names the scale, so charts sharing it still link their zoom.
16
+ */
17
+ title?: boolean;
18
+ /**
19
+ * How far left of the axis the title sits, in px (default 32), or `'auto'`:
20
+ * clear of the widest tick label, as far as the chart's left margin allows.
21
+ */
22
+ titleOffset?: number | 'auto';
23
+ /**
24
+ * Where the title goes: `'side'` (default), rotated beside the tick labels, or
25
+ * `'top'`, horizontal above the plot at the axis. `'top'` cannot meet the tick
26
+ * labels, so it needs no `titleOffset` and no room in the left margin; it takes
27
+ * the space above the plot (the chart's top margin).
28
+ */
29
+ titlePlacement?: 'side' | 'top';
11
30
  }
12
31
 
13
- let { ticksNumber = 5, tickFormat = null }: Props = $props();
32
+ let {
33
+ ticksNumber = 5,
34
+ tickFormat = null,
35
+ title = true,
36
+ titleOffset = 32,
37
+ titlePlacement = 'side'
38
+ }: Props = $props();
14
39
  const c = getContext<ChartContext>('chart');
15
40
 
16
41
  // Propagate custom format to scale state once it exists (for MarginLabel, tooltips, etc.)
@@ -34,6 +59,43 @@
34
59
 
35
60
  // Unified formatter interface - all scale states have tickFormat()
36
61
  const tickFormatter = $derived(c.yScaleState?.tickFormat ? c.yScaleState.tickFormat() : String);
62
+
63
+ // The title's distance from the axis. Measured only for 'auto', from the labels
64
+ // drawn (each distinct string once, then cached). It follows the widest label
65
+ // seen on this scale and never moves back in: pan and zoom change the ticks
66
+ // all the time, and a title that followed them would jitter. Taking the
67
+ // maximum is idempotent, so re-evaluating this changes nothing.
68
+ let widestSeen = 0;
69
+ let widestScale: unknown = null;
70
+ // The ticks and formatter the widest was last taken from: a pan that only moves
71
+ // the same ticks measures (and formats) nothing.
72
+ let measuredTicks: ArrayLike<unknown> | null = null;
73
+ let measuredFormatter: unknown = null;
74
+ const titleX = $derived.by(() => {
75
+ if (titleOffset !== 'auto') return titleOffset;
76
+ const scale = c.yScaleState;
77
+ if (scale !== widestScale) {
78
+ widestScale = scale;
79
+ widestSeen = 0;
80
+ measuredTicks = null;
81
+ }
82
+ const band = c.scaleTypes?.y === 'band';
83
+ const ticks: ArrayLike<unknown> = band ? bandTicks : continuousTicks;
84
+ if (!sameTicks(measuredTicks, ticks) || (!band && measuredFormatter !== tickFormatter)) {
85
+ if (band) {
86
+ for (const tick of bandTicks) {
87
+ widestSeen = Math.max(widestSeen, textWidth(String(categoryLabel(tick.value))));
88
+ }
89
+ } else {
90
+ for (const tick of continuousTicks) {
91
+ widestSeen = Math.max(widestSeen, textWidth(String(tickFormatter(tick))));
92
+ }
93
+ }
94
+ measuredTicks = ticks;
95
+ measuredFormatter = tickFormatter;
96
+ }
97
+ return clearTitleOffset(widestSeen, c.margin.left);
98
+ });
37
99
  </script>
38
100
 
39
101
  {#if c.isReady && c.scaleTypes}
@@ -47,10 +109,6 @@
47
109
  {tickFormatter(tick)}
48
110
  </text>
49
111
  {/each}
50
-
51
- <text text-anchor="middle" transform={`translate(-32, ${c.innerHeight / 2}) rotate(-90)`}>
52
- {c.yLabel}
53
- </text>
54
112
  {:else if c.scaleTypes.y === 'band' && c.yScale}
55
113
  <BandAxisTicks
56
114
  scale={c.yScaleState as BandScaleState}
@@ -60,10 +118,21 @@
60
118
  dir={-1}
61
119
  textAnchor="end"
62
120
  />
121
+ {/if}
63
122
 
64
- <text text-anchor="middle" transform={`translate(-32, ${c.innerHeight / 2}) rotate(-90)`}>
65
- {c.yLabel}
66
- </text>
123
+ {#if title && c.yScale}
124
+ {#if titlePlacement === 'top'}
125
+ <text text-anchor="start" x={0} y={-18}>
126
+ {c.yLabel}
127
+ </text>
128
+ {:else}
129
+ <text
130
+ text-anchor="middle"
131
+ transform={`translate(${-titleX}, ${c.innerHeight / 2}) rotate(-90)`}
132
+ >
133
+ {c.yLabel}
134
+ </text>
135
+ {/if}
67
136
  {/if}
68
137
  </g>
69
138
  {/if}
@@ -1,6 +1,23 @@
1
1
  interface Props {
2
2
  ticksNumber?: number;
3
3
  tickFormat?: ((value: unknown) => string) | null;
4
+ /**
5
+ * Draw the scale's label as the axis title (default). `false` draws none; the
6
+ * label still names the scale, so charts sharing it still link their zoom.
7
+ */
8
+ title?: boolean;
9
+ /**
10
+ * How far left of the axis the title sits, in px (default 32), or `'auto'`:
11
+ * clear of the widest tick label, as far as the chart's left margin allows.
12
+ */
13
+ titleOffset?: number | 'auto';
14
+ /**
15
+ * Where the title goes: `'side'` (default), rotated beside the tick labels, or
16
+ * `'top'`, horizontal above the plot at the axis. `'top'` cannot meet the tick
17
+ * labels, so it needs no `titleOffset` and no room in the left margin; it takes
18
+ * the space above the plot (the chart's top margin).
19
+ */
20
+ titlePlacement?: 'side' | 'top';
4
21
  }
5
22
  declare const AxisLeft: import("svelte").Component<Props, {}, "">;
6
23
  type AxisLeft = ReturnType<typeof AxisLeft>;
@@ -1,21 +1,25 @@
1
1
  <script lang="ts">
2
+ import { getContext } from 'svelte';
2
3
  import type { BandScaleState, BandTick } from '../../scales/BandScaleState.svelte';
4
+ import type { ChartContext } from '../../types';
5
+ import { categoryLabel } from '../../query-engine/categorical-identity';
6
+ import type { FilterGroupRegistry } from '../../query-engine/FilterGroupRegistry.svelte';
3
7
 
4
8
  /**
5
9
  * The labels on a band axis, wherever that axis is drawn.
6
10
  *
7
11
  * Three components rendered this: AxisBottom, AxisLeft and AxisCenter. The
8
- * arithmetic was already shared — `tickPosition`, `tickOpacity` and
9
- * `isTickDimmed` all live on the scale — so what was actually duplicated
12
+ * arithmetic was already shared — `tickPosition` and `tickOpacity` live on
13
+ * the scale — so what was actually duplicated
10
14
  * was the markup around it, and what differs between the three is a handful
11
15
  * of numbers: which way the ticks point, how long they are, how far the
12
16
  * label sits, and where it anchors.
13
17
  *
14
18
  * Those are props here, so the structure is written once. The structure is
15
19
  * the part with the reasons attached: the keyed `{#each}` so a reorder
16
- * MOVES a label rather than replacing it, the opacity wrapper the scale
17
- * drives per frame so a label travels and fades with its bar, and the
18
- * dimming class inside it so a selection does not fight the transition.
20
+ * MOVES a label rather than replacing it, and the opacity wrapper the scale
21
+ * drives per frame so a label travels, enters, leaves and dims for a
22
+ * selection with its bar, all on the scale's clocks.
19
23
  *
20
24
  * A band label is positioned through `tickPosition`, which reads the
21
25
  * category's CURRENT fractional slot, not through the d3 scale. The d3
@@ -54,26 +58,66 @@
54
58
  textAnchor = 'middle'
55
59
  }: Props = $props();
56
60
 
61
+ const chart = getContext<ChartContext | undefined>('chart');
62
+ const filterGroups = getContext<FilterGroupRegistry | undefined>('filter-groups');
63
+
64
+ /**
65
+ * The filter groups whose selection the labels show: those of the chart's
66
+ * marks. A selection dims the axis's marks in their own group, so a label
67
+ * dims when any of those groups dims its category; a background mark in a
68
+ * group with nothing selected changes nothing.
69
+ */
70
+ const groups = $derived.by(() => {
71
+ if (!chart || !filterGroups) return [];
72
+ const indices: number[] = [];
73
+ for (const entry of chart.config.entries.values()) {
74
+ // A 'filter' mark never dims; its labels follow it.
75
+ if (entry.selectionResponse === 'filter') continue;
76
+ const index = filterGroups.get(entry.selection).index;
77
+ if (!indices.includes(index)) indices.push(index);
78
+ }
79
+ return indices;
80
+ });
81
+
82
+ /**
83
+ * The field the axis's selection is kept under: its identity key, or its
84
+ * column before. As for the legend, the two differ for a computed field,
85
+ * so its labels briefly lose their dimming while a reload empties identity.
86
+ */
87
+ const key = $derived(scale.identityKey ?? scale.sourceField);
88
+
89
+ /**
90
+ * A label's opacity: the scale's own (travel, clipping) times its selection
91
+ * fade. Read by id where the tick's key is one — the only way to reach an
92
+ * Other bucket, whose label is not a value — and by value before identity.
93
+ */
94
+ function opacityOf(tick: BandTick): number {
95
+ const selected = !filterGroups
96
+ ? 1
97
+ : scale.hasIdentity
98
+ ? filterGroups.opacityOfId(key, groups, tick.key)
99
+ : filterGroups.opacityOfValue(key, groups, tick.value);
100
+ return scale.tickOpacity(tick.key) * selected;
101
+ }
102
+
57
103
  const tickEnd = $derived(base + dir * tickLength);
58
104
  const labelAt = $derived(base + dir * labelGap);
59
105
  </script>
60
106
 
61
107
  {#each ticks as tick (tick.key)}
62
- {@const pos = scale.tickPosition(tick.key, rangeSize, axis)}
63
- <g opacity={scale.tickOpacity(tick.key)}>
64
- <g class:tick-dimmed={scale.isTickDimmed(tick)}>
65
- {#if axis === 'x'}
66
- <line x1={pos} x2={pos} y1={base} y2={tickEnd} />
67
- <text text-anchor={textAnchor} x={pos} y={labelAt}>
68
- {tick.value}
69
- </text>
70
- {:else}
71
- <line x1={base} x2={tickEnd} y1={pos} y2={pos} />
72
- <text text-anchor={textAnchor} dominant-baseline="middle" x={labelAt} y={pos}>
73
- {tick.value}
74
- </text>
75
- {/if}
76
- </g>
108
+ {@const pos = scale.tickPosition(tick.key, rangeSize)}
109
+ <g opacity={opacityOf(tick)}>
110
+ {#if axis === 'x'}
111
+ <line x1={pos} x2={pos} y1={base} y2={tickEnd} />
112
+ <text text-anchor={textAnchor} x={pos} y={labelAt}>
113
+ {categoryLabel(tick.value)}
114
+ </text>
115
+ {:else}
116
+ <line x1={base} x2={tickEnd} y1={pos} y2={pos} />
117
+ <text text-anchor={textAnchor} dominant-baseline="middle" x={labelAt} y={pos}>
118
+ {categoryLabel(tick.value)}
119
+ </text>
120
+ {/if}
77
121
  </g>
78
122
  {/each}
79
123
 
@@ -87,9 +131,4 @@
87
131
  font-family: var(--fjader-font, system-ui, sans-serif);
88
132
  font-size: 10px;
89
133
  }
90
-
91
- .tick-dimmed {
92
- opacity: 0.25;
93
- transition: opacity 0.15s ease;
94
- }
95
134
  </style>
@@ -3,17 +3,17 @@ import type { BandScaleState, BandTick } from '../../scales/BandScaleState.svelt
3
3
  * The labels on a band axis, wherever that axis is drawn.
4
4
  *
5
5
  * Three components rendered this: AxisBottom, AxisLeft and AxisCenter. The
6
- * arithmetic was already shared — `tickPosition`, `tickOpacity` and
7
- * `isTickDimmed` all live on the scale — so what was actually duplicated
6
+ * arithmetic was already shared — `tickPosition` and `tickOpacity` live on
7
+ * the scale — so what was actually duplicated
8
8
  * was the markup around it, and what differs between the three is a handful
9
9
  * of numbers: which way the ticks point, how long they are, how far the
10
10
  * label sits, and where it anchors.
11
11
  *
12
12
  * Those are props here, so the structure is written once. The structure is
13
13
  * the part with the reasons attached: the keyed `{#each}` so a reorder
14
- * MOVES a label rather than replacing it, the opacity wrapper the scale
15
- * drives per frame so a label travels and fades with its bar, and the
16
- * dimming class inside it so a selection does not fight the transition.
14
+ * MOVES a label rather than replacing it, and the opacity wrapper the scale
15
+ * drives per frame so a label travels, enters, leaves and dims for a
16
+ * selection with its bar, all on the scale's clocks.
17
17
  *
18
18
  * A band label is positioned through `tickPosition`, which reads the
19
19
  * category's CURRENT fractional slot, not through the d3 scale. The d3
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Where an axis title sits relative to its tick labels.
3
+ *
4
+ * The left axis puts its tick labels right-anchored 9px from the axis line and
5
+ * its title, rotated, to the left of them. A fixed offset is right for short
6
+ * labels and runs into wide ones (`40,000`, a band name). `'auto'` places the
7
+ * title clear of the widest label, within the margin the chart gave it.
8
+ */
9
+ /** The title's offset when nothing says otherwise, and the least `'auto'` uses. */
10
+ export declare const DEFAULT_TITLE_OFFSET = 32;
11
+ /**
12
+ * The width of a tick label in px. Measured on a canvas, once per distinct
13
+ * string (a band axis has the same labels frame after frame); 6px a character
14
+ * where there is no document. An estimate by design: the page's own font can
15
+ * differ a little, and CLEARANCE covers that.
16
+ */
17
+ export declare function textWidth(text: string): number;
18
+ /**
19
+ * How far left of the axis line the title's baseline goes, clear of a label
20
+ * `widest` px wide: never less than the default, and never so far that its body
21
+ * leaves the margin. A margin too narrow for the labels leaves them overlapping,
22
+ * and only a wider margin fixes that.
23
+ */
24
+ export declare function clearTitleOffset(widest: number, marginLeft: number): number;
25
+ /**
26
+ * Whether two tick lists are the same ticks: equal values, or band ticks of the
27
+ * same key. A loop with no allocation, to skip re-measuring labels when a pan
28
+ * moved the ticks but did not change them.
29
+ */
30
+ export declare function sameTicks(a: ArrayLike<unknown> | null, b: ArrayLike<unknown>): boolean;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Where an axis title sits relative to its tick labels.
3
+ *
4
+ * The left axis puts its tick labels right-anchored 9px from the axis line and
5
+ * its title, rotated, to the left of them. A fixed offset is right for short
6
+ * labels and runs into wide ones (`40,000`, a band name). `'auto'` places the
7
+ * title clear of the widest label, within the margin the chart gave it.
8
+ */
9
+ /** Where the tick labels end, from the axis line: AxisLeft's text x and BandAxisTicks' labelGap. */
10
+ const TICK_GAP = 9;
11
+ /** Between the widest label and the title's baseline; the title's descenders reach into it. */
12
+ const CLEARANCE = 12;
13
+ /** A rotated title's glyphs sit on the far side of its baseline, away from the axis. */
14
+ const TITLE_BODY = 10;
15
+ /** The title's offset when nothing says otherwise, and the least `'auto'` uses. */
16
+ export const DEFAULT_TITLE_OFFSET = 32;
17
+ const FONT = '10px system-ui, sans-serif';
18
+ const FALLBACK_CHAR_WIDTH = 6;
19
+ const CACHE_LIMIT = 2000;
20
+ let context;
21
+ const widths = new Map();
22
+ /**
23
+ * The width of a tick label in px. Measured on a canvas, once per distinct
24
+ * string (a band axis has the same labels frame after frame); 6px a character
25
+ * where there is no document. An estimate by design: the page's own font can
26
+ * differ a little, and CLEARANCE covers that.
27
+ */
28
+ export function textWidth(text) {
29
+ const known = widths.get(text);
30
+ if (known !== undefined)
31
+ return known;
32
+ if (context === undefined) {
33
+ context =
34
+ typeof document === 'undefined' ? null : document.createElement('canvas').getContext('2d');
35
+ if (context)
36
+ context.font = FONT;
37
+ }
38
+ const width = context ? context.measureText(text).width : text.length * FALLBACK_CHAR_WIDTH;
39
+ if (widths.size >= CACHE_LIMIT)
40
+ widths.clear();
41
+ widths.set(text, width);
42
+ return width;
43
+ }
44
+ /**
45
+ * How far left of the axis line the title's baseline goes, clear of a label
46
+ * `widest` px wide: never less than the default, and never so far that its body
47
+ * leaves the margin. A margin too narrow for the labels leaves them overlapping,
48
+ * and only a wider margin fixes that.
49
+ */
50
+ export function clearTitleOffset(widest, marginLeft) {
51
+ const wanted = Math.max(TICK_GAP + widest + CLEARANCE, DEFAULT_TITLE_OFFSET);
52
+ return Math.max(0, Math.min(wanted, marginLeft - TITLE_BODY));
53
+ }
54
+ /**
55
+ * Whether two tick lists are the same ticks: equal values, or band ticks of the
56
+ * same key. A loop with no allocation, to skip re-measuring labels when a pan
57
+ * moved the ticks but did not change them.
58
+ */
59
+ export function sameTicks(a, b) {
60
+ if (!a || a.length !== b.length)
61
+ return false;
62
+ for (let i = 0; i < b.length; i++) {
63
+ const x = a[i];
64
+ const y = b[i];
65
+ if (x === y)
66
+ continue;
67
+ if (typeof x === 'object' &&
68
+ typeof y === 'object' &&
69
+ x !== null &&
70
+ y !== null &&
71
+ x.key !== undefined &&
72
+ x.key === y.key)
73
+ continue;
74
+ return false;
75
+ }
76
+ return true;
77
+ }
@@ -897,7 +897,7 @@
897
897
  if (overBrush) {
898
898
  tooltipManager?.updatePosition(null, null);
899
899
  } else {
900
- tooltipManager?.updatePointerScreen(x, y);
900
+ tooltipManager?.updatePointerClient(x + svgLeft, y + svgTop);
901
901
  tooltipManager?.updatePosition(
902
902
  scales.xScaleState?.currentDataPoint,
903
903
  scales.yScaleState?.currentDataPoint
@@ -13,6 +13,7 @@
13
13
  import { identityKeyFor } from '../../query-engine/identity-key';
14
14
  import { checkFacetCells } from './facet-limits';
15
15
  import ChartCell from './ChartCell.svelte';
16
+ import { categoryLabel } from '../../query-engine/categorical-identity';
16
17
 
17
18
  interface Props {
18
19
  xLabel?: string | null;
@@ -77,6 +78,13 @@
77
78
  clipPadding?: number;
78
79
  useXSelection?: boolean;
79
80
  useYSelection?: boolean;
81
+ /**
82
+ * The tooltip's colours against the color-scheme the chart inherits:
83
+ * `contrast` is the opposite scheme (dark on a light page), `follow` the
84
+ * same. Unset, the nearest `data-fjader-tooltip` ancestor decides, else
85
+ * contrast. `--fjader-tooltip-bg`/`-text` set by the app override both.
86
+ */
87
+ tooltipScheme?: 'contrast' | 'follow';
80
88
  }
81
89
 
82
90
  let {
@@ -107,7 +115,8 @@
107
115
  margin: marginOverride = {},
108
116
  clipPadding = 0,
109
117
  useXSelection = false,
110
- useYSelection = false
118
+ useYSelection = false,
119
+ tooltipScheme = undefined
111
120
  }: Props = $props();
112
121
 
113
122
  const defaultMargin = { top: 30, right: 50, bottom: 35, left: 75 };
@@ -451,7 +460,7 @@
451
460
  setContext('legend-position', () => legendPosition);
452
461
  </script>
453
462
 
454
- <div class="figure-container" bind:clientWidth={layout.width}>
463
+ <div class="figure-container" bind:clientWidth={layout.width} data-fjader-tooltip={tooltipScheme}>
455
464
  {#if legend && legendPosition === 'top'}
456
465
  <div
457
466
  class="legend-container legend-top"
@@ -481,7 +490,7 @@
481
490
  <ChartCell
482
491
  cellChartId={facetChartIds[i]}
483
492
  facetIndex={i}
484
- facetLabel={String(cat)}
493
+ facetLabel={String(categoryLabel(cat))}
485
494
  margin={{ top: 20, right: 10, bottom: 35, left: 55 }}
486
495
  {scales}
487
496
  {chartConfig}
@@ -69,6 +69,13 @@ interface Props {
69
69
  clipPadding?: number;
70
70
  useXSelection?: boolean;
71
71
  useYSelection?: boolean;
72
+ /**
73
+ * The tooltip's colours against the color-scheme the chart inherits:
74
+ * `contrast` is the opposite scheme (dark on a light page), `follow` the
75
+ * same. Unset, the nearest `data-fjader-tooltip` ancestor decides, else
76
+ * contrast. `--fjader-tooltip-bg`/`-text` set by the app override both.
77
+ */
78
+ tooltipScheme?: 'contrast' | 'follow';
72
79
  }
73
80
  declare const ChartContainer: import("svelte").Component<Props, {}, "">;
74
81
  type ChartContainer = ReturnType<typeof ChartContainer>;
@@ -166,7 +166,8 @@
166
166
 
167
167
  const brushHandle: SourceHandle | null = filterGroup
168
168
  ? filterGroup.register(sourceId, {
169
- excludeMarkIds: () => new Set<string>(),
169
+ // No chart: an interval cannot dim, so the chart it sits in filters by
170
+ // it rather than showing nothing.
170
171
  // The brush maps the bar linearly onto [min, max], whatever the color
171
172
  // transform, so its pixel bins do too: barLength pixels over the domain.
172
173
  activeDims: () =>
@@ -193,7 +194,13 @@
193
194
  column: scaleState.field,
194
195
  label: scaleState.field,
195
196
  values: value ? [(value as number[])[0], (value as number[])[1]] : []
196
- })
197
+ }),
198
+ // The clause was removed from outside (clearAll, a filter pill): take the
199
+ // brush down too, which only this legend holds.
200
+ onCleared: () => {
201
+ brushT0 = null;
202
+ brushT1 = null;
203
+ }
197
204
  })
198
205
  : null;
199
206