chartist 2.0.0-rc.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -797,6 +797,160 @@ const easings = {
797
797
  1.55
798
798
  ]
799
799
  };
800
+ /** The CSS easing keywords as cubic Bézier curves. */
801
+ const CSS_EASINGS = {
802
+ linear: [
803
+ 0,
804
+ 0,
805
+ 1,
806
+ 1
807
+ ],
808
+ ease: [
809
+ .25,
810
+ .1,
811
+ .25,
812
+ 1
813
+ ],
814
+ "ease-in": [
815
+ .42,
816
+ 0,
817
+ 1,
818
+ 1
819
+ ],
820
+ "ease-out": [
821
+ 0,
822
+ 0,
823
+ .58,
824
+ 1
825
+ ],
826
+ "ease-in-out": [
827
+ .42,
828
+ 0,
829
+ .58,
830
+ 1
831
+ ]
832
+ };
833
+ const NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
834
+ /** The four numbers of a cubic Bézier easing, or nothing for other easings. */
835
+ function curveOf(easing) {
836
+ const css = easing.trim();
837
+ if (CSS_EASINGS[css]) return CSS_EASINGS[css];
838
+ if (easings[css]) return easings[css];
839
+ const numbers = css.startsWith("cubic-bezier(") ? css.match(NUMBER) : null;
840
+ return numbers?.length === 4 ? numbers.map(Number) : void 0;
841
+ }
842
+ /** A cubic Bézier easing as a function of time, solved for x by bisection. */
843
+ function bezier([x1, y1, x2, y2]) {
844
+ const at = (a, b, t) => 3 * a * t * (1 - t) ** 2 + 3 * b * t ** 2 * (1 - t) + t ** 3;
845
+ return (time) => {
846
+ let low = 0;
847
+ let high = 1;
848
+ let t = time;
849
+ for (let i = 0; i < 30; i++) {
850
+ const x = at(x1, x2, t);
851
+ if (Math.abs(x - time) < 1e-6) break;
852
+ if (x < time) low = t;
853
+ else high = t;
854
+ t = (low + high) / 2;
855
+ }
856
+ return at(y1, y2, t);
857
+ };
858
+ }
859
+ /** `steps(n, position)` as a function of time. */
860
+ function steps(css) {
861
+ const count = Math.max(1, Math.round(Number(css.match(NUMBER)?.[0] ?? 1)));
862
+ const position = css.match(/(jump-)?(start|end|none|both)\s*\)/)?.[2] ?? "end";
863
+ const jumps = position === "none" ? count - 1 : position === "both" ? count + 1 : count;
864
+ const offset = position === "start" || position === "both" ? 1 : 0;
865
+ return (time) => Math.min(1, (Math.min(Math.floor(time * count), count - 1) + offset) / Math.max(1, jumps));
866
+ }
867
+ /** `linear(…)` as a function of time: its stops, evenly spread where not given. */
868
+ function linearStops(css) {
869
+ const stops = css.slice(css.indexOf("(") + 1, css.lastIndexOf(")")).split(",").map((stop) => stop.trim().split(/\s+/).map(parseFloat));
870
+ const ys = stops.map(([value]) => value);
871
+ const xs = stops.map(([, at], index) => at === void 0 || Number.isNaN(at) ? index / Math.max(1, stops.length - 1) : at / 100);
872
+ return (time) => {
873
+ let i = 1;
874
+ while (i < xs.length - 1 && time > xs[i]) i++;
875
+ const span = xs[i] - xs[i - 1] || 1;
876
+ return ys[i - 1] + (ys[i] - ys[i - 1]) * (time - xs[i - 1]) / span;
877
+ };
878
+ }
879
+ /** An easing as a function of time, `ease` for easings it doesn't know. */
880
+ function easingFunction(easing) {
881
+ if (typeof easing !== "string") return bezier(easing);
882
+ const css = easing.trim();
883
+ if (css.startsWith("steps(")) return steps(css);
884
+ if (css.startsWith("linear(")) return linearStops(css);
885
+ return bezier(curveOf(css) ?? CSS_EASINGS.ease);
886
+ }
887
+ /**
888
+ * The SMIL `keySplines` of an easing SMIL can express: a cubic Bézier
889
+ * curve whose four numbers lie within 0 and 1 (the CSS keywords, many
890
+ * `cubic-bezier()` curves, most names of {@link easings}). Browsers ignore
891
+ * an animation whose `keySplines` leave that range, so for other easings
892
+ * it returns nothing: sample them with {@link sampleEasing} instead.
893
+ *
894
+ * @example
895
+ * ```ts
896
+ * import { keySplinesOf } from 'chartist';
897
+ *
898
+ * keySplinesOf('ease-out'); // '0 0 0.58 1'
899
+ * keySplinesOf('easeOutBack'); // undefined: it overshoots
900
+ * ```
901
+ * @param easing A CSS easing, a name of {@link easings}, or four numbers
902
+ * @returns The `keySplines` value, or nothing
903
+ */
904
+ function keySplinesOf(easing) {
905
+ const curve = typeof easing === "string" ? curveOf(easing) : easing;
906
+ return curve?.length === 4 && curve.every((value) => value >= 0 && value <= 1) ? curve.join(" ") : void 0;
907
+ }
908
+ /**
909
+ * The eased progress of an animation at evenly spaced times, for SMIL
910
+ * `values` with even `keyTimes`: 12 points, and one per 50 ms for
911
+ * animations longer than 600 ms, at most 40. It takes the CSS easings
912
+ * (the keywords, `cubic-bezier()` curves, also ones that overshoot,
913
+ * `linear()` and `steps()`), the names of {@link easings} and four
914
+ * numbers of a cubic Bézier curve; anything else is sampled as `ease`.
915
+ * Points lie below 0 or above 1 where the easing overshoots.
916
+ *
917
+ * @example
918
+ * ```ts
919
+ * import { sampleEasing } from 'chartist';
920
+ *
921
+ * // A value that grows from 10 to 20 and overshoots a little on the way.
922
+ * const progress = sampleEasing('cubic-bezier(0.34, 1.56, 0.64, 1)', 400);
923
+ * const values = progress.map(p => 10 + (20 - 10) * p).join(';');
924
+ * ```
925
+ * @param easing The easing
926
+ * @param duration The animation's duration in ms
927
+ * @returns The progress at each point, from 0 to 1
928
+ */
929
+ function sampleEasing(easing, duration) {
930
+ const count = duration > 600 ? Math.min(40, Math.max(12, Math.round(duration / 50))) : 12;
931
+ const at = easingFunction(easing);
932
+ return Array.from({ length: count }, (_, index) => {
933
+ const time = index / (count - 1);
934
+ return index === 0 ? 0 : index === count - 1 ? 1 : at(time);
935
+ });
936
+ }
937
+ /**
938
+ * The value between `from` and `to` at `progress`: the numbers in `to`
939
+ * moved from their counterparts in `from`, when both have the same shape
940
+ * (`10px` and `40px`, two paths with the same commands), else `from`
941
+ * until halfway and `to` after.
942
+ */
943
+ function between(from, to, progress) {
944
+ const fromNumbers = from.match(NUMBER) ?? [];
945
+ const toNumbers = to.match(NUMBER) ?? [];
946
+ if (fromNumbers.length !== toNumbers.length || from.replace(NUMBER, "#") !== to.replace(NUMBER, "#")) return progress < .5 ? from : to;
947
+ let index = 0;
948
+ return to.replace(NUMBER, () => {
949
+ const start = Number(fromNumbers[index]);
950
+ const end = Number(toNumbers[index++]);
951
+ return String(+(start + (end - start) * progress).toFixed(4));
952
+ });
953
+ }
800
954
  /**
801
955
  * The property of an element that holds the emitter its descendants'
802
956
  * animations report to when `animate()` gets none. The compat plugin
@@ -816,13 +970,22 @@ function createAnimation(element, attribute, animationDefinition, createGuided =
816
970
  const attributeProperties = {};
817
971
  let animationEasing;
818
972
  let timeout;
819
- if (easing) animationEasing = Array.isArray(easing) ? easing : easings[easing];
973
+ if (easing) animationEasing = Array.isArray(easing) ? easing : easings[easing] ?? easing;
820
974
  def.begin = ensureUnit(def.begin, "ms");
821
975
  def.dur = ensureUnit(def.dur, "ms");
822
976
  if (animationEasing) {
823
- def.calcMode = "spline";
824
- def.keySplines = animationEasing.join(" ");
825
- def.keyTimes = "0;1";
977
+ const keySplines = keySplinesOf(animationEasing);
978
+ if (keySplines) {
979
+ def.calcMode = "spline";
980
+ def.keySplines = keySplines;
981
+ def.keyTimes = "0;1";
982
+ } else if (def.from !== void 0 && def.to !== void 0) {
983
+ const duration = quantity(def.dur);
984
+ const progress = sampleEasing(animationEasing, duration.unit === "s" ? duration.value * 1e3 : duration.value);
985
+ def.calcMode = String(animationEasing).trim().startsWith("steps(") ? "discrete" : "linear";
986
+ def.values = progress.map((at) => between(String(def.from), String(def.to), at)).join(";");
987
+ def.keyTimes = progress.map((_, index) => +(index / (progress.length - 1)).toFixed(4)).join(";");
988
+ }
826
989
  }
827
990
  if (createGuided) {
828
991
  def.fill = "freeze";
@@ -2234,7 +2397,6 @@ function seriesOption(series, options, key) {
2234
2397
  * ]
2235
2398
  * });
2236
2399
  * ```
2237
- * @experimental
2238
2400
  */
