@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.
- package/README.md +2 -0
- package/dist/components/axes/AxisBottom.svelte +23 -6
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisLeft.svelte +77 -8
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +64 -25
- package/dist/components/axes/BandAxisTicks.svelte.d.ts +5 -5
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +1 -1
- package/dist/components/charts/ChartContainer.svelte +12 -3
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +9 -2
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +96 -35
- package/dist/components/marks/AreaMarkX.svelte +21 -6
- package/dist/components/marks/AreaMarkY.svelte +21 -6
- package/dist/components/marks/BarMarkX.svelte +16 -1
- package/dist/components/marks/BarMarkY.svelte +16 -1
- package/dist/components/marks/DotsMark.svelte +5 -0
- package/dist/components/marks/LineMark.svelte +8 -1
- package/dist/components/marks/ParallelCoordinatesMark.svelte +10 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +5 -0
- package/dist/components/marks/RectMark.svelte +5 -0
- package/dist/components/marks/RectXMark.svelte +5 -0
- package/dist/components/marks/RectYMark.svelte +5 -0
- package/dist/components/marks/RuleMarkX.svelte +12 -2
- package/dist/components/marks/RuleMarkY.svelte +12 -2
- package/dist/components/marks/TextMark.svelte +76 -13
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +21 -2
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +300 -42
- package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
- package/dist/components/marks/lifecycle/domain-queries.js +22 -15
- package/dist/components/marks/lifecycle/domain-registration.js +2 -6
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +104 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +26 -1
- package/dist/components/marks/lifecycle/query-builder.js +25 -5
- package/dist/components/marks/lifecycle/transform-scale-key.d.ts +16 -0
- package/dist/components/marks/lifecycle/transform-scale-key.js +27 -0
- package/dist/components/marks/utils/area-outline.d.ts +24 -0
- package/dist/components/marks/utils/area-outline.js +10 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +9 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/Areas.svelte +30 -6
- package/dist/components/rendering/Areas.svelte.d.ts +2 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +8 -3
- package/dist/components/rendering/Dots.svelte +24 -3
- package/dist/components/rendering/Dots.svelte.d.ts +2 -0
- package/dist/components/rendering/GridLine.svelte +2 -1
- package/dist/components/rendering/Lines.svelte +49 -7
- package/dist/components/rendering/Lines.svelte.d.ts +9 -0
- package/dist/components/rendering/ParallelLines.svelte +26 -7
- package/dist/components/rendering/ParallelLines.svelte.d.ts +2 -0
- package/dist/components/rendering/Rects.svelte +129 -20
- package/dist/components/rendering/Rects.svelte.d.ts +9 -0
- package/dist/components/rendering/RuleX.svelte +20 -5
- package/dist/components/rendering/RuleX.svelte.d.ts +2 -0
- package/dist/components/rendering/RuleY.svelte +20 -5
- package/dist/components/rendering/RuleY.svelte.d.ts +2 -0
- package/dist/components/rendering/activity-reads.d.ts +111 -0
- package/dist/components/rendering/activity-reads.js +154 -0
- package/dist/components/rendering/uniform-sizes.d.ts +26 -0
- package/dist/components/rendering/uniform-sizes.js +28 -0
- package/dist/config/ChartConfig.svelte.d.ts +5 -1
- package/dist/gpu/ColorScaleShaderLib.d.ts +13 -1
- package/dist/gpu/ColorScaleShaderLib.js +56 -5
- package/dist/gpu/GPUCoordinator.svelte.d.ts +8 -0
- package/dist/gpu/GPUCoordinator.svelte.js +68 -32
- package/dist/gpu/MorphedChannels.d.ts +6 -32
- package/dist/gpu/MorphedChannels.js +45 -3
- package/dist/gpu/ResolveShader.d.ts +2 -2
- package/dist/gpu/ResolveShader.js +2 -2
- package/dist/gpu/ScaleTemplateSystem.d.ts +19 -0
- package/dist/gpu/ScaleTemplateSystem.js +111 -11
- package/dist/gpu/SizeScaleShaderLib.d.ts +4 -4
- package/dist/gpu/SizeScaleShaderLib.js +4 -4
- package/dist/gpu/identity-slot-packing.d.ts +157 -40
- package/dist/gpu/identity-slot-packing.js +262 -62
- package/dist/gpu/tracked-scales.d.ts +14 -0
- package/dist/gpu/tracked-scales.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/interactors/BandLabelToggle.svelte +5 -9
- package/dist/interactors/BandLabelToggle.svelte.d.ts +3 -3
- package/dist/interactors/DerivedInterval1D.svelte +26 -54
- package/dist/interactors/Interval1D.svelte +8 -2
- package/dist/interactors/Interval2D.svelte +8 -2
- package/dist/interactors/ParallelAxisSource.svelte +14 -5
- package/dist/interactors/ParallelAxisSource.svelte.d.ts +2 -1
- package/dist/interactors/ParallelBrushes.svelte +8 -9
- package/dist/interactors/ParallelBrushes.svelte.d.ts +3 -3
- package/dist/interactors/Toggle.svelte +48 -38
- package/dist/interactors/Toggle.svelte.d.ts +4 -3
- package/dist/interactors/Tooltip.svelte +140 -42
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +40 -10
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/interactors/relay-clauses.d.ts +23 -0
- package/dist/interactors/relay-clauses.js +60 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +88 -15
- package/dist/query-engine/CrossfilterSelection.svelte.js +94 -20
- package/dist/query-engine/FieldActivity.svelte.d.ts +128 -0
- package/dist/query-engine/FieldActivity.svelte.js +378 -0
- package/dist/query-engine/FilterGroup.svelte.d.ts +76 -30
- package/dist/query-engine/FilterGroup.svelte.js +223 -124
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +73 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +161 -12
- package/dist/query-engine/MarkHandle.svelte.d.ts +9 -1
- package/dist/query-engine/MarkHandle.svelte.js +12 -2
- package/dist/query-engine/QueryEngine.d.ts +52 -16
- package/dist/query-engine/QueryEngine.js +116 -34
- package/dist/query-engine/categorical-identity.d.ts +114 -27
- package/dist/query-engine/categorical-identity.js +225 -51
- package/dist/query-engine/index.d.ts +2 -1
- package/dist/query-engine/index.js +1 -0
- package/dist/query-engine/preagg/Preagg.d.ts +1 -1
- package/dist/query-engine/preagg/Preagg.js +11 -9
- package/dist/query-engine/preagg/plan.d.ts +14 -5
- package/dist/query-engine/preagg/plan.js +44 -18
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +41 -2
- package/dist/scales/ActivityTable.d.ts +103 -0
- package/dist/scales/ActivityTable.js +220 -0
- package/dist/scales/BandScaleState.svelte.d.ts +41 -15
- package/dist/scales/BandScaleState.svelte.js +78 -36
- package/dist/scales/ChartScales.svelte.d.ts +2 -2
- package/dist/scales/ColorScaleState.svelte.d.ts +36 -8
- package/dist/scales/ColorScaleState.svelte.js +118 -24
- package/dist/scales/DomainResolver.d.ts +2 -0
- package/dist/scales/DomainResolver.js +15 -8
- package/dist/scales/ScaleRegistry.svelte.d.ts +9 -0
- package/dist/scales/ScaleRegistry.svelte.js +22 -1
- package/dist/scales/TimeScaleState.svelte.js +7 -1
- package/dist/spec/index.js +1 -0
- package/dist/spec/types.d.ts +4 -0
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +37 -2
- package/dist/transforms/TransformCompiler.js +44 -1
- package/dist/transforms/index.d.ts +27 -0
- package/dist/transforms/index.js +127 -19
- package/dist/types.d.ts +71 -0
- 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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
9
|
-
*
|
|
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
|
|
18
|
-
*
|
|
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
|
|
63
|
-
<g opacity={
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
7
|
-
*
|
|
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
|
|
16
|
-
*
|
|
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?.
|
|
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
|
-
|
|
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
|
|