@fjader/svelte 0.2.3 → 0.2.5

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 (167) hide show
  1. package/README.md +3 -1
  2. package/dist/animation/EntranceGate.d.ts +52 -0
  3. package/dist/animation/EntranceGate.js +93 -0
  4. package/dist/animation/TransitionClock.d.ts +61 -30
  5. package/dist/animation/TransitionClock.js +67 -29
  6. package/dist/animation/transition-log.d.ts +41 -0
  7. package/dist/animation/transition-log.js +57 -0
  8. package/dist/components/axes/AxisBottom.svelte +27 -8
  9. package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
  10. package/dist/components/axes/AxisCenter.svelte +4 -2
  11. package/dist/components/axes/AxisLeft.svelte +81 -10
  12. package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
  13. package/dist/components/axes/BandAxisTicks.svelte +4 -3
  14. package/dist/components/axes/axis-title.d.ts +30 -0
  15. package/dist/components/axes/axis-title.js +77 -0
  16. package/dist/components/charts/ChartCell.svelte +33 -6
  17. package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
  18. package/dist/components/charts/ChartContainer.svelte +55 -5
  19. package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
  20. package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
  21. package/dist/components/charts/ChartHover.svelte.js +22 -0
  22. package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
  23. package/dist/components/charts/ChartVisibility.svelte.js +67 -0
  24. package/dist/components/guides/Grid.svelte +12 -1
  25. package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
  26. package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
  27. package/dist/components/marks/AreaMarkX.svelte +18 -4
  28. package/dist/components/marks/AreaMarkY.svelte +18 -4
  29. package/dist/components/marks/BarMarkX.svelte +20 -4
  30. package/dist/components/marks/BarMarkY.svelte +20 -4
  31. package/dist/components/marks/DotsMark.svelte +8 -3
  32. package/dist/components/marks/LineMark.svelte +13 -6
  33. package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
  34. package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
  35. package/dist/components/marks/RectMark.svelte +8 -3
  36. package/dist/components/marks/RectXMark.svelte +9 -3
  37. package/dist/components/marks/RectYMark.svelte +9 -3
  38. package/dist/components/marks/RuleMarkX.svelte +31 -25
  39. package/dist/components/marks/RuleMarkY.svelte +31 -25
  40. package/dist/components/marks/TextMark.svelte +52 -14
  41. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
  42. package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
  43. package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
  44. package/dist/components/marks/lifecycle/domain-queries.js +43 -11
  45. package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
  46. package/dist/components/marks/lifecycle/domain-registration.js +17 -20
  47. package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
  48. package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
  49. package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
  50. package/dist/components/marks/lifecycle/other-split.js +112 -0
  51. package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
  52. package/dist/components/marks/lifecycle/query-builder.js +46 -23
  53. package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
  54. package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
  55. package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
  56. package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
  57. package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
  58. package/dist/components/marks/utils/reactive-config.d.ts +1 -1
  59. package/dist/components/marks/utils/reactive-config.js +9 -0
  60. package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
  61. package/dist/components/marks/utils/tooltip-helpers.js +2 -1
  62. package/dist/components/rendering/Areas.svelte +57 -19
  63. package/dist/components/rendering/Areas.svelte.d.ts +12 -0
  64. package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
  65. package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
  66. package/dist/components/rendering/Dots.svelte +23 -3
  67. package/dist/components/rendering/Dots.svelte.d.ts +6 -0
  68. package/dist/components/rendering/GridLine.svelte +13 -1
  69. package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
  70. package/dist/components/rendering/Lines.svelte +48 -13
  71. package/dist/components/rendering/Lines.svelte.d.ts +12 -0
  72. package/dist/components/rendering/ParallelLines.svelte +5 -5
  73. package/dist/components/rendering/Rects.svelte +164 -30
  74. package/dist/components/rendering/Rects.svelte.d.ts +23 -2
  75. package/dist/components/rendering/RuleX.svelte +1 -1
  76. package/dist/components/rendering/RuleY.svelte +1 -1
  77. package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
  78. package/dist/components/rendering/activity-reads.d.ts +47 -12
  79. package/dist/components/rendering/activity-reads.js +61 -19
  80. package/dist/components/rendering/uniform-sizes.d.ts +1 -1
  81. package/dist/components/rendering/uniform-sizes.js +1 -1
  82. package/dist/config/ChartConfig.svelte.d.ts +10 -1
  83. package/dist/config/ChartConfig.svelte.js +17 -0
  84. package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
  85. package/dist/gpu/GPUCoordinator.svelte.js +164 -7
  86. package/dist/gpu/MorphedChannels.d.ts +11 -28
  87. package/dist/gpu/MorphedChannels.js +72 -8
  88. package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
  89. package/dist/gpu/ScaleTemplateSystem.js +83 -6
  90. package/dist/index.d.ts +19 -4
  91. package/dist/index.js +22 -4
  92. package/dist/interactors/Interval1D.svelte +6 -1
  93. package/dist/interactors/Interval2D.svelte +6 -1
  94. package/dist/interactors/ParallelAxisSource.svelte +10 -1
  95. package/dist/interactors/Toggle.svelte +32 -3
  96. package/dist/interactors/Tooltip.svelte +138 -41
  97. package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
  98. package/dist/interactors/TooltipManager.svelte.js +46 -12
  99. package/dist/interactors/other-tooltip.d.ts +16 -0
  100. package/dist/interactors/other-tooltip.js +22 -0
  101. package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
  102. package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
  103. package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
  104. package/dist/query-engine/FieldActivity.svelte.js +175 -5
  105. package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
  106. package/dist/query-engine/FilterGroup.svelte.js +43 -3
  107. package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
  108. package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
  109. package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
  110. package/dist/query-engine/MarkHandle.svelte.js +33 -1
  111. package/dist/query-engine/QueryEngine.d.ts +225 -5
  112. package/dist/query-engine/QueryEngine.js +697 -47
  113. package/dist/query-engine/categorical-identity.d.ts +41 -0
  114. package/dist/query-engine/categorical-identity.js +145 -65
  115. package/dist/query-engine/debug/query-log.d.ts +39 -0
  116. package/dist/query-engine/debug/query-log.js +90 -0
  117. package/dist/query-engine/duckdb.d.ts +55 -8
  118. package/dist/query-engine/duckdb.js +82 -30
  119. package/dist/query-engine/index.d.ts +7 -4
  120. package/dist/query-engine/index.js +3 -3
  121. package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
  122. package/dist/query-engine/preagg/BuildQueue.js +51 -15
  123. package/dist/query-engine/preagg/Preagg.d.ts +46 -3
  124. package/dist/query-engine/preagg/Preagg.js +159 -31
  125. package/dist/query-engine/preagg/plan.d.ts +33 -1
  126. package/dist/query-engine/preagg/plan.js +158 -14
  127. package/dist/query-engine/sliced-lane.d.ts +108 -0
  128. package/dist/query-engine/sliced-lane.js +299 -0
  129. package/dist/query-engine/source-routing.d.ts +106 -0
  130. package/dist/query-engine/source-routing.js +189 -0
  131. package/dist/query-engine/toggle-predicate.d.ts +21 -0
  132. package/dist/query-engine/toggle-predicate.js +29 -1
  133. package/dist/scales/ActivityTable.d.ts +5 -1
  134. package/dist/scales/ActivityTable.js +12 -3
  135. package/dist/scales/BandScaleState.svelte.d.ts +22 -9
  136. package/dist/scales/BandScaleState.svelte.js +39 -21
  137. package/dist/scales/ChartScales.svelte.d.ts +3 -0
  138. package/dist/scales/ChartScales.svelte.js +26 -8
  139. package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
  140. package/dist/scales/ColorScaleState.svelte.js +138 -26
  141. package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
  142. package/dist/scales/LinearScaleState.svelte.js +21 -39
  143. package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
  144. package/dist/scales/ScaleGeneration.svelte.js +42 -0
  145. package/dist/scales/ScaleRegistry.svelte.js +3 -1
  146. package/dist/scales/SlotTransition.d.ts +15 -1
  147. package/dist/scales/SlotTransition.js +33 -4
  148. package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
  149. package/dist/scales/TimeScaleState.svelte.js +7 -7
  150. package/dist/scales/color-utils.d.ts +0 -3
  151. package/dist/scales/color-utils.js +0 -3
  152. package/dist/scales/entered.d.ts +8 -0
  153. package/dist/scales/entered.js +10 -0
  154. package/dist/spec/LazyQueryEngine.js +2 -19
  155. package/dist/spec/MarkRegistry.d.ts +3 -1
  156. package/dist/spec/MarkRegistry.js +13 -1
  157. package/dist/spec/PlotFromSpec.svelte +1 -0
  158. package/dist/spec/export.js +11 -2
  159. package/dist/spec/types.d.ts +43 -13
  160. package/dist/sql/bin.d.ts +25 -0
  161. package/dist/sql/bin.js +108 -5
  162. package/dist/theme.css +5 -4
  163. package/dist/transforms/TransformCompiler.d.ts +47 -2
  164. package/dist/transforms/TransformCompiler.js +70 -1
  165. package/dist/transforms/index.js +176 -42
  166. package/dist/types.d.ts +177 -2
  167. package/package.json +4 -3