2239
2401
  function line(options = {}) {
2240
2402
  const classNames = {
@@ -2263,10 +2425,11 @@ function line(options = {}) {
2263
2425
  const seriesName = safeHasProperty(series, "name") && series.name;
2264
2426
  const seriesClassName = safeHasProperty(series, "className") && series.className;
2265
2427
  const seriesMeta = safeHasProperty(series, "meta") ? series.meta : void 0;
2428
+ const smoothing = interpolationOf(seriesOption(series, options, "lineSmooth"));
2266
2429
  const path = seriesPath(values, {
2267
2430
  x: axisX,
2268
2431
  y: axisY
2269
- }, area, shift, interpolationOf(seriesOption(series, options, "lineSmooth")), (valueIndex) => getMetaData(series, valueIndex));
2432
+ }, area, shift, smoothing, (valueIndex) => getMetaData(series, valueIndex));
2270
2433
  const seriesContext = {
2271
2434
  series,
2272
2435
  seriesIndex,
@@ -2304,7 +2467,8 @@ function line(options = {}) {
2304
2467
  ctx: {
2305
2468
  ...seriesContext,
2306
2469
  values,
2307
- path: path.clone()
2470
+ path: path.clone(),
2471
+ smoothing
2308
2472
  }
2309
2473
  }), ...children];
