@pixerate/editor 0.46.0 → 0.46.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.
@@ -310,7 +310,7 @@ interface NiceScale {
310
310
  * inside them.
311
311
  */
312
312
  declare function niceScale(min: number, max: number, count?: number, fixedMin?: number | null, fixedMax?: number | null): NiceScale;
313
- /** Decimals needed to tell ticks `step` apart (at most 6). */
313
+ /** Decimals needed to write ticks `step` apart exactly: 1 → 0, 2.5 → 1, 0.25 → 2 (at most 6). */
314
314
  declare function stepDecimals(step: number): number;
315
315
  /**
316
316
  * Writes a number for a chart label. Locale-independent (so labels and tests are the same
@@ -332,11 +332,24 @@ interface ChartLayoutOptions {
332
332
  measureText?: ChartTextMeasure;
333
333
  /** Shown when there is nothing to draw (default "No data"). */
334
334
  emptyMessage?: string;
335
+ /**
336
+ * Fixes the value axis to this range (over `yAxis.min` / `yAxis.max`). Canvases pass
337
+ * `ChartPointDrag.valueDomain` during a drag so the axis does not rescale under the pointer.
338
+ */
339
+ valueDomain?: {
340
+ min: number;
341
+ max: number;
342
+ } | null;
335
343
  }
336
344
  /** A DOM-free text width estimate (average glyph ≈ 0.56 em, bold ≈ 0.6 em). */
337
345
  declare const estimateChartTextWidth: ChartTextMeasure;
338
346
  /** Whether two selections point at the same thing. */
339
347
  declare function sameChartSelection(a: ChartSelection | null | undefined, b: ChartSelection | null | undefined): boolean;
348
+ /**
349
+ * Black or white text for a label drawn on `fill` (#rgb, #rrggbb or rgb()), by perceived
350
+ * luminance (0.299 R + 0.587 G + 0.114 B). Unparseable colours (CSS variables) get white.
351
+ */
352
+ declare function contrastingTextColor(fill: string): string;
340
353
  /**
341
354
  * Lays a chart out as drawing primitives (rects, paths, circles, lines and text) in a
342
355
  * `width × height` box. Pure and DOM-free: the React and Svelte canvases render the marks
@@ -444,6 +457,7 @@ declare class ChartController {
444
457
  private issues;
445
458
  private gestureKey;
446
459
  private gestureCount;
460
+ private gestureStart;
447
461
  private revision;
448
462
  private applying;
449
463
  private readonly onChange?;
@@ -606,6 +620,14 @@ declare function canDragChartPoint(controller: ChartController, scene: ChartScen
606
620
  kind: 'point';
607
621
  }>;
608
622
  interface ChartPointDrag {
623
+ /**
624
+ * The value axis range when the drag started. Pass it to `layoutChart` (`valueDomain`)
625
+ * until `end()` so the axis does not rescale under the pointer.
626
+ */
627
+ readonly valueDomain: {
628
+ min: number;
629
+ max: number;
630
+ };
609
631
  /** Moves the value with the pointer (scene coordinates). */
