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