@@ -178,10 +178,10 @@ const SCALE_FRAGMENTS = {
178
178
  rangeMax
179
179
  );
180
180
 
181
- // Return center of band
182
- // X: left to right (index 0 at left)
183
- // Y: bottom to top (index 0 at bottom, matching D3 convention)
184
- ${axis === 'y' ? 'return rangeMax - bandLayout.x - bandLayout.y * 0.5;' : 'return rangeMin + bandLayout.x + bandLayout.y * 0.5;'}
181
+ // Return center of band, slot 0 at the start of the range on both
182
+ // axes: the left on x, the top on y, so a y band axis reads top-down
183
+ // like a list and its exit (one past the last slot) is the list's end.
184
+ return rangeMin + bandLayout.x + bandLayout.y * 0.5;
185
185
  }
186
186
  `,
187
187
  bufferLayout: {
@@ -280,14 +280,14 @@ function _scaleFunctionName(scaleType, axis) {
280
280
  }
281
281
  /**
282
282
  * Bytes of immediate data every render pipeline declares: the chart's x and y
283
- * `ScaleParam`, 16 bytes each. Every layout declares the same size because a
283
+ * `ScaleParam`, 16 bytes each, and its entrance, a vec4. Every layout declares the same size because a
284
284
  * draw requires every declared byte to have been set, and the pass sets them
285
285
  * once, before its first draw.
286
286
  *
287
287
  * Immediates carry state shared across the pass and read by several marks: the
288
288
  * chart's scales and settings. 64 bytes are available on M-series Macs.
289
289
  */
290
- export const CHART_SCALE_IMMEDIATE_SIZE = 32;
290
+ export const CHART_SCALE_IMMEDIATE_SIZE = 48;
291
291
  /**
292
292
  * Main template system class
293
293
  */
@@ -376,6 +376,7 @@ export class ScaleTemplateSystem {
376
376
  // ===== 3. HELPER FUNCTIONS =====
377
377
 
378
378
  ${this._buildSlotTableHelpers()}
379
+ ${this._buildEntranceHelpers(scaleTypes)}
379
380
  ${this._buildBandActivityHelpers(scaleTypes)}
380
381
  ${bandHelpers}
381
382
  ${implementations}
@@ -413,6 +414,10 @@ export class ScaleTemplateSystem {
413
414
  struct ChartScales {
414
415
  x: ScaleParam,
415
416
  y: ScaleParam,
417
+ // The chart's entrance, written per pass from its entrance clock:
418
+ // x = how far a growing mark has grown, y = a growing mark's opacity,
419
+ // z = a fading mark's opacity. All 1 once the chart has entered.
420
+ entrance: vec4<f32>,
416
421
  };
417
422
  var<immediate> chartScales: ChartScales;
418
423
 
@@ -448,6 +453,57 @@ export class ScaleTemplateSystem {
448
453
  `;
