@fjader/svelte 0.2.3 → 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 +1 -1
- 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 +10 -2
- package/dist/components/charts/ChartContainer.svelte.d.ts +7 -0
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +2 -0
- package/dist/components/marks/AreaMarkY.svelte +2 -0
- package/dist/components/marks/BarMarkX.svelte +13 -1
- package/dist/components/marks/BarMarkY.svelte +13 -1
- package/dist/components/marks/DotsMark.svelte +2 -0
- package/dist/components/marks/LineMark.svelte +5 -1
- package/dist/components/marks/ParallelCoordinatesMark.svelte +4 -0
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +2 -0
- package/dist/components/marks/RectMark.svelte +2 -0
- package/dist/components/marks/RectXMark.svelte +2 -0
- package/dist/components/marks/RectYMark.svelte +2 -0
- package/dist/components/marks/RuleMarkX.svelte +2 -0
- package/dist/components/marks/RuleMarkY.svelte +2 -0
- package/dist/components/marks/TextMark.svelte +8 -9
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +6 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +187 -18
- package/dist/components/marks/lifecycle/domain-queries.d.ts +3 -1
- package/dist/components/marks/lifecycle/domain-queries.js +11 -6
- 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/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +6 -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/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Rects.svelte +117 -23
- package/dist/components/rendering/Rects.svelte.d.ts +7 -0
- package/dist/components/rendering/activity-reads.d.ts +18 -0
- package/dist/components/rendering/activity-reads.js +19 -0
- package/dist/config/ChartConfig.svelte.d.ts +3 -1
- package/dist/gpu/MorphedChannels.d.ts +6 -32
- package/dist/gpu/MorphedChannels.js +45 -3
- package/dist/gpu/ScaleTemplateSystem.js +25 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +32 -7
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +50 -1
- package/dist/query-engine/FieldActivity.svelte.js +176 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/QueryEngine.js +26 -6
- package/dist/query-engine/categorical-identity.d.ts +15 -0
- package/dist/query-engine/categorical-identity.js +15 -0
- package/dist/query-engine/preagg/plan.d.ts +5 -0
- package/dist/query-engine/preagg/plan.js +31 -7
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +13 -8
- package/dist/scales/BandScaleState.svelte.js +23 -16
- package/dist/spec/types.d.ts +2 -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.js +32 -18
- package/dist/types.d.ts +41 -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>;
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
</script>
|
|
106
106
|
|
|
107
107
|
{#each ticks as tick (tick.key)}
|
|
108
|
-
{@const pos = scale.tickPosition(tick.key, rangeSize
|
|
108
|
+
{@const pos = scale.tickPosition(tick.key, rangeSize)}
|
|
109
109
|
<g opacity={opacityOf(tick)}>
|
|
110
110
|
{#if axis === 'x'}
|
|
111
111
|
<line x1={pos} x2={pos} y1={base} y2={tickEnd} />
|
|
@@ -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
|
|
@@ -78,6 +78,13 @@
|
|
|
78
78
|
clipPadding?: number;
|
|
79
79
|
useXSelection?: boolean;
|
|
80
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';
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
let {
|
|
@@ -108,7 +115,8 @@
|
|
|
108
115
|
margin: marginOverride = {},
|
|
109
116
|
clipPadding = 0,
|
|
110
117
|
useXSelection = false,
|
|
111
|
-
useYSelection = false
|
|
118
|
+
useYSelection = false,
|
|
119
|
+
tooltipScheme = undefined
|
|
112
120
|
}: Props = $props();
|
|
113
121
|
|
|
114
122
|
const defaultMargin = { top: 30, right: 50, bottom: 35, left: 75 };
|
|
@@ -452,7 +460,7 @@
|
|
|
452
460
|
setContext('legend-position', () => legendPosition);
|
|
453
461
|
</script>
|
|
454
462
|
|
|
455
|
-
<div class="figure-container" bind:clientWidth={layout.width}>
|
|
463
|
+
<div class="figure-container" bind:clientWidth={layout.width} data-fjader-tooltip={tooltipScheme}>
|
|
456
464
|
{#if legend && legendPosition === 'top'}
|
|
457
465
|
<div
|
|
458
466
|
class="legend-container legend-top"
|
|
@@ -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>;
|
|
@@ -194,7 +194,13 @@
|
|
|
194
194
|
column: scaleState.field,
|
|
195
195
|
label: scaleState.field,
|
|
196
196
|
values: value ? [(value as number[])[0], (value as number[])[1]] : []
|
|
197
|
-
})
|
|
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
|
+
}
|
|
198
204
|
})
|
|
199
205
|
: null;
|
|
200
206
|
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
id: number | null;
|
|
66
66
|
value: DimValue;
|
|
67
67
|
color: string;
|
|
68
|
-
/** An Other bucket:
|
|
68
|
+
/** An Other bucket: a click selects what it holds (see toggleOther). */
|
|
69
69
|
bucket: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -128,8 +128,11 @@
|
|
|
128
128
|
onDestroy(() => toggle?.destroy());
|
|
129
129
|
|
|
130
130
|
// Shift/meta-click adds or removes; selecting every value clears instead.
|
|
131
|
-
function handleClick(
|
|
132
|
-
|
|
131
|
+
function handleClick(item: Item, event: MouseEvent) {
|
|
132
|
+
const multi = event.shiftKey || event.metaKey;
|
|
133
|
+
// An Other bucket names no value: the click selects what it holds.
|
|
134
|
+
if (item.bucket && item.id !== null) toggle?.toggleOther(item.id, null, multi);
|
|
135
|
+
else toggle?.toggle([item.value], multi, scaleState.domain.length);
|
|
133
136
|
}
|
|
134
137
|
</script>
|
|
135
138
|
|
|
@@ -149,11 +152,11 @@
|
|
|
149
152
|
>
|
|
150
153
|
{#each items as item (item.id ?? item.value)}
|
|
151
154
|
{@const { value, color } = item}
|
|
152
|
-
{#if isInteractive
|
|
155
|
+
{#if isInteractive}
|
|
153
156
|
<button
|
|
154
157
|
class="legend-item legend-item-interactive"
|
|
155
158
|
style:opacity={opacityOf(item)}
|
|
156
|
-
onclick={(e) => handleClick(
|
|
159
|
+
onclick={(e) => handleClick(item, e)}
|
|
157
160
|
>
|
|
158
161
|
<span
|
|
159
162
|
class="legend-swatch"
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
table,
|
|
49
49
|
selection = undefined,
|
|
50
50
|
selectionResponse = undefined,
|
|
51
|
+
nulls = undefined,
|
|
51
52
|
y,
|
|
52
53
|
x,
|
|
53
54
|
x1: x1Prop,
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
transforms: () => transforms,
|
|
102
103
|
selection: () => selection,
|
|
103
104
|
selectionResponse: () => selectionResponse,
|
|
105
|
+
nulls: () => nulls,
|
|
104
106
|
limit: () => limit,
|
|
105
107
|
preferredXScaleType: () => preferredXScaleType,
|
|
106
108
|
preferredYScaleType: () => preferredYScaleType,
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
table,
|
|
49
49
|
selection = undefined,
|
|
50
50
|
selectionResponse = undefined,
|
|
51
|
+
nulls = undefined,
|
|
51
52
|
x,
|
|
52
53
|
y,
|
|
53
54
|
y1: y1Prop,
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
transforms: () => transforms,
|
|
102
103
|
selection: () => selection,
|
|
103
104
|
selectionResponse: () => selectionResponse,
|
|
105
|
+
nulls: () => nulls,
|
|
104
106
|
limit: () => limit,
|
|
105
107
|
preferredXScaleType: () => preferredXScaleType,
|
|
106
108
|
preferredYScaleType: () => preferredYScaleType,
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
table,
|
|
42
42
|
selection = undefined,
|
|
43
43
|
selectionResponse = undefined,
|
|
44
|
+
nulls = undefined,
|
|
45
|
+
contributeDomain = undefined,
|
|
44
46
|
x,
|
|
45
47
|
x1: x1Prop,
|
|
46
48
|
x2: x2Prop,
|
|
@@ -83,6 +85,8 @@
|
|
|
83
85
|
transforms: () => transforms,
|
|
84
86
|
selection: () => selection,
|
|
85
87
|
selectionResponse: () => selectionResponse,
|
|
88
|
+
nulls: () => nulls,
|
|
89
|
+
contributeDomain: () => contributeDomain,
|
|
86
90
|
limit: () => limit,
|
|
87
91
|
padding: () => padding,
|
|
88
92
|
paddingInner: () => paddingInner,
|
|
@@ -280,7 +284,14 @@
|
|
|
280
284
|
colorColumn: 'fill',
|
|
281
285
|
bandAxes: { y: true },
|
|
282
286
|
markColor,
|
|
283
|
-
displayFields: resolvedTip.displayFields
|
|
287
|
+
displayFields: resolvedTip.displayFields,
|
|
288
|
+
otherStats: (point) => {
|
|
289
|
+
const id = point.__bandIdY;
|
|
290
|
+
const facet = point.__facetId;
|
|
291
|
+
return typeof id === 'number'
|
|
292
|
+
? setup!.otherRows.statsAt(id, typeof facet === 'number' ? facet : null)
|
|
293
|
+
: null;
|
|
294
|
+
}
|
|
284
295
|
})
|
|
285
296
|
);
|
|
286
297
|
});
|
|
@@ -303,6 +314,7 @@
|
|
|
303
314
|
{y1Data}
|
|
304
315
|
colorValueData={markColor.data}
|
|
305
316
|
alphaData={opacityData}
|
|
317
|
+
share={setup?.otherRows.split ?? null}
|
|
306
318
|
colorMode={markColor.colorMode}
|
|
307
319
|
colorScaleIndex={markColor.colorScaleIndex}
|
|
308
320
|
groupIndex={setup?.groupIndex ?? 0}
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
table,
|
|
41
41
|
selection = undefined,
|
|
42
42
|
selectionResponse = undefined,
|
|
43
|
+
nulls = undefined,
|
|
44
|
+
contributeDomain = undefined,
|
|
43
45
|
x,
|
|
44
46
|
y,
|
|
45
47
|
y1: y1Prop,
|
|
@@ -82,6 +84,8 @@
|
|
|
82
84
|
transforms: () => transforms,
|
|
83
85
|
selection: () => selection,
|
|
84
86
|
selectionResponse: () => selectionResponse,
|
|
87
|
+
nulls: () => nulls,
|
|
88
|
+
contributeDomain: () => contributeDomain,
|
|
85
89
|
limit: () => limit,
|
|
86
90
|
padding: () => padding,
|
|
87
91
|
paddingInner: () => paddingInner,
|
|
@@ -281,7 +285,14 @@
|
|
|
281
285
|
colorColumn: 'fill',
|
|
282
286
|
bandAxes: { x: true },
|
|
283
287
|
markColor,
|
|
284
|
-
displayFields: resolvedTip.displayFields
|
|
288
|
+
displayFields: resolvedTip.displayFields,
|
|
289
|
+
otherStats: (point) => {
|
|
290
|
+
const id = point.__bandIdX;
|
|
291
|
+
const facet = point.__facetId;
|
|
292
|
+
return typeof id === 'number'
|
|
293
|
+
? setup!.otherRows.statsAt(id, typeof facet === 'number' ? facet : null)
|
|
294
|
+
: null;
|
|
295
|
+
}
|
|
285
296
|
})
|
|
286
297
|
);
|
|
287
298
|
});
|
|
@@ -304,6 +315,7 @@
|
|
|
304
315
|
{y1Data}
|
|
305
316
|
colorValueData={markColor.data}
|
|
306
317
|
alphaData={opacityData}
|
|
318
|
+
share={setup?.otherRows.split ?? null}
|
|
307
319
|
colorMode={markColor.colorMode}
|
|
308
320
|
colorScaleIndex={markColor.colorScaleIndex}
|
|
309
321
|
groupIndex={setup?.groupIndex ?? 0}
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
table,
|
|
44
44
|
selection = undefined,
|
|
45
45
|
selectionResponse = undefined,
|
|
46
|
+
nulls = undefined,
|
|
46
47
|
x,
|
|
47
48
|
y,
|
|
48
49
|
fill = null,
|
|
@@ -82,6 +83,7 @@
|
|
|
82
83
|
transforms: () => transforms,
|
|
83
84
|
selection: () => selection,
|
|
84
85
|
selectionResponse: () => selectionResponse,
|
|
86
|
+
nulls: () => nulls,
|
|
85
87
|
limit: () => limit,
|
|
86
88
|
preferredXScaleType: () => preferredXScaleType,
|
|
87
89
|
preferredYScaleType: () => preferredYScaleType,
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
table,
|
|
72
72
|
selection = undefined,
|
|
73
73
|
selectionResponse = undefined,
|
|
74
|
+
nulls = undefined,
|
|
74
75
|
x,
|
|
75
76
|
y,
|
|
76
77
|
stroke = null,
|
|
@@ -124,6 +125,7 @@
|
|
|
124
125
|
transforms: () => transforms,
|
|
125
126
|
selection: () => selection,
|
|
126
127
|
selectionResponse: () => selectionResponse,
|
|
128
|
+
nulls: () => nulls,
|
|
127
129
|
limit: () => limit,
|
|
128
130
|
preferredXScaleType: () => preferredXScaleType,
|
|
129
131
|
preferredYScaleType: () => preferredYScaleType,
|
|
@@ -146,7 +148,9 @@
|
|
|
146
148
|
inferMarkScaleType(entry.y, entry.table, entry.preferredYScaleType)
|
|
147
149
|
);
|
|
148
150
|
|
|
149
|
-
// Series grouping: z overrides stroke for grouping
|
|
151
|
+
// Series grouping: z overrides stroke for grouping. A selection dims a line
|
|
152
|
+
// by its stroke colour only; dimming by a separate z field is not supported
|
|
153
|
+
// yet (see activityReads in activity-reads.ts).
|
|
150
154
|
const seriesChannelKey = entry.z != null ? 'z' : 'stroke';
|
|
151
155
|
const markSeries = new MarkSeries(
|
|
152
156
|
() => entry.z ?? entry.stroke,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<script lang="ts">
|
|
14
14
|
import type { SelectionResponse } from '../rendering/activity-reads';
|
|
15
|
+
import type { NullsResponse } from '../../types';
|
|
15
16
|
import { getContext, onDestroy, untrack } from 'svelte';
|
|
16
17
|
import { MarkLifecycleV2 } from './lifecycle/MarkLifecycleV2.svelte';
|
|
17
18
|
import { ParallelLines } from '../rendering';
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
selection?: string;
|
|
35
36
|
/** How this mark responds to selections in its group (see BaseMarkProps). */
|
|
36
37
|
selectionResponse?: SelectionResponse;
|
|
38
|
+
nulls?: NullsResponse;
|
|
37
39
|
transforms?: Transform[];
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
curve = 'linear',
|
|
48
50
|
selection = undefined,
|
|
49
51
|
selectionResponse = undefined,
|
|
52
|
+
nulls = undefined,
|
|
50
53
|
transforms = []
|
|
51
54
|
}: Props = $props();
|
|
52
55
|
|
|
@@ -64,6 +67,7 @@
|
|
|
64
67
|
scheme: () => scheme,
|
|
65
68
|
selection: () => selection,
|
|
66
69
|
selectionResponse: () => selectionResponse,
|
|
70
|
+
nulls: () => nulls,
|
|
67
71
|
transforms: () => transforms
|
|
68
72
|
});
|
|
69
73
|
|
|
@@ -10,6 +10,7 @@ export declare const parallelParity: {
|
|
|
10
10
|
reverseScales: boolean;
|
|
11
11
|
};
|
|
12
12
|
import type { SelectionResponse } from '../rendering/activity-reads';
|
|
13
|
+
import type { NullsResponse } from '../../types';
|
|
13
14
|
import type { FieldRef, ColorScheme, CurveType, Transform } from '../../types';
|
|
14
15
|
interface Props {
|
|
15
16
|
table: string;
|
|
@@ -22,6 +23,7 @@ interface Props {
|
|
|
22
23
|
selection?: string;
|
|
23
24
|
/** How this mark responds to selections in its group (see BaseMarkProps). */
|
|
24
25
|
selectionResponse?: SelectionResponse;
|
|
26
|
+
nulls?: NullsResponse;
|
|
25
27
|
transforms?: Transform[];
|
|
26
28
|
}
|
|
27
29
|
declare const ParallelCoordinatesMark: import("svelte").Component<Props, {}, "">;
|