svelteplot 0.11.0 → 0.11.1

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 (86) hide show
  1. package/dist/Mark.svelte +6 -1
  2. package/dist/Mark.svelte.d.ts +1 -1
  3. package/dist/constants.js +2 -0
  4. package/dist/core/Plot.svelte +31 -3
  5. package/dist/helpers/arrowPath.js +10 -5
  6. package/dist/helpers/autoScales.js +4 -2
  7. package/dist/helpers/autoTicks.js +4 -4
  8. package/dist/helpers/autoTimeFormat.js +22 -12
  9. package/dist/helpers/colors.d.ts +4 -4
  10. package/dist/helpers/facets.d.ts +42 -1
  11. package/dist/helpers/facets.js +83 -0
  12. package/dist/helpers/math.js +1 -1
  13. package/dist/helpers/noise.js +1 -1
  14. package/dist/helpers/roundedRect.js +1 -1
  15. package/dist/helpers/scales.d.ts +1 -0
  16. package/dist/helpers/scales.js +8 -5
  17. package/dist/helpers/time.js +1 -1
  18. package/dist/helpers/typeChecks.d.ts +1 -0
  19. package/dist/helpers/typeChecks.js +3 -0
  20. package/dist/marks/Area.svelte.d.ts +1 -1
  21. package/dist/marks/AreaX.svelte.d.ts +1 -1
  22. package/dist/marks/AreaY.svelte.d.ts +1 -1
  23. package/dist/marks/Arrow.svelte.d.ts +1 -1
  24. package/dist/marks/AxisX.svelte +8 -3
  25. package/dist/marks/AxisX.svelte.d.ts +1 -1
  26. package/dist/marks/AxisY.svelte +8 -3
  27. package/dist/marks/AxisY.svelte.d.ts +1 -1
  28. package/dist/marks/BarX.svelte.d.ts +1 -1
  29. package/dist/marks/BarY.svelte.d.ts +1 -1
  30. package/dist/marks/BollingerX.svelte.d.ts +1 -1
  31. package/dist/marks/BollingerY.svelte.d.ts +1 -1
  32. package/dist/marks/BoxY.svelte.d.ts +1 -1
  33. package/dist/marks/Brush.svelte.d.ts +1 -1
  34. package/dist/marks/Cell.svelte.d.ts +1 -1
  35. package/dist/marks/CellX.svelte.d.ts +1 -1
  36. package/dist/marks/CellY.svelte.d.ts +1 -1
  37. package/dist/marks/CustomMark.svelte.d.ts +1 -1
  38. package/dist/marks/DifferenceY.svelte.d.ts +1 -1
  39. package/dist/marks/Dot.svelte.d.ts +1 -1
  40. package/dist/marks/DotX.svelte.d.ts +1 -1
  41. package/dist/marks/DotY.svelte.d.ts +1 -1
  42. package/dist/marks/Frame.svelte.d.ts +1 -1
  43. package/dist/marks/Geo.svelte.d.ts +1 -1
  44. package/dist/marks/GridX.svelte.d.ts +1 -1
  45. package/dist/marks/GridY.svelte.d.ts +1 -1
  46. package/dist/marks/HTMLTooltip.svelte +28 -25
  47. package/dist/marks/Image.svelte.d.ts +1 -1
  48. package/dist/marks/Line.svelte.d.ts +1 -1
  49. package/dist/marks/LineX.svelte.d.ts +1 -1
  50. package/dist/marks/LineY.svelte.d.ts +1 -1
  51. package/dist/marks/Link.svelte.d.ts +1 -1
  52. package/dist/marks/Pointer.svelte +31 -29
  53. package/dist/marks/Rect.svelte.d.ts +1 -1
  54. package/dist/marks/RectX.svelte.d.ts +1 -1
  55. package/dist/marks/RectY.svelte.d.ts +1 -1
  56. package/dist/marks/RuleX.svelte.d.ts +1 -1
  57. package/dist/marks/RuleY.svelte.d.ts +1 -1
  58. package/dist/marks/Spike.svelte.d.ts +1 -1
  59. package/dist/marks/Text.svelte.d.ts +2 -2
  60. package/dist/marks/TickX.svelte.d.ts +1 -1
  61. package/dist/marks/TickY.svelte.d.ts +1 -1
  62. package/dist/marks/Trail.svelte.d.ts +1 -1
  63. package/dist/marks/Vector.svelte.d.ts +1 -1
  64. package/dist/marks/WaffleX.svelte.d.ts +1 -1
  65. package/dist/marks/WaffleY.svelte.d.ts +1 -1
  66. package/dist/marks/helpers/Box.svelte.d.ts +1 -1
  67. package/dist/marks/helpers/MarkerPath.svelte.d.ts +1 -1
  68. package/dist/marks/helpers/Regression.svelte +2 -1
  69. package/dist/marks/helpers/trail.js +1 -1
  70. package/dist/marks/helpers/waffle.js +1 -1
  71. package/dist/regression/polynomial.js +2 -2
  72. package/dist/transforms/bollinger.js +6 -3
  73. package/dist/transforms/density.js +3 -3
  74. package/dist/transforms/group.d.ts +1 -1
  75. package/dist/transforms/interval.d.ts +2 -2
  76. package/dist/transforms/jitter.d.ts +3 -3
  77. package/dist/transforms/map.d.ts +3 -3
  78. package/dist/transforms/map.js +2 -2
  79. package/dist/transforms/normalize.d.ts +2 -2
  80. package/dist/transforms/normalize.js +1 -1
  81. package/dist/transforms/select.d.ts +7 -7
  82. package/dist/transforms/window.d.ts +2 -2
  83. package/dist/types/mark.d.ts +2 -2
  84. package/dist/types/plot.d.ts +1 -1
  85. package/dist/types/scale.d.ts +2 -2
  86. package/package.json +181 -179