2310
2474
  if (seriesOption(series, options, "showArea") && axisY.range) {
@@ -2313,14 +2477,15 @@ function line(options = {}) {
2313
2477
  path.splitByCommand("M").filter((segment) => segment.pathElements.length > 1).map((segment) => {
2314
2478
  const first = segment.pathElements[0];
2315
2479
  const last = segment.pathElements[segment.pathElements.length - 1];
2316
- return segment.clone(true).position(0).remove(1).move(first.x, areaBaseProjected).line(first.x, first.y).position(segment.pathElements.length + 1).line(last.x, areaBaseProjected);
2480
+ return segment.clone(true).position(0).remove(1).move(first.x, areaBaseProjected).line(first.x, first.y, false, first.data).position(segment.pathElements.length + 1).line(last.x, areaBaseProjected);
2317
2481
  }).forEach((areaPath) => {
2318
2482
  children = [node("path", { d: areaPath.stringify() }, classNames.area, {
2319
2483
  role: "area",
2320
2484
  ctx: {
2321
2485
  ...seriesContext,
2322
2486
  values,
2323
- path: areaPath.clone()
2487
+ path: areaPath.clone(),
2488
+ smoothing
2324
2489
  }
2325
2490
  }), ...children];
2326
2491
  });
@@ -2729,7 +2894,6 @@ function presetAxes(config, boundOf) {
2729
2894
  * `{ x, y }` form.
2730
2895
  * @param extra `reverseData` and `normalize`, for the list form (the
2731
2896
  * `{ x, y }` form takes them inline).
2732
- * @experimental
2733
2897
  */
2734
2898
  function cartesianAxes(input, extra = {}) {
2735
2899
  const withOffset = (setup, dimension) => ({
@@ -2796,7 +2960,6 @@ const valueOf = (value, dimension) => Number(getMultiValue(value, dimension));
2796
2960
  * layers: [axisGrid(), line(), axisLabels()]
2797
2961
  * });
2798
2962
  * ```
2799
- * @experimental
2800
2963
  */
2801
2964
  function stepAxis() {
2802
2965
  return {
@@ -2855,7 +3018,6 @@ function stepAxis() {
2855
3018
  * layers: [axisGrid(), line(), axisLabels()]
2856
3019
  * });
2857
3020
  * ```
2858
- * @experimental
2859
3021
  */
2860
3022
  function autoScaleAxis() {
2861
3023
  return {
@@ -2915,7 +3077,6 @@ function autoScaleAxis() {
2915
3077
  * layers: [axisGrid(), line({ showLine: false }), axisLabels()]
2916
3078
  * });
2917
3079
  * ```
2918
- * @experimental
2919
3080
  */
2920
3081
  function fixedScaleAxis() {
2921
3082
  return {
@@ -3049,8 +3210,9 @@ function createPhaseEmitter() {
3049
3210
  return current;
3050
3211
  },
3051
3212
  notify(name, value) {
3052
- (listeners.get(name) ?? []).forEach(({ listener }) => listener(value));
3213
+ const results = (listeners.get(name) ?? []).map(({ listener }) => listener(value));
3053
3214
  observe(name, value);
3215
+ return results;
3054
3216
  },
3055
3217
  clear() {
3056
3218
  listeners.clear();
@@ -3117,7 +3279,6 @@ function findAll(scene, query = {}) {
3117
3279
  * });
3118
3280
  * const markup = mapped ? renderToString(mapped) : '';
3119
3281
  * ```
3120
- * @experimental
3121
3282
  */
3122
3283
  function mapNodes(scene, mapper) {
3123
3284
  let children = scene.children;
@@ -3262,7 +3423,7 @@ function layout(emitter, config, width, height) {
3262
3423
  validate(result);
3263
3424
  return result;
3264
3425
  }
3265
- function compose(emitter, config, data, area, measureText, axes = {}, normalized, padding = config.padding, bindings = [], labels = "html", measureStyle) {
3426
+ function compose(emitter, config, data, area, measureText, axes = {}, normalized, padding = config.padding, bindings = [], labels = "html", measureStyle, drawn = []) {
3266
3427
  const byId = new Map((axes.all ?? []).map((axis) => [axis.id, axis]));
3267
3428
  const layers = config.layers.map((layer, layerIndex) => {
3268
3429
  const binding = bindings[layerIndex] ?? {};
@@ -3292,10 +3453,16 @@ function compose(emitter, config, data, area, measureText, axes = {}, normalized
3292
3453
  labels,
3293
3454
  measureStyle
3294
3455
  };
3456
+ const nodes = layer.compose(context).map((top) => assignKeys(top, layerIndex));
3457
+ drawn.push({
3458
+ nodes: [],
3459
+ context
3460
+ });
3295
3461
  return {
3296
3462
  layer,
3297
3463
  context,
3298
- nodes: layer.compose(context).map((top) => assignKeys(top, layerIndex))
3464
+ nodes,
3465
+ layerIndex
3299
3466
  };
3300
3467
  });
3301
3468
  const refiners = layers.flatMap(({ layer, context }) => layer.refine ? [{
@@ -3310,9 +3477,12 @@ function compose(emitter, config, data, area, measureText, axes = {}, normalized
3310
3477
  }) : refined;
3311
3478
  }, current);
3312
3479
  const composed = [];
3313
- for (const { nodes } of layers) for (const top of nodes) {
3480
+ for (const { nodes, layerIndex } of layers) for (const top of nodes) {
3314
3481
  const result = mapNodes(top, (current) => current.role === void 0 ? void 0 : emitter.transform("compose", refiners.length > 0 ? refine(current) : current));
3315
- if (result) composed.push(result);
3482
+ if (result) {
3483
+ drawn[layerIndex].nodes.push(result);
3484
+ composed.push(result);
3485
+ }
3316
3486
  }
3317
3487
  return composed;
3318
3488
  }
@@ -3341,6 +3511,7 @@ function runPipeline(emitter, options, environment, previous, from = "resolve")
3341
3511
  rootClass: className,
3342
3512
  ...measureOptions
3343
3513
  }) : measure;
3514
+ const drawn = [];
3344
3515
  const measureText = withRootClass(environment.measureText);
3345
3516
  const measureStyle = withRootClass(environment.measureStyle);
3346
3517
  return {
@@ -3349,10 +3520,11 @@ function runPipeline(emitter, options, environment, previous, from = "resolve")
3349
3520
  layout: laidOut,
3350
3521
  axes,
3351
3522
  normalized,
3352
- scene: scene(emitter, config, compose(emitter, config, data, laidOut.area, measureText, axes, normalized, laidOut.padding, bindings, config.labels ?? environment.labels ?? "html", measureStyle), environment.rootSize ?? {
3523
+ scene: scene(emitter, config, compose(emitter, config, data, laidOut.area, measureText, axes, normalized, laidOut.padding, bindings, config.labels ?? environment.labels ?? "html", measureStyle, drawn), environment.rootSize ?? {
3353
3524
  width: config.width ?? laidOut.width,
3354
3525
  height: config.height ?? laidOut.height
3355
- })
3526
+ }),
3527
+ drawn
3356
3528
  };
3357
3529
  }
3358
3530
  /**
@@ -3360,7 +3532,6 @@ function runPipeline(emitter, options, environment, previous, from = "resolve")
3360
3532
  * below the top of the line (a typical font's ascent). Lines are one font
3361
3533
  * size apart, so a renderer finds a line's vertical center at
3362
3534
  * `y - (textAscent - 0.5) * fontSize` from its `y`.
3363
- * @experimental
3364
3535
  */
3365
3536
  const textAscent = .85;
3366
3537
  /**
@@ -3378,7 +3549,6 @@ const textAscent = .85;
3378
3549
  * return { ...size, width: size.width * 1.1 };
3379
3550
  * };
3380
3551
  * ```
3381
- * @experimental
3382
3552
  */
3383
3553
  const estimateText = (text, options = {}) => {
3384
3554
  const fontSize = options.fontSize ?? 12;
@@ -3390,7 +3560,27 @@ const estimateText = (text, options = {}) => {
3390
3560
  };
3391
3561
  //#endregion
3392
3562
  //#region src/render/dom.ts
3393
- function setAttribute(element, name, value) {
3563
+ /**
3564
+ * Sets one scene attribute on an element as the DOM renderer does: names
3565
+ * with a known prefix (`ct:value`) in their namespace, and `style` through
3566
+ * the CSSOM, which a Content Security Policy allows. For renderers that
3567
+ * change elements they keep.
3568
+ *
3569
+ * @example
3570
+ * ```ts
3571
+ * import { applyAttribute } from 'chartist';
3572
+ *
3573
+ * const point = document.querySelector('.ct-point');
3574
+ * if (point) {
3575
+ * applyAttribute(point, 'ct:value', 12);
3576
+ * applyAttribute(point, 'style', 'stroke-width: 12px;');
3577
+ * }
3578
+ * ```
3579
+ * @param element The element to change
3580
+ * @param name The attribute's name, as in a scene node's `attrs`
3581
+ * @param value The attribute's value
3582
+ */
3583
+ function applyAttribute(element, name, value) {
3394
3584
  const separator = name.indexOf(":");
3395
3585
  const prefix = separator > 0 ? name.slice(0, separator) : "";
3396
3586
  if (name === "style") setStyle(element, String(value));
@@ -3398,17 +3588,34 @@ function setAttribute(element, name, value) {
3398
3588
  else element.setAttribute(name, String(value));
3399
3589
  }
3400
3590
  /**
3401
- * Creates the element tree. Content of a `foreignObject` is HTML and is
3402
- * created in the XHTML namespace.
3591
+ * Creates the element tree of a scene node as the DOM renderer does: SVG
3592
+ * elements, HTML elements inside a `foreignObject`, attributes set with
3593
+ * {@link applyAttribute}. For renderers that create elements.
3594
+ *
3595
+ * @example
3596
+ * ```ts
3597
+ * import { node, renderElement } from 'chartist';
3598
+ *
3599
+ * const dot = renderElement(node('circle', { cx: 5, cy: 5, r: 4 }, 'ct-dot'), document);
3600
+ * document.querySelector('svg')?.append(dot);
3601
+ * ```
3602
+ * @param current The node to create
3603
+ * @param document The document to create the elements in
3604
+ * @param options The namespace, and a callback for each created element
3605
+ * @returns The node's element, with its children
3403
3606
  */
3404
- function build(document, current, elements, html = false) {
3607
+ function renderElement(current, document, options = {}) {
3608
+ const { html = false, created } = options;
3405
3609
  const element = document.createElementNS(html ? namespaces.xhtml : namespaces.svg, current.name);
3406
3610
  if (current.classes.length) element.setAttribute("class", current.classes.join(" "));
3407
- for (const [name, value] of Object.entries(current.attrs)) setAttribute(element, name, value);
3611
+ for (const [name, value] of Object.entries(current.attrs)) applyAttribute(element, name, value);
3408
3612
  if (current.content) element.appendChild(document.createTextNode(current.content));
3409
- const childrenAreHtml = html || current.name === "foreignObject";
3410
- for (const child of current.children) element.appendChild(build(document, child, elements, childrenAreHtml));
3411
- elements.set(current, element);
3613
+ const childOptions = html || current.name !== "foreignObject" ? options : {
3614
+ ...options,
3615
+ html: true
3616
+ };
3617
+ for (const child of current.children) element.appendChild(renderElement(child, document, childOptions));
3618
+ created?.(current, element);
3412
3619
  return element;
3413
3620
  }
3414
3621
  /** Removes Chartist SVGs (the ones with the ct namespace) from a container. */
@@ -3456,8 +3663,10 @@ function measureInDocument(document) {
3456
3663
  /**
3457
3664
  * Creates the renderer `chart()` uses by default: it renders scenes as
3458
3665
  * inline SVG into the container, replacing an earlier Chartist SVG there
3459
- * (as 1.x does), and measures text and reads styles for layout in the
3460
- * container, so its CSS applies.
3666
+ * (as 1.x does) on every render, and measures text and reads styles for
3667
+ * layout in the container, so its CSS applies. It ignores the previous
3668
+ * render it gets from a chart's second render on; `liveDomRenderer()`
3669
+ * from `chartist/live` keeps its SVG and changes it instead.
3461
3670
  * Wrap it to change what every render puts on the page.
3462
3671
  *
3463
3672
  * @example
@@ -3483,7 +3692,6 @@ function measureInDocument(document) {
3483
3692
  * renderer
3484
3693
  * });
3485
3694
  * ```
3486
- * @experimental
3487
3695
  */
3488
3696
  function domRenderer() {
3489
3697
  return {
@@ -3491,7 +3699,7 @@ function domRenderer() {
3491
3699
  render(scene, container) {
3492
3700
  const document = container.ownerDocument;
3493
3701
  const elements = /* @__PURE__ */ new WeakMap();
3494
- const svg = build(document, scene, elements);
3702
+ const svg = renderElement(scene, document, { created: (node, element) => elements.set(node, element) });
3495
3703
  svg.setAttributeNS(namespaces.xmlns, "xmlns:ct", namespaces.ct);
3496
3704
  removePrevious(container);
3497
3705
  container.appendChild(svg);
@@ -3513,6 +3721,35 @@ function domRenderer() {
3513
3721
  };
3514
3722
  }
3515
3723
  //#endregion
3724
+ //#region src/chart/changes.ts
3725
+ /** Adds an entry for `current` and each of its descendants, in document order. */
3726
+ function each$1(current, op, into) {
3727
+ into.push({
3728
+ op,
3729
+ node: current
3730
+ });
3731
+ current.children.forEach((child) => each$1(child, op, into));
3732
+ }
3733
+ /**
3734
+ * The changes of a render that replaced its output: every node of the
3735
+ * previous scene removed, every node of the new one added.
3736
+ */
3737
+ function replacementChanges(previous, next) {
3738
+ const changes = [];
3739
+ if (previous) each$1(previous, "remove", changes);
3740
+ each$1(next, "add", changes);
3741
+ return changes;
3742
+ }
3743
+ /**
3744
+ * Set on a chart handle by the 1.x classes and the compat plugin: the
3745
+ * chart never hands its renderer the previous render, so every render
3746
+ * draws new elements, as 1.x code expects. A global symbol, so the compat
3747
+ * build sets the same one as the core.
3748
+ */
3749
+ const REPLACES_OUTPUT = Symbol.for("chartist.replacesOutput");
3750
+ /** Whether the chart must replace its output on every render. */
3751
+ const replacesOutput = (handle) => handle[REPLACES_OUTPUT] === true;
3752
+ //#endregion
3516
3753
  //#region src/chart/className.ts
3517
3754
  /**
3518
3755
  * The chart type class for a chart's layers, so the stylesheet's chart
@@ -3565,7 +3802,6 @@ function withChartTypeClass(config) {
3565
3802
  * { plugins: [withOptions(lastPoint, { className: 'ct-last' })] }
3566
3803
  * );
3567
3804
  * ```
3568
- * @experimental
3569
3805
  */
3570
3806
  function withOptions(plugin, options) {
3571
3807
  return [plugin, options];
@@ -3631,16 +3867,37 @@ function asUserListeners(chart, add) {
3631
3867
  else add();
3632
3868
  }
3633
3869
  /**
3634
- * Installs v2 plugins in order and returns the event names they provide.
3635
- * Provided names are known before any plugin installs, so a plugin may
3636
- * listen to events another plugin provides. Listeners added while a
3637
- * plugin installs belong to plugins.
3870
+ * Installs v2 plugins in order and adds the event names they provide to
3871
+ * `provided`. Provided names are known before any plugin installs, so a
3872
+ * plugin may listen to events another plugin provides. Listeners added
3873
+ * while a plugin installs belong to plugins.
3638
3874
  */
3639
3875
  function installPlugins(chart, plugins = [], provided, scope) {
3640
3876
  scopes.set(chart, scope);
3641
3877
  const entries = normalize(plugins);
3642
3878
  for (const [plugin] of entries) plugin.provides?.forEach((name) => provided.add(name));
3643
- for (const [plugin, options] of entries) addingAs(scope, "plugin", () => plugin.install(chart, options));
3879
+ const cleanups = [];
3880
+ for (const [plugin, options] of entries) addingAs(scope, "plugin", () => {
3881
+ const cleanup = plugin.install(chart, options);
3882
+ if (typeof cleanup === "function") cleanups.push(cleanup);
3883
+ });
3884
+ return {
3885
+ plugins: entries.map(([plugin]) => plugin),
3886
+ cleanups
3887
+ };
3888
+ }
3889
+ /**
3890
+ * Calls every function, also when one throws, and throws the first error
3891
+ * afterwards.
3892
+ */
3893
+ function runAll(functions) {
3894
+ const errors = [];
3895
+ for (const run of functions) try {
3896
+ run();
3897
+ } catch (error) {
3898
+ errors.push(error);
3899
+ }
3900
+ if (errors.length) throw errors[0];
3644
3901
  }
3645
3902
  /** Registers `config.on` listeners. */
3646
3903
  function registerListeners(chart, listeners = {}) {
@@ -3741,7 +3998,6 @@ function resolveTarget(target) {
3741
3998
  * @param target The container element, or a CSS selector for it.
3742
3999
  * @param config The data, axes, layers and the rest of the chart.
3743
4000
  * @returns A handle to update the chart, listen to it or remove it.
3744
- * @experimental
3745
4001
  */
3746
4002
  function chart(target, config) {
3747
4003
  const container = resolveTarget(target);
@@ -3767,6 +4023,12 @@ function chart(target, config) {
3767
4023
  let watcher;
3768
4024
  let state;
3769
4025
  let output;
4026
+ let cleanups = [];
4027
+ let installed = {
4028
+ plugins: [],
4029
+ cleanups: []
4030
+ };
4031
+ let layerOf;
3770
4032
  let observer;
3771
4033
  let destroyed = false;
3772
4034
  let attached = true;
@@ -3774,7 +4036,7 @@ function chart(target, config) {
3774
4036
  const view = container.ownerDocument.defaultView;
3775
4037
  const fonts = container.ownerDocument.fonts;
3776
4038
  const onFontsLoaded = () => {
3777
- if (!destroyed && state && measuredText) render("layout");
4039
+ if (!destroyed && state && measuredText) render("layout", "fonts");
3778
4040
  };
3779
4041
  let printed;
3780
4042
  let printing = false;
@@ -3820,9 +4082,33 @@ function chart(target, config) {
3820
4082
  height: effective.viewBox?.height || svg.height || (fixedSize(effective.height) ?? container.clientHeight)
3821
4083
  };
3822
4084
  }
3823
- function render(from) {
4085
+ /**
4086
+ * Asks the plugins with `keyframes`, in order, then the layer that drew
4087
+ * the node, for the keyframes of a node's update animation.
4088
+ */
4089
+ function keyframes(previous, next, progress) {
4090
+ for (const plugin of installed.plugins) {
4091
+ const frames = plugin.keyframes?.(previous, next, progress);
4092
+ if (frames) return frames;
4093
+ }
4094
+ if (!state) return;
4095
+ if (!layerOf) {
4096
+ const map = /* @__PURE__ */ new WeakMap();
4097
+ const mark = (drawnNode, index) => {
4098
+ map.set(drawnNode, index);
4099
+ drawnNode.children.forEach((child) => mark(child, index));
4100
+ };
4101
+ state.drawn.forEach(({ nodes }, index) => nodes.forEach((top) => mark(top, index)));
4102
+ layerOf = map;
4103
+ }
4104
+ const index = layerOf.get(next);
4105
+ return index === void 0 ? void 0 : state.config.layers[index].keyframes?.(previous, next, state.drawn[index].context, progress);
4106
+ }
4107
+ function render(from, cause) {
3824
4108
  const { width, height } = size();
3825
4109
  measuredText = false;
4110
+ const previous = state;
4111
+ const previousOutput = output;
3826
4112
  state = runPipeline(emitter, withChartTypeClass(effective), {
3827
4113
  width,
3828
4114
  height,
@@ -3835,18 +4121,35 @@ function chart(target, config) {
3835
4121
  }
3836
4122
  }, state, from);
3837
4123
  const { scene } = state;
3838
- output = renderer.render(scene, container);
4124
+ layerOf = void 0;
4125
+ const pending = cleanups;
4126
+ cleanups = [];
4127
+ runAll(pending);
4128
+ const update = previous && previousOutput && !replacesOutput(handle) ? {
4129
+ previous: previousOutput,
4130
+ previousScene: previous.scene,
4131
+ cause,
4132
+ labels: state.data.labels,
4133
+ previousLabels: previous.data.labels,
4134
+ keyframes
4135
+ } : void 0;
4136
+ const rendered = renderer.render(scene, container, update);
4137
+ output = rendered;
3839
4138
  if (printing) applyPrintViewBox();
3840
- emitter.notify("render", {
4139
+ const event = {
3841
4140
  scene,
3842
- output
3843
- });
3844
- const elementOf = output.elementOf ?? (() => void 0);
3845
- emitter.notify("mount", {
3846
- scene,
3847
- output,
3848
- elementOf
3849
- });
4141
+ output: rendered,
4142
+ cause,
4143
+ changes: update && rendered.patched && rendered.changes ? rendered.changes : replacementChanges(previous?.scene, scene),
4144
+ previous: previous?.scene
4145
+ };
4146
+ emitter.notify("render", event);
4147
+ const elementOf = (node) => rendered.elementOf?.(node) ?? previousOutput?.elementOf?.(node);
4148
+ cleanups = emitter.notify("mount", {
4149
+ ...event,
4150
+ elementOf,
4151
+ animation: rendered.animation
4152
+ }).filter((result) => typeof result === "function");
3850
4153
  }
3851
4154
  function resolveResponsive() {
3852
4155
  const entries = current.responsive?.entries;
@@ -3862,7 +4165,7 @@ function chart(target, config) {
3862
4165
  previousConfig,
3863
4166
  currentConfig: effective
3864
4167
  });
3865
- render("resolve");
4168
+ render("resolve", "responsive");
3866
4169
  }
3867
4170
  /** (Re)creates the watcher of the responsive entries. */
3868
4171
  function watch() {
@@ -3881,7 +4184,7 @@ function chart(target, config) {
3881
4184
  };
3882
4185
  if ("responsive" in changes) watch();
3883
4186
  else resolveResponsive();
3884
- render("resolve");
4187
+ render("resolve", "update");
3885
4188
  return handle;
3886
4189
  },
3887
4190
  detach() {
@@ -3900,8 +4203,15 @@ function chart(target, config) {
3900
4203
  if (destroyed) return;
3901
4204
  destroyed = true;
3902
4205
  handle.detach();
3903
- output?.remove?.();
3904
- emitter.clear();
4206
+ const pending = cleanups;
4207
+ cleanups = [];
4208
+ try {
4209
+ runAll(pending);
4210
+ } finally {
4211
+ output?.remove?.();
4212
+ emitter.clear();
4213
+ runAll([...installed.cleanups].reverse());
4214
+ }
3905
4215
  },
3906
4216
  ...eventMethods(emitter, () => handle, provided, scope)
3907
4217
  };
@@ -3917,15 +4227,15 @@ function chart(target, config) {
3917
4227
  return;
3918
4228
  }
3919
4229
  const { width, height } = size();
3920
- if (width !== state.layout.width || height !== state.layout.height) render("layout");
4230
+ if (width !== state.layout.width || height !== state.layout.height) render("layout", "resize");
3921
4231
  });
3922
4232
  observer.observe(container);
3923
4233
  }
3924
- installPlugins(handle, config.plugins, provided, scope);
4234
+ installed = installPlugins(handle, config.plugins, provided, scope);
3925
4235
  registerListeners(handle, config.on);
3926
4236
  if (attached) watch();
3927
4237
  else resolveResponsive();
3928
- render("resolve");
4238
+ render("resolve", "create");
3929
4239
  return handle;
3930
4240
  }
3931
4241
  //#endregion
@@ -4158,7 +4468,9 @@ function svgLabels(measureText, kind, override = {}) {
4158
4468
  * ]
4159
4469
  * );
4160
4470
  * ```
4161
- * @experimental
4471
+ * @experimental It rests on `Element.matchContainer()`, a draft standard
4472
+ * (w3c/csswg-drafts#6205), through a polyfill: it may still change in a
4473
+ * 2.x minor.
4162
4474
  */
4163
4475
  function container(query) {
4164
4476
  return Object.freeze({
@@ -4275,7 +4587,6 @@ function watchResponsive(entries, host, onChange, observeResize = false) {
4275
4587
  * ])
4276
4588
  * });
4277
4589
  * ```
4278
- * @experimental
4279
4590
  */
4280
4591
  function responsive(entries) {
4281
4592
  return {
@@ -4383,7 +4694,7 @@ function rootStyle(getOptions, state) {
4383
4694
  */
4384
4695
  function presetConfig(spec, data, options) {
4385
4696
  const resolved = extend({}, spec.defaults, options);
4386
- const { plugins, on } = options ?? {};
4697
+ const { plugins, on, renderer } = options ?? {};
4387
4698
  return {
4388
4699
  ...spec.config({
4389
4700
  ...data,
@@ -4391,7 +4702,8 @@ function presetConfig(spec, data, options) {
4391
4702
  series: data.series || []
4392
4703
  }, resolved),
4393
4704
  plugins,
4394
- on
4705
+ on,
4706
+ ...renderer && { renderer }
4395
4707
  };
4396
4708
  }
4397
4709
  /** Internal: creates a preset chart; used by the preset functions and classes. */
@@ -4447,7 +4759,8 @@ function createPreset(spec, target, initialData, options = {}, responsiveOptions
4447
4759
  const inner = chart(target, {
4448
4760
  ...withState(spec.config(data, current)),
4449
4761
  plugins: [rootStyle(() => current, state), ...plugins ?? []],
4450
- on: options.on
4762
+ on: options.on,
4763
+ renderer: options.renderer
4451
4764
  });
4452
4765
  function onResponsiveChange() {
4453
4766
  const previousOptions = current;
@@ -4582,7 +4895,6 @@ const lineSpec = {
4582
4895
  * toScene(config, { width: 600, height: 300 })
4583
4896
  * );
4584
4897
  * ```
4585
- * @experimental
4586
4898
  */
4587
4899
  function lineChartConfig(data, options = {}) {
4588
4900
  return presetConfig(lineSpec, data, options);
@@ -4595,7 +4907,6 @@ function createLineChart(target, data, options = {}, responsiveOptions, plugins)
4595
4907
  * Creates a line chart with the 1.x options and output, rendered
4596
4908
  * synchronously. Plugins must be v2 plugins; convert 1.x plugins with
4597
4909
  * `fromLegacyPlugin` from `chartist/compat`.
4598
- * @experimental
4599
4910
  */
4600
4911
  function lineChart(target, data, options = {}, responsiveOptions) {
4601
4912
  return createLineChart(target, data, options, responsiveOptions).handle;
@@ -4667,6 +4978,7 @@ var PresetChart = class extends BaseChart {
4667
4978
  name: "chart-listeners",
4668
4979
  install: (handle) => {
4669
4980
  this.installing = handle;
4981
+ handle[REPLACES_OUTPUT] = true;
4670
4982
  asUserListeners(handle, () => queued.forEach(([event, listener]) => this.on(event, listener)));
4671
4983
  }
4672
4984
  };
@@ -4860,7 +5172,6 @@ function rawSeries(series) {
4860
5172
  * className: 'ct-chart-bar'
4861
5173
  * });
4862
5174
  * ```
4863
- * @experimental
4864
5175
  */
4865
5176
  function bar(options = {}) {
4866
5177
  const classNames = {
@@ -5160,7 +5471,6 @@ const barSpec = {
5160
5471
  * options, without a container: the bar layer with its axes, grid and labels, horizontal or stacked as the options say. Pass it to `toScene()` to render
5161
5472
  * the preset chart with another renderer. Responsive options and the
5162
5473
  * inline size style of the root svg are left out; both need a container.
5163
- * @experimental
5164
5474
  */
5165
5475
  function barChartConfig(data, options = {}) {
5166
5476
  return presetConfig(barSpec, data, options);
@@ -5173,7 +5483,6 @@ function createBarChart(target, data, options = {}, responsiveOptions, plugins)
5173
5483
  * Creates a bar chart with the 1.x options and output, rendered
5174
5484
  * synchronously. Plugins must be v2 plugins; convert 1.x plugins with
5175
5485
  * `fromLegacyPlugin` from `chartist/compat`.
5176
- * @experimental
5177
5486
  */
5178
5487
  function barChart(target, data, options = {}, responsiveOptions) {
5179
5488
  return createBarChart(target, data, options, responsiveOptions).handle;
@@ -5289,7 +5598,6 @@ function labelText(text, position, anchor, className, ctx) {
5289
5598
  * className: 'ct-chart-donut'
5290
5599
  * });
5291
5600
  * ```
5292
- * @experimental
5293
5601
  */
5294
5602
  function pie(options = {}) {
5295
5603
  const classNames = {
@@ -5493,7 +5801,6 @@ const pieSpec = {
5493
5801
  * options, without a container: the pie layer with its labels. Pass it to `toScene()` to render
5494
5802
  * the preset chart with another renderer. Responsive options and the
5495
5803
  * inline size style of the root svg are left out; both need a container.
5496
- * @experimental
5497
5804
  */
5498
5805
  function pieChartConfig(data, options = {}) {
5499
5806
  return presetConfig(pieSpec, data, options);
@@ -5506,7 +5813,6 @@ function createPieChart(target, data, options = {}, responsiveOptions, plugins)
5506
5813
  * Creates a pie or donut chart with the 1.x options and output, rendered
5507
5814
  * synchronously. Plugins must be v2 plugins; convert 1.x plugins with
5508
5815
  * `fromLegacyPlugin` from `chartist/compat`.
5509
- * @experimental
5510
5816
  */
5511
5817
  function pieChart(target, data, options = {}, responsiveOptions) {
5512
5818
  return createPieChart(target, data, options, responsiveOptions).handle;
@@ -5818,7 +6124,6 @@ var interpolation_exports = /* @__PURE__ */ __exportAll({
5818
6124
  * // The color of series `c`, the third.
5819
6125
  * export const third = seriesColors[2];
5820
6126
  * ```
5821
- * @experimental
5822
6127
  */
5823
6128
  const seriesColors = [
5824
6129
  "#d70206",
@@ -5901,7 +6206,6 @@ function serialize$1(current, position) {
5901
6206
  * return renderToString(scene);
5902
6207
  * }
5903
6208
  * ```
5904
- * @experimental
5905
6209
  */
5906
6210
  function renderToString(scene) {
5907
6211
  return serialize$1(scene, "root");
@@ -5964,7 +6268,6 @@ function unavailable(method) {
5964
6268
  *
5965
6269
  * export const markup = renderToString(scene);
5966
6270
  * ```
5967
- * @experimental
5968
6271
  */
5969
6272
  function toScene(config, size) {
5970
6273
  const emitter = createPhaseEmitter();
@@ -6131,7 +6434,6 @@ function edgeContext(option, context) {
6131
6434
  * ]
6132
6435
  * });
6133
6436
  * ```
6134
- * @experimental
6135
6437
  */
6136
6438
  function band(options) {
6137
6439
  const hasEdges = options.low !== void 0 || options.high !== void 0;
@@ -6171,6 +6473,7 @@ function band(options) {
6171
6473
  const clamp = (value) => range ? Math.min(Math.max(value, range.min), range.max) : value;
6172
6474
  const edges = bandEdges(options, context, clamp);
6173
6475
  let pieces;
6476
+ const interpolation = interpolationOf(options.lineSmooth ?? true);
6174
6477
  if (edges.constant) {
6175
6478
  const { low, high } = edges.at(0);
6176
6479
  const lowY = area.y1 - axisY.project(low);
@@ -6191,7 +6494,6 @@ function band(options) {
6191
6494
  });
6192
6495
  }
6193
6496
  const shift = centerShift(axisX, options.align);
6194
- const interpolation = interpolationOf(options.lineSmooth ?? true);
6195
6497
  const pathOf = (values) => seriesPath(values, {
6196
6498
  x: axisX,
6197
6499
  y: axisY
@@ -6214,7 +6516,8 @@ function band(options) {
6214
6516
  role: "band",
6215
6517
  ctx: {
6216
6518
  ...edgesContext,
6217
- path: path.clone()
6519
+ path: path.clone(),
6520
+ smoothing: interpolation
6218
6521
  }
6219
6522
  }));
6220
6523
  return [withFields(node("g", {}, options.className ? `${classNames.band} ${options.className}` : classNames.band), { children })];
@@ -6285,7 +6588,6 @@ const LABEL_INSET = 4;
6285
6588
  * ]
6286
6589
  * });
6287
6590
  * ```
6288
- * @experimental
6289
6591
  */
6290
6592
  function targetLine(options) {
6291
6593
  const { value, label, className } = options;
@@ -6455,7 +6757,6 @@ function hasX(values) {
6455
6757
  * layers: [axisGrid(), line(), trendLine(), axisLabels()]
6456
6758
  * });
6457
6759
  * ```
6458
- * @experimental
6459
6760
  */
6460
6761
  function trendLine(options = {}) {
6461
6762
  const classNames = {
@@ -6630,7 +6931,6 @@ function nodesOf(axis, context, classNames, svgLabel) {
6630
6931
  * ```
6631
6932
  * @param options The axes to draw and their classes; `background` adds a
6632
6933
  * `rect.ct-grid-background` over the chart area, behind the lines.
6633
- * @experimental
6634
6934
  */
6635
6935
  function axisGrid(options = {}) {
6636
6936
  const classNames = {
@@ -6685,7 +6985,6 @@ function axisGrid(options = {}) {
6685
6985
  * ]
6686
6986
  * });
6687
6987
  * ```
6688
- * @experimental
6689
6988
  */
6690
6989
  function axisLabels(options = {}) {
6691
6990
  const classNames = {
@@ -6726,6 +7025,7 @@ exports.Svg = Svg;
6726
7025
  exports.SvgList = SvgList;
6727
7026
  exports.SvgPath = SvgPath;
6728
7027
  exports.alphaNumerate = alphaNumerate;
7028
+ exports.applyAttribute = applyAttribute;
6729
7029
  exports.autoScaleAxis = autoScaleAxis;
6730
7030
  exports.axisGrid = axisGrid;
6731
7031
  exports.axisLabels = axisLabels;
@@ -6765,6 +7065,7 @@ exports.isDataHoleValue = isDataHoleValue;
6765
7065
  exports.isFalseyButZero = isFalseyButZero;
6766
7066
  exports.isMultiValue = isMultiValue;
6767
7067
  exports.isNumeric = isNumeric;
7068
+ exports.keySplinesOf = keySplinesOf;
6768
7069
  exports.line = line;
6769
7070
  exports.lineChart = lineChart;
6770
7071
  exports.lineChartConfig = lineChartConfig;
@@ -6783,11 +7084,13 @@ exports.polarToCartesian = polarToCartesian;
6783
7084
  exports.precision = precision;
6784
7085
  exports.projectLength = projectLength;
6785
7086
  exports.quantity = quantity;
7087
+ exports.renderElement = renderElement;
6786
7088
  exports.renderToString = renderToString;
6787
7089
  exports.responsive = responsive;
6788
7090
  exports.rho = rho;
6789
7091
  exports.roundWithPrecision = roundWithPrecision;
6790
7092
  exports.safeHasProperty = safeHasProperty;
7093
+ exports.sampleEasing = sampleEasing;
6791
7094
  exports.serialMap = serialMap;
6792
7095
  exports.serialize = serialize;
6793
7096
  exports.seriesColors = seriesColors;