449
454
  return `${structDef}\n${chartScaleDeclaration}`;
450
455
  }
456
+ /**
457
+ * The chart's entrance, for marks to apply in the vertex stage.
458
+ *
459
+ * A mark either GROWS along one axis (bars, histograms and areas, from the
460
+ * value 0 on their measure axis) or FADES (everything else). Which one is
461
+ * the mark's to say, as `axis`: 0 fades, 1 grows along x, 2 along y. The
462
+ * chart's mode is already folded into the immediates, so a chart set to
463
+ * fade hands a growing mark g = 1 and its opacity instead.
464
+ *
465
+ * The base a mark grows from is where 0 sits on screen, clamped to the plot:
466
+ * a domain that leaves 0 out grows from the edge, which is where its bars
467
+ * visibly start anyway. A log axis has no 0 and grows from its low edge.
468
+ * Decided per scale type here, so no shader evaluates log(0).
469
+ */
470
+ _buildEntranceHelpers(scaleTypes) {
471
+ const base = (axis, type) => {
472
+ const a = axis.toLowerCase();
473
+ const lo = `frameUniforms.rangeMin.${a}`;
474
+ const hi = `frameUniforms.rangeMax.${a}`;
475
+ if (type === 'band' || type === 'point')
476
+ return '0.0';
477
+ // Screen y runs downward: the low edge of a y axis is rangeMax.
478
+ if (type === 'log')
479
+ return axis === 'X' ? lo : hi;
480
+ return `clamp(${_scaleFunctionName(type, axis)}From(0.0, chartScale${axis}()), ${lo}, ${hi})`;
481
+ };
482
+ return `
483
+ // ===== ENTRANCE =====
484
+ fn entranceBaseX() -> f32 {
485
+ return ${base('X', scaleTypes.x)};
486
+ }
487
+ fn entranceBaseY() -> f32 {
488
+ return ${base('Y', scaleTypes.y)};
489
+ }
490
+ // A screen position as it is during the entrance: pulled toward the base
491
+ // on the growing axis, untouched otherwise.
492
+ fn entrancePosition(p: vec2<f32>, axis: u32) -> vec2<f32> {
493
+ let g = chartScales.entrance.x;
494
+ if (axis == 1u) {
495
+ return vec2<f32>(mix(entranceBaseX(), p.x, g), p.y);
496
+ }
497
+ if (axis == 2u) {
498
+ return vec2<f32>(p.x, mix(entranceBaseY(), p.y, g));
499
+ }
500
+ return p;
501
+ }
502
+ fn entranceAlpha(axis: u32) -> f32 {
503
+ return select(chartScales.entrance.z, chartScales.entrance.y, axis != 0u);
504
+ }
505
+ `;
506
+ }
451
507
  /**
452
508
  * `bandActivityX(value, group)` and `bandActivityY(value, group)`: how
453
509
  * selected a band or point axis's category is in the mark's filter group,
@@ -465,6 +521,9 @@ export class ScaleTemplateSystem {
465
521
  if (type !== 'band' && type !== 'point') {
466
522
  return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
467
523
  return 1.0;
524
+ }
525
+ fn bandFreeActivity${axis}(group: u32) -> f32 {
526
+ return 1.0;
468
527
  }`;
469
528
  }
470
529
  return `fn bandActivity${axis}(value: f32, group: u32) -> f32 {
@@ -472,6 +531,9 @@ export class ScaleTemplateSystem {
472
531
  return 1.0;
473
532
  }
474
533
  return identityActivity(u32(chartScale${axis}().p4), group, u32(value));
534
+ }
535
+ fn bandFreeActivity${axis}(group: u32) -> f32 {
536
+ return identityFreeActivity(u32(chartScale${axis}().p4), group);
475
537
  }`;
476
538
  };
477
539
  return `
@@ -554,6 +616,21 @@ export class ScaleTemplateSystem {
554
616
  }
555
617
  return identitySlots[actRef + n * group + id];
556
618
  }
619
+
620
+ fn identityFreeActivity(base: u32, group: u32) -> f32 {
621
+ // The last slot of the group's run, past every id: 1 while the group
622
+ // selects nothing on the field, 0 while it selects anything, at the
623
+ // selection clock (FieldActivity's FREE SLOT). 1 without a block.
624
+ if (group == ${NO_GROUP}u) {
625
+ return 1.0;
626
+ }
627
+ let actRef = u32(identitySlots[base + 1u]);
628
+ let n = u32(identitySlots[base + 2u]);
629
+ if (n == 0u) {
630
+ return 1.0;
631
+ }
632
+ return identitySlots[actRef + n * group + n - 1u];
633
+ }
557
634
  `;
558
635
  }
559
636
  _buildBandHelpers(scaleTypes) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,11 @@
1
- import type { DuckDBConnection } from './query-engine';
1
+ import type { DuckDBConnection, InitDuckDBOptions, QueryEngineOptions } from './query-engine';
2
+ export interface FjaderOptions extends InitDuckDBOptions {
3
+ /** An existing connection (or a promise of one); skips internal DuckDB-WASM init. */
4
+ connection?: DuckDBConnection | Promise<DuckDBConnection>;
5
+ /** Advanced: query engine tuning (see QueryEngineOptions). */
6
+ engine?: QueryEngineOptions;
7
+ }
8
+ type FjaderInit = DuckDBConnection | Promise<DuckDBConnection> | FjaderOptions;
2
9
  /**
3
10
  * Initialize Fjader: QueryEngine, ScaleRegistry, GPUCoordinator, FilterGroups.
4
11
  * Call once in a top-level component's <script> block.
@@ -6,10 +13,14 @@ import type { DuckDBConnection } from './query-engine';
6
13
  * import { initFjader } from '@fjader/svelte';
7
14
  * const { queryEngine } = initFjader();
8
15
  *
9
- * Pass an existing DuckDB connection to skip internal DuckDB-WASM init:
10
- * const { queryEngine } = initFjader(myConnection);
16
+ * Serve the DuckDB-WASM bundles yourself instead of loading them from jsDelivr:
17
+ * initFjader({ bundles: { mvp: { mainModule, mainWorker }, eh: { … } } });
18
+ *
19
+ * Pass an existing DuckDB connection to skip internal DuckDB-WASM init, e.g. to
20
+ * keep the AsyncDuckDB instance for registerFileBuffer:
21
+ * const { queryEngine } = initFjader(connectionFrom(await db.connect()));
11
22
  */