package/dist/Mark.svelte CHANGED
@@ -297,7 +297,12 @@
297
297
  : plot.scales[scale].fn(value)
298
298
  : value;
299
299
 
300
- out.valid = out.valid && (scale === 'color' || isValid(value));
300
+ out.valid =
301
+ out.valid &&
302
+ (scale === 'color' ||
303
+ scale === 'fx' ||
304
+ scale === 'fy' ||
305
+ isValid(value));
301
306
 
302
307
  // apply dx/dy transform
303
308
  (out as any)[channel] =
@@ -14,7 +14,7 @@ declare function $$render<Datum extends DataRecord>(): {
14
14
  fill: ChannelAccessor<Datum>;
15
15
  fillOpacity: import("./types/index.js").ConstantAccessor<number, Datum>;
16
16
  fontFamily: import("./types/index.js").ConstantAccessor<import("csstype").Property.FontFamily, Datum>;
17
- fontSize: import("./types/index.js").ConstantAccessor<number | "-moz-initial" | "inherit" | "initial" | "revert" | "revert-layer" | "unset" | "math" | (string & {}) | "large" | "medium" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "larger" | "smaller", Datum>;
17
+ fontSize: import("./types/index.js").ConstantAccessor<import("csstype").Property.FontSize<number>, Datum>;
18
18
  fontStyle: import("./types/index.js").ConstantAccessor<import("csstype").Property.FontStyle, Datum>;
19
19
  fontVariant: import("./types/index.js").ConstantAccessor<import("csstype").Property.FontVariant, Datum>;
20
20
  fontWeight: import("./types/index.js").ConstantAccessor<import("csstype").Property.FontWeight, Datum>;
package/dist/constants.js CHANGED
@@ -68,6 +68,7 @@ export const VALID_SCALE_TYPES = {
68
68
  'log',
69
69
  'symlog',
70
70
  'time',
71
+ 'utc',
71
72
  'ordinal',
72
73
  'band',
73
74
  'point',
@@ -80,6 +81,7 @@ export const VALID_SCALE_TYPES = {
80
81
  'log',
81
82
  'symlog',
82
83
  'time',
84
+ 'utc',
83
85
  'ordinal',
84
86
  'band',
85
87
  'point',
@@ -11,6 +11,7 @@
11
11
  import { setContext } from 'svelte';
12
12
  import { SvelteMap } from 'svelte/reactivity';
13
13
  import { writable } from 'svelte/store';
14
+ import { scaleBand } from 'd3-scale';
14
15
 
15
16
  import type {
16
17
  PlotOptions,
@@ -28,7 +29,7 @@
28
29
  import FacetGrid from './FacetGrid.svelte';
29
30
 
30
31
  import mergeDeep from '../helpers/mergeDeep.js';
31
- import { computeScales, projectXY } from '../helpers/scales.js';
32
+ import { computeScales, normalizeScaleFn, projectXY } from '../helpers/scales.js';
32
33
  import { CHANNEL_SCALE, SCALES } from '../constants.js';
33
34
  import { getPlotDefaults, setPlotDefaults } from '../hooks/plotDefaults.js';
34
35
  import { maybeNumber } from '../helpers/index.js';
@@ -285,11 +286,38 @@
285
286
  marks,
286
287
  DEFAULTS
287
288
  );
289
+ // Fix fx/fy scale ranges: computeScales creates them with empty ranges
290
+ // because getScaleRange has no case for fx/fy. We set the correct range
291
+ // here using overall plotWidth/plotHeight, matching FacetGrid's layout.
292
+ if (scales.fx.domain.length > 0) {
293
+ const fxOpts = plotOptions.fx;
294
+ const fxPaddingInner = fxOpts?.paddingInner ?? fxOpts?.padding ?? 0.1;
295
+ const fxFn = scaleBand()
296
+ .domain(scales.fx.domain as string[])
297
+ .paddingOuter(0)
298
+ .paddingInner(scales.fx.domain.length > 1 ? fxPaddingInner : 0)
299
+ .rangeRound([0, plotWidth]) as any;
300
+ fxFn.ticks = () => scales.fx.domain;
301
+ scales.fx.fn = normalizeScaleFn(fxFn);
302
+ scales.fx.range = fxFn.range();
303
+ }
304
+ if (scales.fy.domain.length > 0) {
305
+ const fyOpts = plotOptions.fy;
306
+ const fyPaddingInner = fyOpts?.paddingInner ?? fyOpts?.padding ?? 0.1;
307
+ const fyFn = scaleBand()
308
+ .domain(scales.fy.domain as string[])
309
+ .paddingOuter(0)
310
+ .paddingInner(scales.fy.domain.length > 1 ? fyPaddingInner : 0)
311
+ .rangeRound([0, plotHeight]) as any;
312
+ fyFn.ticks = () => scales.fy.domain;
313
+ scales.fy.fn = normalizeScaleFn(fyFn);
314
+ scales.fy.range = fyFn.range();
315
+ }
288
316
  const colorSymbolRedundant =
289
317
  scales.color.uniqueScaleProps?.size === 1 &&
290
318
  scales.symbol.uniqueScaleProps?.size === 1 &&
291
- [...scales.color.uniqueScaleProps?.values()][0] ===
292
- [...scales.symbol.uniqueScaleProps?.values()][0];
319
+ [...(scales.color.uniqueScaleProps?.values() ?? [])][0] ===
320
+ [...(scales.symbol.uniqueScaleProps?.values() ?? [])][0];
293
321
  return {
294
322
  options: plotOptions,
295
323
  width,
@@ -53,16 +53,21 @@ export function arrowPath(x1, y1, x2, y2, insetStart, insetEnd, headAngle, headL
53
53
  if (insetEnd) {
54
54
  const [x, y] = circleCircleIntersect([cx, cy, r], [x2, y2, insetEnd], sign * Math.sign(insetEnd));
55
55
  lineAngle += Math.atan2(y - cy, x - cx) - Math.atan2(y2 - cy, x2 - cx);
56
- ((x2 = x), (y2 = y));
56
+ x2 = x;
57
+ y2 = y;
57
58
  }
58
59
  }
59
60
  else {
60
61
  // For inset straight arrows, offset along the straight line.
61
62
  const dx = x2 - x1, dy = y2 - y1, d = Math.hypot(dx, dy);
62
- if (insetStart)
63
- ((x1 += (dx / d) * insetStart), (y1 += (dy / d) * insetStart));
64
- if (insetEnd)
65
- ((x2 -= (dx / d) * insetEnd), (y2 -= (dy / d) * insetEnd));
63
+ if (insetStart) {
64
+ x1 += (dx / d) * insetStart;
65
+ y1 += (dy / d) * insetStart;
66
+ }
67
+ if (insetEnd) {
68
+ x2 -= (dx / d) * insetEnd;
69
+ y2 -= (dy / d) * insetEnd;
70
+ }
66
71
  }
67
72
  }
68
73
  // The angle of the arrow as it approaches the endpoint, and the
@@ -1,4 +1,4 @@
1
- import { scaleBand, scaleDiverging, scaleDivergingLog, scaleDivergingPow, scaleDivergingSqrt, scaleDivergingSymlog, scaleLinear, scaleLog, scaleOrdinal, scalePoint, scalePow, scaleQuantile, scaleQuantize, scaleSequential, scaleSequentialLog, scaleSequentialPow, scaleSequentialQuantile, scaleSequentialSqrt, scaleSequentialSymlog, scaleSqrt, scaleSymlog, scaleThreshold, scaleTime } from 'd3-scale';
1
+ import { scaleBand, scaleDiverging, scaleDivergingLog, scaleDivergingPow, scaleDivergingSqrt, scaleDivergingSymlog, scaleLinear, scaleLog, scaleOrdinal, scalePoint, scalePow, scaleQuantile, scaleQuantize, scaleSequential, scaleSequentialLog, scaleSequentialPow, scaleSequentialQuantile, scaleSequentialSqrt, scaleSequentialSymlog, scaleSqrt, scaleSymlog, scaleThreshold, scaleTime, scaleUtc } from 'd3-scale';
2
2
  import { range as d3Range } from 'd3-array';
3
3
  import { categoricalSchemes, isCategoricalScheme, isDivergingScheme, isOrdinalScheme, isQuantitativeScheme, ordinalScheme, quantitativeScheme } from './colors.js';
4
4
  import callWithProps from './callWithProps.js';
@@ -6,11 +6,13 @@ import { interpolateLab, interpolateRound } from 'd3-interpolate';
6
6
  import { coalesce, maybeNumber } from './index.js';
7
7
  import { getLogTicks } from './getLogTicks.js';
8
8
  import { isPlainObject } from 'es-toolkit';
9
+ import { isTemporalScale } from './typeChecks.js';
9
10
  const Scales = {
10
11
  point: scalePoint,
11
12
  band: scaleBand,
12
13
  linear: scaleLinear,
13
14
  time: scaleTime,
15
+ utc: scaleUtc,
14
16
  sqrt: scaleSqrt,
15
17
  pow: scalePow,
16
18
  log: scaleLog,
@@ -55,7 +57,7 @@ export function autoScale({ name, type, domain, scaleOptions, plotOptions, plotW
55
57
  const scaleProps = {
56
58
  domain,
57
59
  range,
58
- ...((type === 'linear' || type === 'log' || type === 'time') && scaleOptions.nice
60
+ ...((type === 'linear' || type === 'log' || isTemporalScale(type)) && scaleOptions.nice
59
61
  ? {
60
62
  nice: scaleOptions.nice ? niceTickCount : true
61
63
  }
@@ -1,6 +1,6 @@
1
- import { maybeTimeInterval } from './time.js';
1
+ import { maybeTimeInterval, maybeUtcInterval } from './time.js';
2
2
  import { extent, range as rangei } from 'd3-array';
3
- export function maybeInterval(interval) {
3
+ export function maybeInterval(interval, scaleType) {
4
4
  if (interval == null)
5
5
  return;
6
6
  if (typeof interval === 'number') {
@@ -22,7 +22,7 @@ export function maybeInterval(interval) {
22
22
  };
23
23
  }
24
24
  if (typeof interval === 'string')
25
- return maybeTimeInterval(interval);
25
+ return scaleType === 'utc' ? maybeUtcInterval(interval) : maybeTimeInterval(interval);
26
26
  if (typeof interval.floor !== 'function')
27
27
  throw new Error('invalid interval; missing floor method');
28
28
  if (typeof interval.offset !== 'function')
@@ -36,7 +36,7 @@ export function autoTicks(type, ticks, interval, domain, scaleFn, count) {
36
36
  const [lo, hi] = extent(domain);
37
37
  if (lo == null || hi == null)
38
38
  return [];
39
- const I = maybeInterval(interval);
39
+ const I = maybeInterval(interval, type);
40
40
  if (!I)
41
41
  return [];
42
42
  return I.range(lo, I.offset(hi)).filter((d) => d >= lo && d <= hi);
@@ -5,33 +5,43 @@ const DATE_TIME = {
5
5
  month: 'short',
6
6
  day: 'numeric'
7
7
  };
8
- const autoFormatDateTime = (locale) => {
9
- const format = new Intl.DateTimeFormat(locale, DATE_TIME).format;
10
- return (date) => format(date).replace(', ', '\n');
8
+ const autoFormatDateTime = (locale, utc) => {
9
+ const formatter = new Intl.DateTimeFormat(locale, {
10
+ ...DATE_TIME,
11
+ ...(utc ? { timeZone: 'UTC' } : {})
12
+ });
13
+ return (date) => formatter.format(date).replace(', ', '\n');
11
14
  };
12
15
  const DAY_MONTH = {
13
16
  month: 'short',
14
17
  day: 'numeric'
15
18
  };
16
- const autoFormatDayMonth = (locale) => {
17
- const format = new Intl.DateTimeFormat(locale, DAY_MONTH).format;
18
- return (date) => format(date).replace(' ', '\n');
19
+ const autoFormatDayMonth = (locale, utc) => {
20
+ const formatter = new Intl.DateTimeFormat(locale, {
21
+ ...DAY_MONTH,
22
+ ...(utc ? { timeZone: 'UTC' } : {})
23
+ });
24
+ return (date) => formatter.format(date).replace(' ', '\n');
19
25
  };
20
26
  const MONTH_YEAR = {
21
27
  month: 'short',
22
28
  year: 'numeric'
23
29
  };
24
- const autoFormatMonthYear = (locale) => {
25
- const format = new Intl.DateTimeFormat(locale, MONTH_YEAR).format;
26
- return (date) => format(date).replace(' ', '\n');
30
+ const autoFormatMonthYear = (locale, utc) => {
31
+ const formatter = new Intl.DateTimeFormat(locale, {
32
+ ...MONTH_YEAR,
33
+ ...(utc ? { timeZone: 'UTC' } : {})
34
+ });
35
+ return (date) => formatter.format(date).replace(' ', '\n');
27
36
  };
28
37
  export default function autoTimeFormat(x, plotWidth, plotLocale) {
38
+ const utc = x.type === 'utc';
29
39
  const daysPer100Px = ((toNumber(x.domain[1]) - toNumber(x.domain[0])) / plotWidth / 864e5) * 100;
30
40
  const format = daysPer100Px < 1
31
- ? autoFormatDateTime(plotLocale)
41
+ ? autoFormatDateTime(plotLocale, utc)
32
42
  : daysPer100Px < 30
33
- ? autoFormatDayMonth(plotLocale)
34
- : autoFormatMonthYear(plotLocale);
43
+ ? autoFormatDayMonth(plotLocale, utc)
44
+ : autoFormatMonthYear(plotLocale, utc);
35
45
  return (date) => format(date).split('\n');
36
46
  }
37
47
  function toNumber(d) {
@@ -4,10 +4,10 @@ export declare const categoricalSchemes: Map<string, readonly string[]>;
4
4
  export declare function isCategoricalScheme(scheme: string): boolean;
5
5
  type SchemeGetter = (n: number) => readonly string[];
6
6
  export declare function isOrdinalScheme(scheme: ColorScheme): boolean;
7
- export declare function ordinalScheme(scheme: ColorScheme | string): SchemeGetter;
8
- export declare function ordinalRange(scheme: ColorScheme | string, length: number): readonly string[];
9
- export declare function maybeBooleanRange(domain: boolean[], scheme?: ColorScheme | string): unknown[] | undefined;
7
+ export declare function ordinalScheme(scheme: ColorScheme | (string & {})): SchemeGetter;
8
+ export declare function ordinalRange(scheme: ColorScheme | (string & {}), length: number): readonly string[];
9
+ export declare function maybeBooleanRange(domain: boolean[], scheme?: ColorScheme | (string & {})): unknown[] | undefined;
10
10
  export declare function isQuantitativeScheme(scheme: string): boolean;
11
- export declare function quantitativeScheme(scheme: ColorScheme | string): typeof interpolateBrBG | undefined;
11
+ export declare function quantitativeScheme(scheme: ColorScheme | (string & {})): typeof interpolateBrBG | undefined;
12
12
  export declare function isDivergingScheme(scheme: string): boolean;
13
13
  export {};
@@ -1,4 +1,4 @@
1
- import type { GenericMarkOptions, Mark, RawValue } from '../types/index.js';
1
+ import type { GenericMarkOptions, Mark, PlotState, RawValue } from '../types/index.js';
2
2
  /**
3
3
  * This function tracks which facets are "empty", meaning that they don't contain
4
4
  * any "facetted" data points. This can happen when fx and fy are combined and
@@ -10,3 +10,44 @@ import type { GenericMarkOptions, Mark, RawValue } from '../types/index.js';
10
10
  * @returns
11
11
  */
12
12
  export declare function getEmptyFacets(marks: Mark<GenericMarkOptions>[], fxValues: RawValue[], fyValues: RawValue[]): Map<RawValue, Map<RawValue, boolean>>;
13
+ /**
14
+ * Stable string key for a (fxValue, fyValue) pair, used as Map keys
15
+ * for the keyed tree map in Pointer/HTMLTooltip.
16
+ */
17
+ export declare function facetKey(fxValue: RawValue | boolean, fyValue: RawValue | boolean): string;
18
+ /**
19
+ * Inverts a d3 band scale: given a pixel position, returns the domain value
20
+ * whose band contains that position, or undefined if outside all bands.
21
+ *
22
+ * d3.scaleBand has no .invert(), so we iterate the domain (O(n), n = facet count,
23
+ * typically <20).
24
+ */
25
+ export declare function invertBand(scale: {
26
+ (value: string): number | undefined;
27
+ bandwidth(): number;
28
+ }, domain: readonly (string | RawValue)[], pixelPos: number): RawValue | undefined;
29
+ /**
30
+ * Walk up the DOM from `target` to find the nearest `g.facet` element.
31
+ * Returns the facet x/y indices from `data-facet-x` and `data-facet-y`
32
+ * attributes, or null if no facet element is found.
33
+ */
34
+ export declare function findFacetFromDOM(target: Element | null): {
35
+ fxIndex: number;
36
+ fyIndex: number;
37
+ } | null;
38
+ /**
39
+ * Detect which facet the mouse event is in and compute the pixel offset.
40
+ *
41
+ * Strategy: try DOM walk first (fast, reliable when the event target is inside
42
+ * a facet `<g>`). Fall back to inverting the fx/fy band scales from the mouse
43
+ * position (works in jsdom where getBoundingClientRect returns zeros).
44
+ *
45
+ * Returns { fxValue, fyValue, offsetX, offsetY } where offset is the pixel
46
+ * translation of the facet from the plot body origin.
47
+ */
48
+ export declare function detectFacet(evt: MouseEvent, plot: PlotState): {
49
+ fxValue: RawValue | boolean;
50
+ fyValue: RawValue | boolean;
51
+ offsetX: number;
52
+ offsetY: number;
53
+ };
@@ -46,3 +46,86 @@ export function getEmptyFacets(marks, fxValues, fyValues) {
46
46
  }
47
47
  return out;
48
48
  }
49
+ /**
50
+ * Stable string key for a (fxValue, fyValue) pair, used as Map keys
51
+ * for the keyed tree map in Pointer/HTMLTooltip.
52
+ */
53
+ export function facetKey(fxValue, fyValue) {
54
+ return JSON.stringify([fxValue, fyValue]);
55
+ }
56
+ /**
57
+ * Inverts a d3 band scale: given a pixel position, returns the domain value
58
+ * whose band contains that position, or undefined if outside all bands.
59
+ *
60
+ * d3.scaleBand has no .invert(), so we iterate the domain (O(n), n = facet count,
61
+ * typically <20).
62
+ */
63
+ export function invertBand(scale, domain, pixelPos) {
64
+ const bw = scale.bandwidth();
65
+ for (const value of domain) {
66
+ const start = scale(value);
67
+ if (start != null && pixelPos >= start && pixelPos < start + bw) {
68
+ return value;
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Walk up the DOM from `target` to find the nearest `g.facet` element.
75
+ * Returns the facet x/y indices from `data-facet-x` and `data-facet-y`
76
+ * attributes, or null if no facet element is found.
77
+ */
78
+ export function findFacetFromDOM(target) {
79
+ let el = target;
80
+ while (el) {
81
+ if (el.classList?.contains('facet')) {
82
+ const fxIndex = parseInt(el.dataset?.facetX ?? '0', 10);
83
+ const fyIndex = parseInt(el.dataset?.facetY ?? '0', 10);
84
+ return { fxIndex, fyIndex };
85
+ }
86
+ el = el.parentElement;
87
+ }
88
+ return null;
89
+ }
90
+ /**
91
+ * Detect which facet the mouse event is in and compute the pixel offset.
92
+ *
93
+ * Strategy: try DOM walk first (fast, reliable when the event target is inside
94
+ * a facet `<g>`). Fall back to inverting the fx/fy band scales from the mouse
95
+ * position (works in jsdom where getBoundingClientRect returns zeros).
96
+ *
97
+ * Returns { fxValue, fyValue, offsetX, offsetY } where offset is the pixel
98
+ * translation of the facet from the plot body origin.
99
+ */
100
+ export function detectFacet(evt, plot) {
101
+ const fxScale = plot.scales.fx;
102
+ const fyScale = plot.scales.fy;
103
+ const fxDomain = fxScale.domain;
104
+ const fyDomain = fyScale.domain;
105
+ const hasFx = fxDomain.length > 0;
106
+ const hasFy = fyDomain.length > 0;
107
+ // Try DOM walk
108
+ const facetInfo = findFacetFromDOM(evt.target);
109
+ if (facetInfo) {
110
+ const fxValue = hasFx ? fxDomain[facetInfo.fxIndex] : true;
111
+ const fyValue = hasFy ? fyDomain[facetInfo.fyIndex] : true;
112
+ return {
113
+ fxValue,
114
+ fyValue,
115
+ offsetX: hasFx ? (fxScale.fn(fxValue) ?? 0) : 0,
116
+ offsetY: hasFy ? (fyScale.fn(fyValue) ?? 0) : 0
117
+ };
118
+ }
119
+ // Fallback: invert mouse position against band scales
120
+ const bodyRect = plot.body.getBoundingClientRect();
121
+ const svgX = evt.clientX - bodyRect.left;
122
+ const svgY = evt.clientY - bodyRect.top;
123
+ const fxValue = hasFx ? (invertBand(fxScale.fn, fxDomain, svgX) ?? fxDomain[0]) : true;
124
+ const fyValue = hasFy ? (invertBand(fyScale.fn, fyDomain, svgY) ?? fyDomain[0]) : true;
125
+ return {
126
+ fxValue,
127
+ fyValue,
128
+ offsetX: hasFx ? (fxScale.fn(fxValue) ?? 0) : 0,
129
+ offsetY: hasFy ? (fyScale.fn(fyValue) ?? 0) : 0
130
+ };
131
+ }
@@ -16,7 +16,7 @@ export function normdev(p) {
16
16
  return -Infinity;
17
17
  if (p == 1)
18
18
  return Infinity;
19
- const a0 = 3.387132872796366608, a1 = 1.3314166789178437745e2, a2 = 1.9715909503065514427e3, a3 = 1.3731693765509461125e4, a4 = 4.5921953931549871457e4, a5 = 6.7265770927008700853e4, a6 = 3.3430575583588128105e4, a7 = 2.5090809287301226727e3, b1 = 4.2313330701600911252e1, b2 = 6.871870074920579083e2, b3 = 5.3941960214247511077e3, b4 = 2.1213794301586595867e4, b5 = 3.930789580009271061e4, b6 = 2.8729085735721942674e4, b7 = 5.226495278852854561e3, c0 = 1.42343711074968357734, c1 = 4.6303378461565452959, c2 = 5.7694972214606914055, c3 = 3.64784832476320460504, c4 = 1.27045825245236838258, c5 = 2.4178072517745061177e-1, c6 = 2.27238449892691845833e-2, c7 = 7.7454501427834140764e-4, d1 = 2.05319162663775882187, d2 = 1.6763848301838038494, d3 = 6.8976733498510000455e-1, d4 = 1.4810397642748007459e-1, d5 = 1.51986665636164571966e-2, d6 = 5.475938084995344946e-4, d7 = 1.05075007164441684324e-9, e0 = 6.6579046435011037772, e1 = 5.4637849111641143699, e2 = 1.7848265399172913358, e3 = 2.9656057182850489123e-1, e4 = 2.6532189526576123093e-2, e5 = 1.2426609473880784386e-3, e6 = 2.71155556874348757815e-5, e7 = 2.01033439929228813265e-7, f1 = 5.9983220655588793769e-1, f2 = 1.3692988092273580531e-1, f3 = 1.48753612908506148525e-2, f4 = 7.868691311456132591e-4, f5 = 1.8463183175100546818e-5, f6 = 1.4215117583164458887e-7, f7 = 2.04426310338993978564e-15;
19
+ const a0 = 3.3871328727963665, a1 = 133.14166789178438, a2 = 1971.5909503065513, a3 = 13731.69376550946, a4 = 45921.95393154987, a5 = 67265.7709270087, a6 = 33430.57558358813, a7 = 2509.0809287301227, b1 = 42.31333070160091, b2 = 687.1870074920579, b3 = 5394.196021424751, b4 = 21213.794301586597, b5 = 39307.89580009271, b6 = 28729.085735721943, b7 = 5226.495278852854, c0 = 1.4234371107496835, c1 = 4.630337846156546, c2 = 5.769497221460691, c3 = 3.6478483247632045, c4 = 1.2704582524523684, c5 = 0.2417807251774506, c6 = 0.022723844989269184, c7 = 0.0007745450142783414, d1 = 2.053191626637759, d2 = 1.6763848301838038, d3 = 0.6897673349851, d4 = 0.14810397642748008, d5 = 0.015198666563616457, d6 = 0.0005475938084995345, d7 = 1.0507500716444169e-9, e0 = 6.657904643501103, e1 = 5.463784911164114, e2 = 1.7848265399172913, e3 = 0.29656057182850487, e4 = 0.026532189526576124, e5 = 0.0012426609473880784, e6 = 0.000027115555687434876, e7 = 2.0103343992922881e-7, f1 = 0.599832206555888, f2 = 0.1369298809227358, f3 = 0.014875361290850615, f4 = 0.0007868691311456133, f5 = 0.000018463183175100548, f6 = 1.421511758316446e-7, f7 = 2.0442631033899397e-15;
20
20
  const q = p - 0.5;
21
21
  let r, z;
22
22
  // p close to 0.5
@@ -9,7 +9,7 @@ const scaled_cosine = (i) => 0.5 * (1.0 - Math.cos(i * Math.PI));
9
9
  let perlin; // will be initialized lazily by noise() or noiseSeed()
10
10
  export function noise(x, y = 0, z = 0) {
11
11
  if (perlin == null) {
12
- perlin = new Array(PERLIN_SIZE + 1);
12
+ perlin = Array.from({ length: PERLIN_SIZE + 1 });
13
13
  for (let i = 0; i < PERLIN_SIZE + 1; i++) {
14
14
  perlin[i] = Math.random();
15
15
  }
@@ -9,7 +9,7 @@ export function roundedRect(x, y, width, height, borderRadius = {
9
9
  }) {
10
10
  const maxRadius = Math.min(width, height) / 2;
11
11
  const [tl, tr, bl, br] = (typeof borderRadius === 'number'
12
- ? new Array(4).fill(borderRadius)
12
+ ? Array.from({ length: 4 }, () => borderRadius)
13
13
  : [
14
14
  borderRadius?.topLeft || 0,
15
15
  borderRadius?.topRight || 0,
@@ -1,4 +1,5 @@
1
1
  import type { ChannelAccessor, GenericMarkOptions, Mark, MarkType, PlotDefaults, PlotScaleFunction, ResolvedPlotOptions, PlotScales, PlotState, RawValue, ScaleName, ScaleOptions, ScaleType, ScaledChannelName, UsedScales } from '../types/index.js';
2
+ export declare function normalizeScaleFn(fn: any): PlotScaleFunction;
2
3
  /**
3
4
  * compute the plot scales
4
5
  */
@@ -1,13 +1,13 @@
1
1
  import { extent, ascending } from 'd3-array';
2
2
  import { isColorOrNull, isDate, isDateOrNull, isNumberOrNull, isNumberOrNullOrNaN, isStringOrNull } from './typeChecks.js';
3
3
  import { CHANNEL_SCALE, ORIGINAL_NAME_KEYS, VALID_SCALE_TYPES } from '../constants.js';
4
- import { isSymbolOrNull } from './typeChecks.js';
4
+ import { isSymbolOrNull, isTemporalScale } from './typeChecks.js';
5
5
  import { resolveProp, toChannelOption } from './resolve.js';
6
6
  import isDataRecord from './isDataRecord.js';
7
7
  import { createProjection } from './projection.js';
8
8
  import { maybeInterval } from './autoTicks.js';
9
9
  import { IS_SORTED } from '../transforms/sort.js';
10
- function normalizeScaleFn(fn) {
10
+ export function normalizeScaleFn(fn) {
11
11
  const out = fn;
12
12
  out.range ||= () => [];
13
13
  out.invert ||= (value) => value;
@@ -165,7 +165,8 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
165
165
  sortOrdinalDomain = false;
166
166
  }
167
167
  // construct domain from data values
168
- const valueArr = [...dataValues.values(), ...(scaleOptions.domain || [])].filter((d) => d != null);
168
+ // For facet scales (fx/fy), null is a valid grouping category (e.g. penguins with sex=null)
169
+ const valueArr = [...dataValues.values(), ...(scaleOptions.domain || [])].filter((d) => d != null || name === 'fx' || name === 'fy');
169
170
  const type = !scaleOptions.type || scaleOptions.type === 'auto'
170
171
  ? inferScaleType(name, valueArr, markTypes, scaleOptions)
171
172
  : scaleOptions.type;
@@ -177,7 +178,9 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
177
178
  if (isOrdinal && sortOrdinalDomain) {
178
179
  valueArr.sort(ascending);
179
180
  }
180
- const valueArray = type === 'quantile' || type === 'quantile-cont' ? allDataValues.toSorted() : valueArr;
181
+ const valueArray = type === 'quantile' || type === 'quantile-cont'
182
+ ? allDataValues.toSorted((a, b) => Number(a) - Number(b))
183
+ : valueArr;
181
184
  let domain = scaleOptions.domain
182
185
  ? isOrdinal
183
186
  ? scaleOptions.domain
@@ -228,7 +231,7 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
228
231
  skip,
229
232
  manualActiveMarks,
230
233
  uniqueScaleProps,
231
- autoTitle: type === 'time'
234
+ autoTitle: isTemporalScale(type)
232
235
  ? null
233
236
  : propNames.size === 1
234
237
  ? `${[...propNames.values()][0]}${type === 'log' ? ' (log)' : ''}`
@@ -139,7 +139,7 @@ const timeFormatIntervals = [
139
139
  // from largest to smallest, used to determine the most specific standard time
140
140
  // format for a given array of dates. This is a subset of the tick intervals
141
141
  // listed above; we only need the breakpoints where the format changes.
142
- const formatIntervals = [
142
+ const _formatIntervals = [
143
143
  utcFormatIntervals[0],
144
144
  timeFormatIntervals[0],
145
145
  utcFormatIntervals[1],
@@ -8,3 +8,4 @@ export declare function isStringOrNull(v: RawValue | null | undefined): v is str
8
8
  export declare function isSymbolOrNull(v: RawValue | null | undefined): v is ("square" | "circle" | "asterisk" | "cross" | "diamond" | "diamond2" | "hexagon" | "plus" | "square2" | "star" | "times" | "triangle" | "triangle2" | "wye") | import("d3-shape").SymbolType | null | undefined;
9
9
  export declare function isColorOrNull(v: RawValue | null | undefined): boolean;
10
10
  export declare function isOpacityOrNull(v: RawValue): boolean;
11
+ export declare function isTemporalScale(type: string): boolean;
@@ -44,3 +44,6 @@ export function isColorOrNull(v) {
44
44
  export function isOpacityOrNull(v) {
45
45
  return v == null || (typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= 1);
46
46
  }
47
+ export function isTemporalScale(type) {
48
+ return type === 'time' || type === 'utc';
49
+ }
@@ -14,7 +14,7 @@ declare function $$render<Datum extends DataRecord>(): {
14
14
  fill: ChannelAccessor<Datum>;
15
15
  fillOpacity: ConstantAccessor<number, Datum>;
16
16
  fontFamily: ConstantAccessor<import("csstype").Property.FontFamily, Datum>;
17
- fontSize: ConstantAccessor<number | "-moz-initial" | "inherit" | "initial" | "revert" | "revert-layer" | "unset" | "math" | (string & {}) | "large" | "medium" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "larger" | "smaller", Datum>;
17
+ fontSize: ConstantAccessor<import("csstype").Property.FontSize<number>, Datum>;
18
18
  fontStyle: ConstantAccessor<import("csstype").Property.FontStyle, Datum>;
19
19
  fontVariant: ConstantAccessor<import("csstype").Property.FontVariant, Datum>;
20
20
  fontWeight: ConstantAccessor<import("csstype").Property.FontWeight, Datum>;
@@ -14,7 +14,7 @@ declare function $$render<Datum extends DataRow>(): {
14
14
  fill: ChannelAccessor<Datum>;
15
15
  fillOpacity: ConstantAccessor<number, Datum>;
16
16
  fontFamily: ConstantAccessor<import("csstype").Property.FontFamily, Datum>;
17
- fontSize: ConstantAccessor<number | "-moz-initial" | "inherit" | "initial" | "revert" | "revert-layer" | "unset" | "math" | (string & {}) | "large" | "medium" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "larger" | "smaller", Datum>;
17
+ fontSize: ConstantAccessor<import("csstype").Property.FontSize<number>, Datum>;
18
18
  fontStyle: ConstantAccessor<import("csstype").Property.FontStyle, Datum>;
19
19
  fontVariant: ConstantAccessor<import("csstype").Property.FontVariant, Datum>;
20
20
  fontWeight: ConstantAccessor<import("csstype").Property.FontWeight, Datum>;
@@ -14,7 +14,7 @@ declare function $$render<Datum extends DataRow>(): {
14
14
  fill: ChannelAccessor<Datum>;
15
15
  fillOpacity: ConstantAccessor<number, Datum>;
16
16
  fontFamily: ConstantAccessor<import("csstype").Property.FontFamily, Datum>;
17
- fontSize: ConstantAccessor<number | "-moz-initial" | "inherit" | "initial" | "revert" | "revert-layer" | "unset" | "math" | (string & {}) | "large" | "medium" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "larger" | "smaller", Datum>;
17
+ fontSize: ConstantAccessor<import("csstype").Property.FontSize<number>, Datum>;
18
18
  fontStyle: ConstantAccessor<import("csstype").Property.FontStyle, Datum>;
19
19
  fontVariant: ConstantAccessor<import("csstype").Property.FontVariant, Datum>;
20
20
  fontWeight: ConstantAccessor<import("csstype").Property.FontWeight, Datum>;
@@ -13,7 +13,7 @@ declare function $$render<Datum = DataRecord | GeoJSON.GeoJsonObject>(): {
13
13
  fill: ChannelAccessor<Datum>;
14
14
  fillOpacity: ConstantAccessor<number, Datum>;
15
15
  fontFamily: ConstantAccessor<import("csstype").Property.FontFamily, Datum>;
16
- fontSize: ConstantAccessor<number | "-moz-initial" | "inherit" | "initial" | "revert" | "revert-layer" | "unset" | "math" | (string & {}) | "large" | "medium" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "larger" | "smaller", Datum>;
16
+ fontSize: ConstantAccessor<import("csstype").Property.FontSize<number>, Datum>;
17
17
  fontStyle: ConstantAccessor<import("csstype").Property.FontStyle, Datum>;
18
18
  fontVariant: ConstantAccessor<import("csstype").Property.FontVariant, Datum>;
19
19
  fontWeight: ConstantAccessor<import("csstype").Property.FontWeight, Datum>;
@@ -18,6 +18,7 @@
18
18
  import type * as CSS from 'csstype';
19
19
  import autoTimeFormat from '../helpers/autoTimeFormat.js';
20
20
  import { autoTicks } from '../helpers/autoTicks.js';
21
+ import { isTemporalScale } from '../helpers/typeChecks.js';
21
22
  import { resolveScaledStyles } from '../helpers/resolve.js';
22
23
  import { getPlotDefaults } from '../hooks/plotDefaults.js';
23
24
  import { extent } from 'd3-array';
@@ -161,10 +162,14 @@
161
162
  ? tickFmt
162
163
  : plot.scales.x.type === 'band' || plot.scales.x.type === 'point'
163
164
  ? (d: RawValue) => String(d)
164
- : plot.scales.x.type === 'time'
165
- ? // time scale
165
+ : isTemporalScale(plot.scales.x.type)
166
+ ? // time/utc scale
166
167
  typeof tickFmt === 'object'
167
- ? (d: Date) => Intl.DateTimeFormat(plot.options.locale, tickFmt).format(d)
168
+ ? (d: Date) =>
169
+ Intl.DateTimeFormat(plot.options.locale, {
170
+ ...tickFmt,
171
+ ...(plot.scales.x.type === 'utc' ? { timeZone: 'UTC' } : {})
172
+ }).format(d)
168
173
  : autoTimeFormat(plot.scales.x, plot.plotWidth, plot.options.locale)
169
174
  : // numeric scale
170
175
  typeof tickFmt === 'object'
@@ -13,7 +13,7 @@ declare function $$render<Datum extends RawValue>(): {
13
13
  fill: import("../types/channel.js").ChannelAccessor<Datum>;
14
14
  fillOpacity: ConstantAccessor<number, Datum>;
15
15
  fontFamily: ConstantAccessor<CSS.Property.FontFamily, Datum>;
16
- fontSize: ConstantAccessor<number | "-moz-initial" | "inherit" | "initial" | "revert" | "revert-layer" | "unset" | "math" | (string & {}) | "large" | "medium" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "larger" | "smaller", Datum>;
16
+ fontSize: ConstantAccessor<CSS.Property.FontSize<number>, Datum>;
17
17
  fontStyle: ConstantAccessor<CSS.Property.FontStyle, Datum>;
18
18
  fontVariant: ConstantAccessor<CSS.Property.FontVariant, Datum>;
19
19
  fontWeight: ConstantAccessor<CSS.Property.FontWeight, Datum>;
@@ -17,6 +17,7 @@
17
17
  } from '../types/index.js';
18
18
  import autoTimeFormat from '../helpers/autoTimeFormat.js';
19
19
  import { autoTicks } from '../helpers/autoTicks.js';
20
+ import { isTemporalScale } from '../helpers/typeChecks.js';
20
21
  import { resolveScaledStyles } from '../helpers/resolve.js';
21
22
  import { getPlotDefaults } from '../hooks/plotDefaults.js';
22
23
  import { extent } from 'd3-array';
@@ -158,10 +159,14 @@
158
159
  ? tickFmt
159
160
  : plot.scales.y.type === 'band' || plot.scales.y.type === 'point'
160
161
  ? (d: RawValue) => String(d)
161
- : plot.scales.y.type === 'time'
162
- ? // time scale
162
+ : isTemporalScale(plot.scales.y.type)
163
+ ? // time/utc scale
163
164
  typeof tickFmt === 'object'
164
- ? (d: Date) => Intl.DateTimeFormat(plot.options.locale, tickFmt).format(d)
165
+ ? (d: Date) =>
166
+ Intl.DateTimeFormat(plot.options.locale, {
167
+ ...tickFmt,
168
+ ...(plot.scales.y.type === 'utc' ? { timeZone: 'UTC' } : {})
169
+ }).format(d)
165
170
  : autoTimeFormat(plot.scales.y, plot.plotWidth, plot.options.locale)
166
171
  : // numeric scale
167
172
  typeof tickFmt === 'object'
@@ -12,7 +12,7 @@ declare function $$render<Datum extends RawValue>(): {
12
12
  fill: import("../types/channel.js").ChannelAccessor<Datum>;
13
13
  fillOpacity: ConstantAccessor<number, Datum>;
14
14
  fontFamily: ConstantAccessor<import("csstype").Property.FontFamily, Datum>;
15
- fontSize: ConstantAccessor<number | "-moz-initial" | "inherit" | "initial" | "revert" | "revert-layer" | "unset" | "math" | (string & {}) | "large" | "medium" | "small" | "x-large" | "x-small" | "xx-large" | "xx-small" | "xxx-large" | "larger" | "smaller", Datum>;
15
+ fontSize: ConstantAccessor<import("csstype").Property.FontSize<number>, Datum>;
16
16
  fontStyle: ConstantAccessor<import("csstype").Property.FontStyle, Datum>;
17
17
  fontVariant: ConstantAccessor<import("csstype").Property.FontVariant, Datum>;
18
18
  fontWeight: ConstantAccessor<import("csstype").Property.FontWeight, Datum>;