610
632
  move(point: {
611
633
  x: number;
@@ -643,4 +665,4 @@ declare function handleChartKey(controller: ChartController, scene: ChartScene |
643
665
  /** A short description of the selection for an inspector heading or a screen reader. */
644
666
  declare function describeChartSelection(controller: ChartController, selection?: ChartSelection | null): string;
645
667
 
646
- export { CHART_CURVES, CHART_LEGEND_POSITIONS, CHART_NUMBER_STYLES, CHART_ORIENTATIONS, CHART_SERIES_TYPES, CHART_STACKINGS, CHART_TYPES, CHART_TYPE_LABELS, CSS_VARIABLE_CHART_THEME, type ChartActionResult, type ChartAxis, type ChartCircleMark, type ChartCommandOptions, ChartController, type ChartControllerOptions, type ChartCurve, type ChartEditOptions, type ChartIssue, type ChartKeyInput, type ChartLayoutOptions, type ChartLegend, type ChartLegendPosition, type ChartLineMark, type ChartMark, type ChartMarkRole, type ChartNumberFormat, type ChartOrientation, type ChartPathMark, type ChartPointDrag, type ChartRect, type ChartRectMark, type ChartScene, type ChartSelection, type ChartSeries, type ChartSeriesInput, type ChartSeriesPatch, type ChartSeriesType, type ChartSpec, type ChartSpecPatch, type ChartStacking, type ChartState, type ChartSvgOptions, type ChartTableData, type ChartTableOptions, type ChartTextMark, type ChartTextMeasure, type ChartTheme, type ChartType, type ChartValue, type ChartValueScale, DEFAULT_CHART_PALETTE, DEFAULT_CHART_THEME, type NiceScale, canDragChartPoint, chartClientToScene, chartColorInputValue, chartDataFromTable, chartDataToTable, chartSceneToSvg, chartSeriesColor, chartSeriesType, chartSliceColor, chartTextBaseline, chartTextTransform, chartToSvg, chartValueAtPoint, cloneChartSeries, cloneChartSpec, createChartCommands, createChartController, createChartSpec, defaultChartAxis, describeChartSelection, estimateChartTextWidth, formatChartNumber, handleChartKey, isCartesianChart, layoutChart, linearMap, niceScale, niceStep, normalizeChartAxis, normalizeChartSpec, normalizeChartValues, parseChartValue, sameChartSelection, snapChartValue, startChartPointDrag, stepDecimals, uniqueChartSeriesId };
668
+ export { CHART_CURVES, CHART_LEGEND_POSITIONS, CHART_NUMBER_STYLES, CHART_ORIENTATIONS, CHART_SERIES_TYPES, CHART_STACKINGS, CHART_TYPES, CHART_TYPE_LABELS, CSS_VARIABLE_CHART_THEME, type ChartActionResult, type ChartAxis, type ChartCircleMark, type ChartCommandOptions, ChartController, type ChartControllerOptions, type ChartCurve, type ChartEditOptions, type ChartIssue, type ChartKeyInput, type ChartLayoutOptions, type ChartLegend, type ChartLegendPosition, type ChartLineMark, type ChartMark, type ChartMarkRole, type ChartNumberFormat, type ChartOrientation, type ChartPathMark, type ChartPointDrag, type ChartRect, type ChartRectMark, type ChartScene, type ChartSelection, type ChartSeries, type ChartSeriesInput, type ChartSeriesPatch, type ChartSeriesType, type ChartSpec, type ChartSpecPatch, type ChartStacking, type ChartState, type ChartSvgOptions, type ChartTableData, type ChartTableOptions, type ChartTextMark, type ChartTextMeasure, type ChartTheme, type ChartType, type ChartValue, type ChartValueScale, DEFAULT_CHART_PALETTE, DEFAULT_CHART_THEME, type NiceScale, canDragChartPoint, chartClientToScene, chartColorInputValue, chartDataFromTable, chartDataToTable, chartSceneToSvg, chartSeriesColor, chartSeriesType, chartSliceColor, chartTextBaseline, chartTextTransform, chartToSvg, chartValueAtPoint, cloneChartSeries, cloneChartSpec, contrastingTextColor, createChartCommands, createChartController, createChartSpec, defaultChartAxis, describeChartSelection, estimateChartTextWidth, formatChartNumber, handleChartKey, isCartesianChart, layoutChart, linearMap, niceScale, niceStep, normalizeChartAxis, normalizeChartSpec, normalizeChartValues, parseChartValue, sameChartSelection, snapChartValue, startChartPointDrag, stepDecimals, uniqueChartSeriesId };
@@ -310,7 +310,7 @@ interface NiceScale {
310
310
  * inside them.
311
311
  */
312
312
  declare function niceScale(min: number, max: number, count?: number, fixedMin?: number | null, fixedMax?: number | null): NiceScale;
313
- /** Decimals needed to tell ticks `step` apart (at most 6). */
313
+ /** Decimals needed to write ticks `step` apart exactly: 1 → 0, 2.5 → 1, 0.25 → 2 (at most 6). */
314
314
  declare function stepDecimals(step: number): number;
315
315
  /**
316
316
  * Writes a number for a chart label. Locale-independent (so labels and tests are the same
@@ -332,11 +332,24 @@ interface ChartLayoutOptions {
332
332
  measureText?: ChartTextMeasure;
333
333
  /** Shown when there is nothing to draw (default "No data"). */
334
334
  emptyMessage?: string;
335
+ /**
336
+ * Fixes the value axis to this range (over `yAxis.min` / `yAxis.max`). Canvases pass
337
+ * `ChartPointDrag.valueDomain` during a drag so the axis does not rescale under the pointer.
338
+ */
339
+ valueDomain?: {
340
+ min: number;
341
+ max: number;
342
+ } | null;
335
343
  }
336
344
  /** A DOM-free text width estimate (average glyph ≈ 0.56 em, bold ≈ 0.6 em). */
337
345
  declare const estimateChartTextWidth: ChartTextMeasure;
338
346
  /** Whether two selections point at the same thing. */
339
347
  declare function sameChartSelection(a: ChartSelection | null | undefined, b: ChartSelection | null | undefined): boolean;
348
+ /**
349
+ * Black or white text for a label drawn on `fill` (#rgb, #rrggbb or rgb()), by perceived
350
+ * luminance (0.299 R + 0.587 G + 0.114 B). Unparseable colours (CSS variables) get white.
351
+ */
352
+ declare function contrastingTextColor(fill: string): string;
340
353
  /**
341
354
  * Lays a chart out as drawing primitives (rects, paths, circles, lines and text) in a
342
355
  * `width × height` box. Pure and DOM-free: the React and Svelte canvases render the marks
@@ -444,6 +457,7 @@ declare class ChartController {
444
457
  private issues;
445
458
  private gestureKey;
446
459
  private gestureCount;
460
+ private gestureStart;
447
461
  private revision;
448
462
  private applying;
449
463
  private readonly onChange?;
@@ -606,6 +620,14 @@ declare function canDragChartPoint(controller: ChartController, scene: ChartScen
606
620
  kind: 'point';
607
621
  }>;
608
622
  interface ChartPointDrag {
623
+ /**
624
+ * The value axis range when the drag started. Pass it to `layoutChart` (`valueDomain`)
625
+ * until `end()` so the axis does not rescale under the pointer.
626
+ */
627
+ readonly valueDomain: {
628
+ min: number;
629
+ max: number;
630
+ };
609
631
  /** Moves the value with the pointer (scene coordinates). */
610
632
  move(point: {
611
633
  x: number;
@@ -643,4 +665,4 @@ declare function handleChartKey(controller: ChartController, scene: ChartScene |
643
665
  /** A short description of the selection for an inspector heading or a screen reader. */
644
666
  declare function describeChartSelection(controller: ChartController, selection?: ChartSelection | null): string;
645
667
 
646
- export { CHART_CURVES, CHART_LEGEND_POSITIONS, CHART_NUMBER_STYLES, CHART_ORIENTATIONS, CHART_SERIES_TYPES, CHART_STACKINGS, CHART_TYPES, CHART_TYPE_LABELS, CSS_VARIABLE_CHART_THEME, type ChartActionResult, type ChartAxis, type ChartCircleMark, type ChartCommandOptions, ChartController, type ChartControllerOptions, type ChartCurve, type ChartEditOptions, type ChartIssue, type ChartKeyInput, type ChartLayoutOptions, type ChartLegend, type ChartLegendPosition, type ChartLineMark, type ChartMark, type ChartMarkRole, type ChartNumberFormat, type ChartOrientation, type ChartPathMark, type ChartPointDrag, type ChartRect, type ChartRectMark, type ChartScene, type ChartSelection, type ChartSeries, type ChartSeriesInput, type ChartSeriesPatch, type ChartSeriesType, type ChartSpec, type ChartSpecPatch, type ChartStacking, type ChartState, type ChartSvgOptions, type ChartTableData, type ChartTableOptions, type ChartTextMark, type ChartTextMeasure, type ChartTheme, type ChartType, type ChartValue, type ChartValueScale, DEFAULT_CHART_PALETTE, DEFAULT_CHART_THEME, type NiceScale, canDragChartPoint, chartClientToScene, chartColorInputValue, chartDataFromTable, chartDataToTable, chartSceneToSvg, chartSeriesColor, chartSeriesType, chartSliceColor, chartTextBaseline, chartTextTransform, chartToSvg, chartValueAtPoint, cloneChartSeries, cloneChartSpec, createChartCommands, createChartController, createChartSpec, defaultChartAxis, describeChartSelection, estimateChartTextWidth, formatChartNumber, handleChartKey, isCartesianChart, layoutChart, linearMap, niceScale, niceStep, normalizeChartAxis, normalizeChartSpec, normalizeChartValues, parseChartValue, sameChartSelection, snapChartValue, startChartPointDrag, stepDecimals, uniqueChartSeriesId };
668
+ export { CHART_CURVES, CHART_LEGEND_POSITIONS, CHART_NUMBER_STYLES, CHART_ORIENTATIONS, CHART_SERIES_TYPES, CHART_STACKINGS, CHART_TYPES, CHART_TYPE_LABELS, CSS_VARIABLE_CHART_THEME, type ChartActionResult, type ChartAxis, type ChartCircleMark, type ChartCommandOptions, ChartController, type ChartControllerOptions, type ChartCurve, type ChartEditOptions, type ChartIssue, type ChartKeyInput, type ChartLayoutOptions, type ChartLegend, type ChartLegendPosition, type ChartLineMark, type ChartMark, type ChartMarkRole, type ChartNumberFormat, type ChartOrientation, type ChartPathMark, type ChartPointDrag, type ChartRect, type ChartRectMark, type ChartScene, type ChartSelection, type ChartSeries, type ChartSeriesInput, type ChartSeriesPatch, type ChartSeriesType, type ChartSpec, type ChartSpecPatch, type ChartStacking, type ChartState, type ChartSvgOptions, type ChartTableData, type ChartTableOptions, type ChartTextMark, type ChartTextMeasure, type ChartTheme, type ChartType, type ChartValue, type ChartValueScale, DEFAULT_CHART_PALETTE, DEFAULT_CHART_THEME, type NiceScale, canDragChartPoint, chartClientToScene, chartColorInputValue, chartDataFromTable, chartDataToTable, chartSceneToSvg, chartSeriesColor, chartSeriesType, chartSliceColor, chartTextBaseline, chartTextTransform, chartToSvg, chartValueAtPoint, cloneChartSeries, cloneChartSpec, contrastingTextColor, createChartCommands, createChartController, createChartSpec, defaultChartAxis, describeChartSelection, estimateChartTextWidth, formatChartNumber, handleChartKey, isCartesianChart, layoutChart, linearMap, niceScale, niceStep, normalizeChartAxis, normalizeChartSpec, normalizeChartValues, parseChartValue, sameChartSelection, snapChartValue, startChartPointDrag, stepDecimals, uniqueChartSeriesId };
@@ -335,9 +335,12 @@ function niceStep(span, count) {
335
335
  }
336
336
  var clean = (n) => Math.abs(n) < 1e-12 ? 0 : Number(n.toPrecision(12));
337
337
  function niceScale(min, max, count = 5, fixedMin = null, fixedMax = null) {
338
+ if (fixedMin !== null && !Number.isFinite(fixedMin)) fixedMin = null;
339
+ if (fixedMax !== null && !Number.isFinite(fixedMax)) fixedMax = null;
338
340
  let lo = fixedMin ?? min;
339
341
  let hi = fixedMax ?? max;
340
342
  if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
343
+ fixedMin = fixedMax = null;
341
344
  lo = 0;
342
345
  hi = 1;
343
346
  }
@@ -347,17 +350,36 @@ function niceScale(min, max, count = 5, fixedMin = null, fixedMax = null) {
347
350
  if (fixedMin === null && lo !== 0) lo -= pad;
348
351
  if (fixedMax === null || lo === hi) hi += pad;
349
352
  }
350
- const step = niceStep(hi - lo, count);
351
- const niceMin = fixedMin ?? clean(Math.floor(lo / step + 1e-9) * step);
352
- const niceMax = fixedMax ?? clean(Math.ceil(hi / step - 1e-9) * step);
353
+ const span = hi - lo;
354
+ const step = Number.isFinite(span) ? niceStep(span, count) : niceStep(hi / 2 - lo / 2, count) * 2;
355
+ const finiteOr = (n2, fallback) => Number.isFinite(n2) ? n2 : fallback;
356
+ const niceMin = fixedMin ?? finiteOr(snap(Math.floor(lo / step + 1e-9) * step, step), lo);
357
+ const niceMax = fixedMax ?? finiteOr(snap(Math.ceil(hi / step - 1e-9) * step, step), hi);
358
+ const firstIndex = Math.ceil(niceMin / step - 1e-9);
359
+ const lastIndex = Math.floor(niceMax / step + 1e-9);
360
+ const n = Math.min(999, Math.max(-1, lastIndex - firstIndex));
353
361
  const ticks = [];
354
- const first = Math.ceil(niceMin / step - 1e-9) * step;
355
- for (let t = first, i = 0; t <= niceMax + step * 1e-9 && i < 1e3; t += step, i++) ticks.push(clean(t));
362
+ for (let i = 0; i <= n; i++) {
363
+ const t = snap((firstIndex + i) * step, step);
364
+ if (Number.isFinite(t) && t !== ticks[ticks.length - 1]) ticks.push(t);
365
+ }
356
366
  return { min: niceMin, max: niceMax, step, ticks };
357
367
  }
368
+ function snap(n, step) {
369
+ const c = clean(n);
370
+ return Math.abs(c - n) <= step * 1e-6 ? c : n;
371
+ }
358
372
  function stepDecimals(step) {
359
- if (!(step > 0) || step >= 1) return 0;
360
- return Math.min(6, Math.max(0, Math.ceil(-Math.log10(step) - 1e-9)));
373
+ if (!(step > 0) || !Number.isFinite(step)) return 0;
374
+ for (let d = 0; d < 6; d++) {
375
+ const scaled = step * Math.pow(10, d);
376
+ if (Math.abs(scaled - Math.round(scaled)) < 1e-9) return d;
377
+ }
378
+ return 6;
379
+ }
380
+ function magnitudeDecimals(value) {
381
+ if (!(value > 0) || value >= 1) return 0;
382
+ return Math.min(6, Math.max(0, Math.ceil(-Math.log10(value) - 1e-9)));
361
383
  }
362
384
  var COMPACT = [
363
385
  { value: 1e12, suffix: "T" },
@@ -379,7 +401,7 @@ function formatChartNumber(value, format = {}, fallbackDecimals) {
379
401
  const decimalsFor = (v) => {
380
402
  if (format.decimals !== void 0) return format.decimals;
381
403
  if (fallbackDecimals !== void 0) return fallbackDecimals;
382
- return Number.isInteger(v) ? 0 : Math.abs(v) < 1 ? Math.min(4, Math.max(2, stepDecimals(Math.abs(v)) + 1)) : 2;
404
+ return Number.isInteger(v) ? 0 : Math.abs(v) < 1 ? Math.min(4, Math.max(2, magnitudeDecimals(Math.abs(v)) + 1)) : 2;
383
405
  };
384
406
  const trim = (s) => format.decimals === void 0 && fallbackDecimals === void 0 && s.includes(".") ? s.replace(/\.?0+$/, "") : s;
385
407
  if (style === "percent") {
@@ -426,6 +448,20 @@ function sameChartSelection(a, b) {
426
448
  return true;
427
449
  }
428
450
  }
451
+ function contrastingTextColor(fill) {
452
+ let rgb = null;
453
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(fill.trim());
454
+ if (hex) {
455
+ const h = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join("") : hex[1];
456
+ rgb = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
457
+ } else {
458
+ const fn = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(fill.trim());
459
+ if (fn) rgb = fn.slice(1, 4).map(Number);
460
+ }
461
+ if (!rgb) return "#ffffff";
462
+ const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
463
+ return luminance > 0.5 ? "#0f172a" : "#ffffff";
464
+ }
429
465
  var r2 = (n) => Math.round(n * 100) / 100;
430
466
  function curvePath(points, curve, map, moveFirst = true) {
431
467
  if (!points.length) return "";
@@ -583,8 +619,11 @@ function layoutChart(spec, options) {
583
619
  const xAxis = spec.xAxis;
584
620
  const yAxis = spec.yAxis;
585
621
  const titleBand = fs * 1.3 + 6;
622
+ const frozen = options.valueDomain;
623
+ const fixedMin = frozen && Number.isFinite(frozen.min) ? frozen.min : yAxis.min;
624
+ const fixedMax = frozen && Number.isFinite(frozen.max) ? frozen.max : yAxis.max;
586
625
  const valueLen0 = horizontal ? area.width : area.height;
587
- let scale = niceScale(lo, hi, Math.max(2, Math.min(10, Math.round(valueLen0 / 56))), yAxis.min, yAxis.max);
626
+ let scale = niceScale(lo, hi, Math.max(2, Math.min(10, Math.round(valueLen0 / 56))), fixedMin, fixedMax);
588
627
  const tickText = (t) => formatChartNumber(t, valueFormat, valueFormat.decimals ?? stepDecimals(scale.step));
589
628
  const valueLabelWidth = Math.max(0, ...scale.ticks.map((t) => measure(tickText(t), fs)));
590
629
  const catLabelWidth = Math.max(0, ...spec.categories.map((c) => measure(c, fs)));
@@ -608,7 +647,7 @@ function layoutChart(spec, options) {
608
647
  width: Math.max(1, area.width - left - rightPad),
609
648
  height: Math.max(1, area.height - bottom - (horizontal ? 4 : fs / 2 + 2))
610
649
  };
611
- scale = niceScale(lo, hi, Math.max(2, Math.min(10, Math.round((horizontal ? plot.width : plot.height) / 56))), yAxis.min, yAxis.max);
650
+ scale = niceScale(lo, hi, Math.max(2, Math.min(10, Math.round((horizontal ? plot.width : plot.height) / 56))), fixedMin, fixedMax);
612
651
  const domain = [scale.min, scale.max];
613
652
  const valueRange = horizontal ? [plot.x, plot.x + plot.width] : [plot.y + plot.height, plot.y];
614
653
  const valuePx = (v) => linearMap(Math.min(scale.max, Math.max(scale.min, v)), domain, valueRange);
@@ -718,6 +757,8 @@ function layoutChart(spec, options) {
718
757
  if (selected) outline(`bar-${s.id}-${i}-selection`, { x: rect.x - 2, y: rect.y - 2, width: rect.width + 4, height: rect.height + 4 });
719
758
  if (spec.dataLabels) {
720
759
  const v = percent ? seg[1] - seg[0] : raw;
760
+ const text2 = dataLabel(v);
761
+ if (stacked && Math.abs(b - a) < (horizontal ? measure(text2, fs - 1) + 4 : fs + 1)) return;
721
762
  const end = valuePx(seg[1]);
722
763
  const outward = (seg[1] >= seg[0] ? 1 : -1) * (horizontal ? 1 : -1);
723
764
  const [lx, ly] = map({ c: c0 + cw / 2, v: stacked ? (a + b) / 2 : end + outward * 4 });
@@ -727,11 +768,11 @@ function layoutChart(spec, options) {
727
768
  role: "data-label",
728
769
  x: lx,
729
770
  y: ly,
730
- text: dataLabel(v),
771
+ text: text2,
731
772
  anchor: horizontal && !stacked ? outward > 0 ? "start" : "end" : "middle",
732
773
  baseline: horizontal || stacked ? "middle" : outward < 0 ? "bottom" : "top",
733
774
  size: fs - 1,
734
- fill: stacked ? "#ffffff" : theme.text,
775
+ fill: stacked ? contrastingTextColor(color) : theme.text,
735
776
  target
736
777
  });
737
778
  }
@@ -1015,6 +1056,20 @@ function chartToSvg(spec, options) {
1015
1056
  }
1016
1057
 
1017
1058
  // src/chart/controller.ts
1059
+ function sameChartJson(a, b) {
1060
+ if (a === b) return true;
1061
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);
1062
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
1063
+ if (Array.isArray(a)) {
1064
+ const bb = b;
1065
+ return a.length === bb.length && a.every((v, i) => sameChartJson(v, bb[i]));
1066
+ }
1067
+ const ao = a;
1068
+ const bo = b;
1069
+ const keys = Object.keys(ao).filter((k) => ao[k] !== void 0);
1070
+ if (keys.length !== Object.keys(bo).filter((k) => bo[k] !== void 0).length) return false;
1071
+ return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameChartJson(ao[k], bo[k]));
1072
+ }
1018
1073
  var fail = (message, issues) => ({ ok: false, message, ...issues ? { issues } : {} });
1019
1074
  var ChartController = class {
1020
1075
  history;
@@ -1023,6 +1078,7 @@ var ChartController = class {
1023
1078
  issues = [];
1024
1079
  gestureKey = null;
1025
1080
  gestureCount = 0;
1081
+ gestureStart = null;
1026
1082
  revision = 0;
1027
1083
  applying = false;
1028
1084
  onChange;
@@ -1130,7 +1186,14 @@ var ChartController = class {
1130
1186
  }
1131
1187
  commit(label, next, select) {
1132
1188
  const before = this.spec;
1189
+ const hadIssues = this.issues.length > 0;
1133
1190
  this.issues = [];
1191
+ if (sameChartJson(before, next)) {
1192
+ const reselect = select !== void 0 && !sameChartSelection(this.selection, select) && !(select === null && this.selection === null);
1193
+ if (reselect) this.selection = select;
1194
+ if (reselect || hadIssues) this.notify(false);
1195
+ return;
1196
+ }
1134
1197
  const command = {
1135
1198
  name: label,
1136
1199
  tag: "chart",
@@ -1486,12 +1549,16 @@ var ChartController = class {
1486
1549
  this.history.endGesture();
1487
1550
  this.gestureKey = key;
1488
1551
  this.gestureCount++;
1552
+ this.gestureStart = { spec: this.spec, mark: this.history.mark() };
1489
1553
  this.notify(false);
1490
1554
  }
1491
1555
  endGesture() {
1492
1556
  if (this.gestureKey === null) return;
1493
1557
  this.gestureKey = null;
1494
1558
  this.history.endGesture();
1559
+ const start = this.gestureStart;
1560
+ this.gestureStart = null;
1561
+ if (start && sameChartJson(start.spec, this.spec)) this.history.revertTo(start.mark);
1495
1562
  this.notify(false);
1496
1563
  }
1497
1564
  // -------------------------------------------------------------------------
@@ -1857,7 +1924,9 @@ function startChartPointDrag(controller, scene, target, start, options = {}) {
1857
1924
  if (startAt === void 0) return null;
1858
1925
  controller.beginGesture(`drag:${seriesId}:${index}`);
1859
1926
  let ended = false;
1927
+ const [min, max] = scene.valueScale.domain;
1860
1928
  return {
1929
+ valueDomain: { min, max },
1861
1930
  move(point) {
1862
1931
  if (ended) return;
1863
1932
  const at = chartValueAtPoint(scene, point);
@@ -1958,6 +2027,7 @@ export {
1958
2027
  chartValueAtPoint,
1959
2028
  cloneChartSeries,
1960
2029
  cloneChartSpec,
2030
+ contrastingTextColor,
1961
2031
  createChartCommands,
1962
2032
  createChartController,
1963
2033
  createChartSpec,