12
- export declare function initFjader(connection?: DuckDBConnection | Promise<DuckDBConnection>): {
23
+ export declare function initFjader(init?: FjaderInit): {
13
24
  queryEngine: import("./query-engine").QueryEngine;
14
25
  filterGroups: import("./query-engine").FilterGroupRegistry;
15
26
  scaleRegistry: import("./scales").ScaleRegistry;
@@ -19,5 +30,9 @@ export * from './components';
19
30
  export * from './transforms';
20
31
  export * from './sql/bin';
21
32
  export * from './types';
33
+ export { getFilterGroups } from './query-engine';
34
+ export { connectionFrom } from './query-engine';
35
+ export type { DuckDBConnection, InitDuckDBOptions } from './query-engine';
36
+ export type { FilterGroupRegistry, FilterGroup, FilterItem } from './query-engine';
22
37
  export { Fjader, render } from './spec';
23
38
  export type { FjaderSpec, FjaderHandle, RenderOptions } from './spec';
package/dist/index.js CHANGED
@@ -2,6 +2,14 @@
2
2
  import { setScaleRegistry } from './scales';
3
3
  import { setGPUCoordinator } from './gpu';
4
4
  import { setQueryContext, initDuckDB } from './query-engine';
5
+ /** A connection has query(), a promise has then(); anything else is options. */
6
+ function toOptions(init) {
7
+ if (!init)
8
+ return {};
9
+ if ('query' in init || 'then' in init)
10
+ return { connection: init };
11
+ return init;
12
+ }
5
13
  /**
6
14
  * Initialize Fjader: QueryEngine, ScaleRegistry, GPUCoordinator, FilterGroups.
7
15
  * Call once in a top-level component's <script> block.
@@ -9,11 +17,16 @@ import { setQueryContext, initDuckDB } from './query-engine';
9
17
  * import { initFjader } from '@fjader/svelte';
10
18
  * const { queryEngine } = initFjader();
11
19
  *
12
- * Pass an existing DuckDB connection to skip internal DuckDB-WASM init:
13
- * const { queryEngine } = initFjader(myConnection);
20
+ * Serve the DuckDB-WASM bundles yourself instead of loading them from jsDelivr:
21
+ * initFjader({ bundles: { mvp: { mainModule, mainWorker }, eh: { … } } });
22
+ *
23
+ * Pass an existing DuckDB connection to skip internal DuckDB-WASM init, e.g. to
24
+ * keep the AsyncDuckDB instance for registerFileBuffer:
25
+ * const { queryEngine } = initFjader(connectionFrom(await db.connect()));
14
26
  */
15
- export function initFjader(connection) {
16
- const { queryEngine, filterGroups } = setQueryContext(connection ?? initDuckDB());
27
+ export function initFjader(init) {
28
+ const { connection, bundles, engine } = toOptions(init);
29
+ const { queryEngine, filterGroups } = setQueryContext(connection ?? initDuckDB({ bundles }), engine);
17
30
  const scaleRegistry = setScaleRegistry();
18
31
  const gpuCoordinator = setGPUCoordinator({ msaaSampleCount: 4 });
19
32
  gpuCoordinator.setScaleRegistry(scaleRegistry);
@@ -25,5 +38,10 @@ export * from './components';
25
38
  export * from './transforms';
26
39
  export * from './sql/bin';
27
40
  export * from './types';
41
+ // Selection state: the registry initFjader() returns, for code outside it (is anything
42
+ // brushed? which groups exist?). Read from the same context.
43
+ export { getFilterGroups } from './query-engine';
44
+ // DuckDB: adapt an app-owned duckdb-wasm connection for initFjader(connection).
45
+ export { connectionFrom } from './query-engine';
28
46
  // Spec-driven rendering API
29
47
  export { Fjader, render } from './spec';
@@ -97,7 +97,12 @@
97
97
  column: typeof resolvedField === 'string' ? resolvedField : (resolvedField?.toString() ?? ''),
98
98
  label: typeof resolvedField === 'string' ? resolvedField : resolvedField?.toString(),
99
99
  values: value ? [(value as number[])[0], (value as number[])[1]] : []
100
- })
100
+ }),
101
+ // The clause was removed from outside (clearAll, a filter pill): take the
102
+ // rectangle down too, which only this control holds.
103
+ onCleared: () => {
104
+ if (brush.active) clearBrush();
105
+ }
101
106
  });
102
107
 
103
108
  // Pointer state (reused to avoid GC)
@@ -97,7 +97,12 @@
97
97
  yColumn: resolvedYField,
98
98
  xValues: (value as unknown[][])?.[0],
99
99
  yValues: (value as unknown[][])?.[1]
100
- })
100
+ }),
101
+ // The clause was removed from outside (clearAll, a filter pill): take the
102
+ // rectangle down too, which only this control holds.
103
+ onCleared: () => {
104
+ if (brush.active) clearBrush();
105
+ }
101
106
  });
102
107
 
103
108
  // Pointer state (reused to avoid GC)
@@ -83,7 +83,16 @@
83
83
  column,
84
84
  label: column,
85
85
  values: value ? [(value as number[])[0], (value as number[])[1]] : []
86
- })
86
+ }),
87
+ // The clause was removed from outside (clearAll, a filter pill): take this
88
+ // axis's rectangle down too, which only ParallelBrushes' state holds.
89
+ onCleared: () => {
90
+ const axis = axes.get(column);
91
+ if (!axis) return;
92
+ axis.brush.clear();
93
+ axis.lastEmitted.min = null;
94
+ axis.lastEmitted.max = null;
95
+ }
87
96
  });
88
97
 
89
98
  axes.set(column, { handle, brush: new BrushState1D(), lastEmitted: { min: null, max: null } });
@@ -70,7 +70,7 @@
70
70
  function bandCategoryAt(
71
71
  axis: 'x' | 'y',
72
72
  innerPos: number
73
- ): { field: string; value: unknown } | null {
73
+ ): { field: string; value: unknown; otherId: number | null } | null {
74
74
  if (chart.scaleTypes?.[axis] !== 'band') return null;
75
75
  const bandState = chart[`${axis}ScaleState`] as BandScaleState;
76
76
  const extent = axis === 'x' ? chart.innerWidth : chart.innerHeight;
@@ -79,7 +79,28 @@
79
79
  const id = bandState.idAtSlot(slot);
80
80
  // undefined is "no category here"; null is the NULL category, a real one.
81
81
  const value = id == null ? undefined : bandState.categoryAt(id);
82
- return value === undefined ? null : { field: bandState.sourceField, value };
82
+ if (value === undefined) return null;
83
+ // The axis's Other bucket: its label is no value, so the click selects
84
+ // what it holds instead.
85
+ const otherId = id != null && id === bandState.otherId ? id : null;
86
+ return { field: bandState.sourceField, value, otherId };
87
+ }
88
+
89
+ /**
90
+ * The Other bucket an inner click hit, or null: the hit row's band id on an
91
+ * axis of the declared field, when it is that axis's Other. The label in
92
+ * the row can't say, so the tooltip keeps the raw id beside it.
93
+ */
94
+ function otherHit(point: Record<string, unknown>): number | null {
95
+ if (!declaredField) return null;
96
+ for (const axis of ['x', 'y'] as const) {
97
+ if (chart.scaleTypes?.[axis] !== 'band') continue;
98
+ const bandState = chart[`${axis}ScaleState`] as BandScaleState;
99
+ if (bandState.sourceField !== declaredField) continue;
100
+ const id = point[axis === 'x' ? '__bandIdX' : '__bandIdY'];
101
+ if (typeof id === 'number' && id === bandState.otherId) return id;
102
+ }
103
+ return null;
83
104
  }
84
105
 
85
106
  chart.onChartClick = (event) => {
@@ -91,6 +112,12 @@
91
112
  // Path 1: Inner area click — use tooltip hit data (don't require visible —
92
113
  // pointer micro-movement during click hides tooltip but data stays valid)
93
114
  if (xInBounds && yInBounds && tm?.currentPoint) {
115
+ const otherId = otherHit(tm.currentPoint);
116
+ if (otherId !== null) {
117
+ const facetId = tm.currentPoint.__facetId;
118
+ declared.toggleOther(otherId, typeof facetId === 'number' ? facetId : null, isMulti);
119
+ return;
120
+ }
94
121
  const tuple = fields.map((f) => tm.currentPoint![f]);
95
122
  // A missing field is undefined; null is the NULL category, selectable.
96
123
  if (tuple.some((v) => v === undefined)) return;
@@ -106,7 +133,9 @@
106
133
  ? bandCategoryAt('x', innerPoint.x)
107
134
  : null;
108
135
  if (hit) {
109
- toggleFor(hit.field)?.toggle([hit.value], isMulti);
136
+ const toggle = toggleFor(hit.field);
137
+ if (hit.otherId !== null) toggle?.toggleOther(hit.otherId, null, isMulti);
138
+ else toggle?.toggle([hit.value], isMulti);
110
139
  return;
111
140
  }
112
141
 
@@ -2,8 +2,12 @@
2
2
  Tooltip.svelte - Display component for data tooltips.
3
3
 
4
4
  Reads from TooltipManager (via context) and renders a floating tooltip
5
- at the cursor position. Uses CSS anchor positioning for automatic
6
- edge-aware placement.
5
+ at the cursor. It is a manual popover, so it paints in the top layer and no
6
+ overflow, scroll area, stacking context or transformed ancestor clips it. It
7
+ stays in the chart's DOM, so inherited theme variables and scoped styles still
8
+ apply. Placed in viewport pixels with the `translate` property (no layout per
9
+ move), flipped and clamped against the viewport, and measured by a
10
+ ResizeObserver rather than by reading its size.
7
11
 
8
12
  Always renders one point at a time (stable layout). When multiple
9
13
  points match, colored dots appear at the bottom and the user can
@@ -14,6 +18,7 @@
14
18
  import { format as d3format } from 'd3-format';
15
19
  import { categoryLabel } from '../query-engine/categorical-identity';
16
20
  import type { ChartContext, ContinuousScaleState } from '../types';
21
+ import { otherTooltipRows } from './other-tooltip';
17
22
 
18
23
  const commaFormat = d3format(',.4~g');
19
24
 
@@ -36,40 +41,76 @@
36
41
  let colorField = $derived(tm?.currentColorField ?? null);
37
42
  let channelFields = $derived(tm?.currentChannelFields ?? null);
38
43
  let displayFields = $derived(tm?.currentDisplayFields ?? null);
39
- let px = $derived(tm?.pointerScreenX ?? 0);
40
- let py = $derived(tm?.pointerScreenY ?? 0);
44
+ let px = $derived(tm?.pointerClientX ?? 0);
45
+ let py = $derived(tm?.pointerClientY ?? 0);
41
46
 
42
- // High-water mark for tooltip width — grows but never shrinks.
43
- let tooltipEl: HTMLDivElement;
47
+ // The tooltip's size, from a ResizeObserver: read after the browser's own
48
+ // layout, so measuring forces none. Its width is a high-water mark (grows,
49
+ // never shrinks) so it does not jitter between points: content wider than
50
+ // min-width makes the observed width exceed it, and min-width follows.
44
51
  let minWidth = $state(0);
45
52
  let tooltipW = $state(0);
46
53
  let tooltipH = $state(0);
47
54
 
48
- $effect(() => {
49
- // Dependencies: re-measure when focused point or index changes
50
- void point;
51
- void focusedIndex;
52
- if (!visible || !tooltipEl) return;
53
- const prev = tooltipEl.style.minWidth;
54
- tooltipEl.style.minWidth = '0px';
55
- const natural = tooltipEl.offsetWidth;
56
- tooltipEl.style.minWidth = prev;
57
- if (natural > minWidth) minWidth = natural;
58
- tooltipW = tooltipEl.offsetWidth;
59
- tooltipH = tooltipEl.offsetHeight;
60
- });
55
+ function measure(el: HTMLElement) {
56
+ const observer = new ResizeObserver((entries) => {
57
+ const box = entries[entries.length - 1].borderBoxSize[0];
58
+ // A closed popover is display: none and reports 0: keep the last size.
59
+ if (box.inlineSize === 0) return;
60
+ tooltipW = box.inlineSize;
61
+ tooltipH = box.blockSize;
62
+ if (box.inlineSize > minWidth) minWidth = box.inlineSize;
63
+ });
64
+ observer.observe(el);
65
+ return () => observer.disconnect();
66
+ }
67
+
68
+ // The viewport, without the scrollbars. Read when the tooltip opens and on
69
+ // resize, never per move.
70
+ let viewW = $state(0);
71
+ let viewH = $state(0);
72
+ function readViewport() {
73
+ const root = document.documentElement;
74
+ viewW = root.clientWidth;
75
+ viewH = root.clientHeight;
76
+ }
77
+
78
+ // Open in the top layer while shown; closed again once it has faded out, so a
79
+ // page of charts keeps at most the hovered one's tooltip there.
80
+ function popover(el: HTMLElement) {
81
+ if (visible && !el.matches(':popover-open')) {
82
+ readViewport();
83
+ el.showPopover();
84
+ }
85
+ }
86
+ function handleTransitionEnd(event: TransitionEvent) {
87
+ const el = event.currentTarget as HTMLElement;
88
+ if (event.propertyName === 'opacity' && !visible && el.matches(':popover-open')) {
89
+ el.hidePopover();
90
+ }
91
+ }
61
92
 
62
- // Flip when tooltip would overflow chart bounds, with hysteresis to prevent jitter
93
+ // Left of the cursor when it fits, else right; below it, else above. With
94
+ // hysteresis so it does not jitter at the edge. Then clamped into the viewport,
95
+ // for a tooltip that fits on neither side.
63
96
  const GAP = 12;
64
97
  const HYSTERESIS = 40;
65
- let flippedX = $state(true);
66
- let flippedY = $state(false);
67
- $effect(() => {
98
+ const MARGIN = 8;
99
+ let leftOfCursor = true;
100
+ let aboveCursor = false;
101
+ const flippedX = $derived.by(() => {
68
102
  const spaceLeft = px - tooltipW - GAP;
69
- flippedX = spaceLeft >= 0 && (!flippedX ? spaceLeft >= HYSTERESIS : true);
70
- const spaceY = chart.height - py - tooltipH;
71
- flippedY = spaceY < 0 || (flippedY && spaceY < HYSTERESIS);
103
+ leftOfCursor = spaceLeft >= 0 && (leftOfCursor || spaceLeft >= HYSTERESIS);
104
+ return leftOfCursor;
72
105
  });
106
+ const flippedY = $derived.by(() => {
107
+ const spaceBelow = viewH - py - tooltipH;
108
+ aboveCursor = spaceBelow < 0 || (aboveCursor && spaceBelow < HYSTERESIS);
109
+ return aboveCursor;
110
+ });
111
+ const clamp = (v: number, max: number) => Math.max(MARGIN, Math.min(v, max - MARGIN));
112
+ const left = $derived(clamp(flippedX ? px - tooltipW - GAP : px + GAP, viewW - tooltipW));
113
+ const top = $derived(clamp(flippedY ? py - tooltipH + 4 : py - 4, viewH - tooltipH));
73
114
 
74
115
  // Transform origin: corner nearest the anchor point
75
116
  let originX = $derived(flippedX ? '100%' : '0%');
@@ -95,7 +136,8 @@
95
136
  if (!point) return [];
96
137
  if (displayFields) return displayFields.filter((k) => k in point);
97
138
  if (fields) return fields.filter((k) => k in point);
98
- return Object.keys(point);
139
+ // Keys starting with `_` are kept for clicks (a band's raw id), never shown.
140
+ return Object.keys(point).filter((k) => !k.startsWith('_'));
99
141
  });
100
142
 
101
143
  // Scale-aware formatters for numeric values (same as MarginLabel)
@@ -110,6 +152,14 @@
110
152
  : null
111
153
  );
112
154
 
155
+ // An Other bar's rows, after the point's own: written with the value axis's format.
156
+ let otherRows = $derived.by(() => {
157
+ const stats = tm?.focusedOtherStats;
158
+ if (!stats || !point) return [];
159
+ const format = ('__bandIdX' in point ? yFormat : xFormat) ?? commaFormat;
160
+ return otherTooltipRows(stats, format);
161
+ });
162
+
113
163
  function formatValue(key: string, value: unknown) {
114
164
  if (formatters[key]) return formatters[key](value);
115
165
  if (typeof value === 'number') {
@@ -123,20 +173,23 @@
123
173
 
124
174
  const channelIcons: Record<string, string> = { x: '\u2192', y: '\u2191' };
125
175
 
126
- // Computed style values — kept here so Prettier can't break them by line-wrapping
127
- const translateX = $derived(flippedX ? 'calc(-100% - 12px)' : '12px');
128
- const translateY = $derived(flippedY ? 'calc(-100% + 4px)' : '-4px');
129
- const translate = $derived(`${translateX} ${translateY}`);
176
+ // Computed style values — kept here so Prettier can't break them by line-wrapping.
177
+ // `translate`, not `transform` or left/top: it moves the box without layout, and
178
+ // it applies after `scale`, so the pop-in scales about the box's own corner.
179
+ const translate = $derived(`${left}px ${top}px`);
130
180
  const transformOrigin = $derived(`${originX} ${originY}`);
131
181
  </script>
132
182
 
133
- <!-- Tooltip positioned via JS with flip detection -->
183
+ <svelte:window onresize={readViewport} />
184
+
185
+ <!-- In the top layer, placed in viewport pixels with flip detection -->
134
186
  <div
135
187
  class="tooltip"
136
188
  class:visible
137
- bind:this={tooltipEl}
138
- style:left="{px}px"
139
- style:top="{py}px"
189
+ popover="manual"
190
+ {@attach measure}
191
+ {@attach popover}
192
+ ontransitionend={handleTransitionEnd}
140
193
  style:translate
141
194
  style:transform-origin={transformOrigin}
142
195
  style:min-width="{minWidth}px"
@@ -155,6 +208,12 @@
155
208
  </span>
156
209
  </div>
157
210
  {/each}
211
+ {#each otherRows as row (row.key)}
212
+ <div class="row">
213
+ <span class="key">{row.key}</span>
214
+ <span class="value">{row.value}</span>
215
+ </div>
216
+ {/each}
158
217
  <div class="dots" class:show={isMulti}>
159
218
  {#each matched as m, i (i)}
160
219
  <span
@@ -168,24 +227,53 @@
168
227
 
169
228
  <style>
170
229
  .tooltip {
171
- position: absolute;
172
- background: var(--fjader-tooltip-bg, rgba(0, 0, 0, 0.88));
173
- color: var(--fjader-tooltip-text, #ffffff);
230
+ /* A popover in the top layer: fixed to the viewport at its origin, placed by
231
+ translate. Resets the UA popover box (inset, margin, border, overflow). */
232
+ position: fixed;
233
+ inset: 0 auto auto 0;
234
+ margin: 0;
235
+ border: 0;
236
+ overflow: visible;
237
+ /* Colours, highest first: the app's --fjader-tooltip-bg/-text; the mode a
238
+ data-fjader-tooltip ancestor picks; contrast. light-dark() resolves against
239
+ the color-scheme the tooltip inherits from the chart's DOM ancestors. */
240
+ background: var(
241
+ --fjader-tooltip-bg,
242
+ var(--_fjader-tooltip-bg, light-dark(rgba(0, 0, 0, 0.88), rgba(255, 255, 255, 0.92)))
243
+ );
244
+ color: var(--fjader-tooltip-text, var(--_fjader-tooltip-text, light-dark(#ffffff, #111827)));
174
245
  padding: 6px 10px;
175
246
  border-radius: 4px;
176
247
  font-family: var(--fjader-font, system-ui, sans-serif);
177
248
  font-size: 12px;
178
249
  line-height: 1.4;
179
250
  pointer-events: none;
180
- z-index: 1000;
181
251
  white-space: nowrap;
182
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
252
+ box-shadow:
253
+ 0 0 0 1px var(--_fjader-tooltip-ring, transparent),
254
+ 0 2px 8px rgba(0, 0, 0, 0.25);
183
255
  opacity: 0;
184
256
  scale: 0.78;
185
257
  transition:
186
258
  opacity 100ms ease-out,
187
259
  scale 200ms cubic-bezier(0.34, 1.3, 0.64, 1);
188
- will-change: opacity, scale;
260
+ will-change: opacity, scale, translate;
261
+ }
262
+
263
+ /* The mode, set on any ancestor (the page, a panel, a chart through ChartContainer's
264
+ tooltipScheme): custom properties inherit, so the nearest one wins. `contrast` is
265
+ the opposite of the scheme, `follow` the same, with a ring to separate it from a
266
+ background of its own colour. */
267
+ :global([data-fjader-tooltip='contrast']) {
268
+ --_fjader-tooltip-bg: light-dark(rgba(0, 0, 0, 0.88), rgba(255, 255, 255, 0.92));
269
+ --_fjader-tooltip-text: light-dark(#ffffff, #111827);
270
+ --_fjader-tooltip-ring: transparent;
271
+ }
272
+
273
+ :global([data-fjader-tooltip='follow']) {
274
+ --_fjader-tooltip-bg: light-dark(rgba(255, 255, 255, 0.96), rgba(31, 41, 55, 0.96));
275
+ --_fjader-tooltip-text: light-dark(#111827, #f9fafb);
276
+ --_fjader-tooltip-ring: light-dark(rgba(17, 24, 39, 0.1), rgba(255, 255, 255, 0.12));
189
277
  }
190
278
 
191
279
  .tooltip.visible {
@@ -193,6 +281,15 @@
193
281
  scale: 1;
194
282
  }
195
283
 
284
+ /* Opening the popover takes it out of display: none, which runs no transition:
285
+ start the pop-in from the hidden look. */
286
+ @starting-style {
287
+ .tooltip.visible {
288
+ opacity: 0;
289
+ scale: 0.78;
290
+ }
291
+ }
292
+
196
293
  .row {
197
294
  display: flex;
198
295
  gap: 8px;