chartist 2.0.0-rc.2 → 2.0.0-rc.3

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
@@ -533,6 +533,21 @@ function deserialize(data) {
533
533
  return parsedData;
534
534
  }
535
535
  //#endregion
536
+ //#region src/render/style.ts
537
+ /**
538
+ * Applies a style declaration list through the CSSOM. A Content Security
539
+ * Policy without `'unsafe-inline'` for styles blocks a `style` attribute set
540
+ * by a script, but not `element.style`; the browser still reflects the
541
+ * declarations into the `style` attribute, so the markup stays the same.
542
+ * Elements without a `style` property (none in HTML or SVG documents) get
543
+ * the attribute.
544
+ */
545
+ function setStyle(element, value) {
546
+ const { style } = element;
547
+ if (style) style.cssText = value;
548
+ else element.setAttribute("style", value);
549
+ }
550
+ //#endregion
536
551
  //#region src/svg/SvgList.ts
537
552
  /**
538
553
  * A list of {@link Svg} wrappers that calls an `Svg` method on all of them
@@ -920,7 +935,8 @@ var Svg = class Svg {
920
935
  }
921
936
  Object.keys(attributes).forEach((key) => {
922
937
  if (attributes[key] === void 0) return;
923
- if (key.indexOf(":") !== -1) {
938
+ if (key === "style") setStyle(this._node, String(attributes[key]));
939
+ else if (key.indexOf(":") !== -1) {
924
940
  const namespacedAttribute = key.split(":");
925
941
  this._node.setAttributeNS(namespaces[namespacedAttribute[0]], key, String(attributes[key]));
926
942
  } else this._node.setAttribute(key, String(attributes[key]));
@@ -1505,6 +1521,65 @@ var BaseChart = class {
1505
1521
  }
1506
1522
  };
1507
1523
  //#endregion
1524
+ //#region src/scene/node.ts
1525
+ const splitClasses = (names) => names.split(/\s+/).filter(Boolean);
1526
+ const builder = {
1527
+ attr(attributes) {
1528
+ const attrs = { ...this.attrs };
1529
+ for (const [name, value] of Object.entries(attributes)) if (value === void 0 || value === null) delete attrs[name];
1530
+ else attrs[name] = value;
1531
+ return withFields(this, { attrs });
1532
+ },
1533
+ addClass(names) {
1534
+ const added = splitClasses(names).filter((name, i, all) => !this.classes.includes(name) && all.indexOf(name) === i);
1535
+ return added.length ? withFields(this, { classes: [...this.classes, ...added] }) : this;
1536
+ },
1537
+ removeClass(names) {
1538
+ const removed = splitClasses(names);
1539
+ return withFields(this, { classes: this.classes.filter((name) => !removed.includes(name)) });
1540
+ },
1541
+ append(child, insertFirst = false) {
1542
+ return withFields(this, { children: insertFirst ? [child, ...this.children] : [...this.children, child] });
1543
+ },
1544
+ remove(child) {
1545
+ return withFields(this, { children: this.children.filter((existing) => existing !== child) });
1546
+ },
1547
+ replace(child, next) {
1548
+ return withFields(this, { children: this.children.map((existing) => existing === child ? next : existing) });
1549
+ },
1550
+ text(content) {
1551
+ return withFields(this, { content });
1552
+ },
1553
+ empty() {
1554
+ return withFields(this, {
1555
+ children: [],
1556
+ content: void 0
1557
+ });
1558
+ }
1559
+ };
1560
+ function create(fields) {
1561
+ const created = Object.create(builder);
1562
+ for (const [field, value] of Object.entries(fields)) if (value !== void 0) created[field] = value;
1563
+ return created;
1564
+ }
1565
+ function withFields(source, changes) {
1566
+ return create({
1567
+ ...source,
1568
+ ...changes
1569
+ });
1570
+ }
1571
+ function node(name, attributes = {}, className = "", meta = {}) {
1572
+ const attrs = {};
1573
+ for (const [attr, value] of Object.entries(attributes)) if (value !== void 0 && value !== null) attrs[attr] = value;
1574
+ return create({
1575
+ name,
1576
+ attrs,
1577
+ classes: [...new Set(splitClasses(className))],
1578
+ children: [],
1579
+ ...meta
1580
+ });
1581
+ }
1582
+ //#endregion
1508
1583
  //#region src/svg/SvgPath.ts
1509
1584
  /**
1510
1585
  * The parameters of each SVG path command, in the order the `d` attribute
@@ -2031,63 +2106,57 @@ function monotoneCubic(options) {
2031
2106
  };
2032
2107
  }
2033
2108
  //#endregion
2034
- //#region src/scene/node.ts
2035
- const splitClasses = (names) => names.split(/\s+/).filter(Boolean);
2036
- const builder = {
2037
- attr(attributes) {
2038
- const attrs = { ...this.attrs };
2039
- for (const [name, value] of Object.entries(attributes)) if (value === void 0 || value === null) delete attrs[name];
2040
- else attrs[name] = value;
2041
- return withFields(this, { attrs });
2042
- },
2043
- addClass(names) {
2044
- const added = splitClasses(names).filter((name, i, all) => !this.classes.includes(name) && all.indexOf(name) === i);
2045
- return added.length ? withFields(this, { classes: [...this.classes, ...added] }) : this;
2046
- },
2047
- removeClass(names) {
2048
- const removed = splitClasses(names);
2049
- return withFields(this, { classes: this.classes.filter((name) => !removed.includes(name)) });
2050
- },
2051
- append(child, insertFirst = false) {
2052
- return withFields(this, { children: insertFirst ? [child, ...this.children] : [...this.children, child] });
2053
- },
2054
- remove(child) {
2055
- return withFields(this, { children: this.children.filter((existing) => existing !== child) });
2056
- },
2057
- replace(child, next) {
2058
- return withFields(this, { children: this.children.map((existing) => existing === child ? next : existing) });
2059
- },
2060
- text(content) {
2061
- return withFields(this, { content });
2062
- },
2063
- empty() {
2064
- return withFields(this, {
2065
- children: [],
2066
- content: void 0
2067
- });
2068
- }
2069
- };
2070
- function create(fields) {
2071
- const created = Object.create(builder);
2072
- for (const [field, value] of Object.entries(fields)) if (value !== void 0) created[field] = value;
2073
- return created;
2109
+ //#region src/layers/path.ts
2110
+ /** The interpolation for a `lineSmooth` option: smooth, straight or given. */
2111
+ function interpolationOf(lineSmooth) {
2112
+ return typeof lineSmooth === "function" ? lineSmooth : lineSmooth ? monotoneCubic() : none();
2074
2113
  }
2075
- function withFields(source, changes) {
2076
- return create({
2077
- ...source,
2078
- ...changes
2079
- });
2114
+ /**
2115
+ * The px shift that puts points in the middle of each step of a step axis,
2116
+ * where bars are centered, for `align: 'center'`.
2117
+ */
2118
+ function centerShift(axisX, align) {
2119
+ return align === "center" && axisX.kind === "step" && !axisX.stretch ? (axisX.step ?? 0) / 2 : 0;
2080
2120
  }
2081
- function node(name, attributes = {}, className = "", meta = {}) {
2082
- const attrs = {};
2083
- for (const [attr, value] of Object.entries(attributes)) if (value !== void 0 && value !== null) attrs[attr] = value;
2084
- return create({
2085
- name,
2086
- attrs,
2087
- classes: [...new Set(splitClasses(className))],
2088
- children: [],
2089
- ...meta
2121
+ /**
2122
+ * Projects a series' values into the chart area and interpolates a path
2123
+ * through them. Holes break the path unless the interpolation fills them.
2124
+ * @param meta The meta of a value, attached to its path element.
2125
+ */
2126
+ function seriesPath(values, axes, area, shift, interpolation, meta = () => void 0) {
2127
+ const pathCoordinates = [];
2128
+ const pathData = [];
2129
+ values.forEach((value, valueIndex) => {
2130
+ pathCoordinates.push(area.x1 + shift + axes.x.project(value, valueIndex, values), area.y1 - axes.y.project(value, valueIndex, values));
2131
+ pathData.push({
2132
+ value,
2133
+ valueIndex,
2134
+ meta: meta(valueIndex)
2135
+ });
2090
2136
  });
2137
+ return interpolation(pathCoordinates, pathData);
2138
+ }
2139
+ /**
2140
+ * The same path traced backward: it starts where the path ends, and each
2141
+ * segment runs to the previous point, with a curve's control points
2142
+ * swapped, so it draws the same shape. Holes (`M` commands within the
2143
+ * path) are kept between the same points. Arcs aren't supported.
2144
+ */
2145
+ function reversePath(path) {
2146
+ const reversed = new SvgPath();
2147
+ const elements = path.pathElements;
2148
+ for (let i = elements.length - 1; i >= 0; i--) {
2149
+ const element = elements[i];
2150
+ const target = elements[i - 1];
2151
+ if (i === elements.length - 1 || elements[i + 1].command === "M") reversed.move(element.x, element.y, false, element.data);
2152
+ if (element.command === "M" || !target) continue;
2153
+ if (element.command === "C") {
2154
+ const curve = element;
2155
+ reversed.curve(curve.x2, curve.y2, curve.x1, curve.y1, target.x, target.y, false, target.data);
2156
+ } else if (element.command === "L") reversed.line(target.x, target.y, false, target.data);
2157
+ else throw new Error(`Chartist: can't reverse a path with "${element.command}" commands`);
2158
+ }
2159
+ return reversed;
2091
2160
  }
2092
2161
  //#endregion
2093
2162
  //#region src/layers/line.ts
@@ -2102,7 +2171,7 @@ const DEFAULTS = {
2102
2171
  showArea: false,
2103
2172
  areaBase: 0
2104
2173
  };
2105
- const CLASS_NAMES$2 = {
2174
+ const CLASS_NAMES$5 = {
2106
2175
  series: "ct-series",
2107
2176
  line: "ct-line",
2108
2177
  point: "ct-point",
@@ -2168,7 +2237,7 @@ function seriesOption(series, options, key) {
2168
2237
  */
2169
2238
  function line(options = {}) {
2170
2239
  const classNames = {
2171
- ...CLASS_NAMES$2,
2240
+ ...CLASS_NAMES$5,
2172
2241
  ...options.classNames
2173
2242
  };
2174
2243
  return {
@@ -2183,7 +2252,7 @@ function line(options = {}) {
2183
2252
  const { x: axisX, y: axisY } = axes;
2184
2253
  if (!axisX || !axisY || !normalized) throw new Error("Layer \"line\" needs an x and a y axis");
2185
2254
  if (seriesIndexes && options.reverseData) throw new Error("Layer \"line\": a series selection can't be combined with reverseData");
2186
- const shift = options.align === "center" && axisX.kind === "step" && !axisX.stretch ? (axisX.step ?? 0) / 2 : 0;
2255
+ const shift = centerShift(axisX, options.align);
2187
2256
  const order = data.series.map((_, i) => i);
2188
2257
  if (options.reverseData) order.reverse();
2189
2258
  return order.map((dataIndex, localIndex) => {
@@ -2193,18 +2262,10 @@ function line(options = {}) {
2193
2262
  const seriesName = safeHasProperty(series, "name") && series.name;
2194
2263
  const seriesClassName = safeHasProperty(series, "className") && series.className;
2195
2264
  const seriesMeta = safeHasProperty(series, "meta") ? series.meta : void 0;
2196
- const pathCoordinates = [];
2197
- const pathData = [];
2198
- values.forEach((value, valueIndex) => {
2199
- pathCoordinates.push(area.x1 + shift + axisX.project(value, valueIndex, values), area.y1 - axisY.project(value, valueIndex, values));
2200
- pathData.push({
2201
- value,
2202
- valueIndex,
2203
- meta: getMetaData(series, valueIndex)
2204
- });
2205
- });
2206
- const lineSmooth = seriesOption(series, options, "lineSmooth");
2207
- const path = (typeof lineSmooth === "function" ? lineSmooth : lineSmooth ? monotoneCubic() : none())(pathCoordinates, pathData);
2265
+ const path = seriesPath(values, {
2266
+ x: axisX,
2267
+ y: axisY
2268
+ }, area, shift, interpolationOf(seriesOption(series, options, "lineSmooth")), (valueIndex) => getMetaData(series, valueIndex));
2208
2269
  const seriesContext = {
2209
2270
  series,
2210
2271
  seriesIndex,
@@ -3201,9 +3262,8 @@ function layout(emitter, config, width, height) {
3201
3262
  return result;
3202
3263
  }
3203
3264
  function compose(emitter, config, data, area, measureText, axes = {}, normalized, padding = config.padding, bindings = [], labels = "html", measureStyle) {
3204
- const composed = [];
3205
3265
  const byId = new Map((axes.all ?? []).map((axis) => [axis.id, axis]));
3206
- config.layers.forEach((layer, layerIndex) => {
3266
+ const layers = config.layers.map((layer, layerIndex) => {
3207
3267
  const binding = bindings[layerIndex] ?? {};
3208
3268
  const selection = data.flat ? void 0 : binding.series;
3209
3269
  const layerData = selection ? {
@@ -3214,7 +3274,7 @@ function compose(emitter, config, data, area, measureText, axes = {}, normalized
3214
3274
  ...normalized,
3215
3275
  series: selection.map((i) => normalized.series[i])
3216
3276
  } : normalized;
3217
- const nodes = layer.compose({
3277
+ const context = {
3218
3278
  data: layerData,
3219
3279
  area,
3220
3280
  padding,
@@ -3230,12 +3290,29 @@ function compose(emitter, config, data, area, measureText, axes = {}, normalized
3230
3290
  seriesIndexes: selection,
3231
3291
  labels,
3232
3292
  measureStyle
3233
- });
3234
- for (const top of nodes) {
3235
- const result = mapNodes(assignKeys(top, layerIndex), (current) => current.role === void 0 ? void 0 : emitter.transform("compose", current));
3236
- if (result) composed.push(result);
3237
- }
3293
+ };
3294
+ return {
3295
+ layer,
3296
+ context,
3297
+ nodes: layer.compose(context).map((top) => assignKeys(top, layerIndex))
3298
+ };
3238
3299
  });
3300
+ const refiners = layers.flatMap(({ layer, context }) => layer.refine ? [{
3301
+ refine: layer.refine.bind(layer),
3302
+ context
3303
+ }] : []);
3304
+ const refine = (current) => refiners.reduce((refined, { refine: refineNode, context }) => {
3305
+ const result = refineNode(refined, context);
3306
+ return result ? withFields(result, {
3307
+ key: refined.key,
3308
+ children: refined.children
3309
+ }) : refined;
3310
+ }, current);
3311
+ const composed = [];
3312
+ for (const { nodes } of layers) for (const top of nodes) {
3313
+ 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);
3315
+ }
3239
3316
  return composed;
3240
3317
  }
3241
3318
  function scene(emitter, config, children, size) {
@@ -3315,7 +3392,8 @@ const estimateText = (text, options = {}) => {
3315
3392
  function setAttribute(element, name, value) {
3316
3393
  const separator = name.indexOf(":");
3317
3394
  const prefix = separator > 0 ? name.slice(0, separator) : "";
3318
- if (prefix && prefix !== "xmlns" && namespaces[prefix]) element.setAttributeNS(namespaces[prefix], name, String(value));
3395
+ if (name === "style") setStyle(element, String(value));
3396
+ else if (prefix && prefix !== "xmlns" && namespaces[prefix]) element.setAttributeNS(namespaces[prefix], name, String(value));
3319
3397
  else element.setAttribute(name, String(value));
3320
3398
  }
3321
3399
  /**
@@ -3343,7 +3421,7 @@ function removePrevious(container) {
3343
3421
  */
3344
3422
  function probe(document, name, options, read) {
3345
3423
  const svg = document.createElementNS(namespaces.svg, "svg");
3346
- svg.setAttribute("style", "position: absolute; visibility: hidden; width: 0; height: 0;");
3424
+ setStyle(svg, "position: absolute; visibility: hidden; width: 0; height: 0;");
3347
3425
  if (options.rootClass) svg.setAttribute("class", options.rootClass);
3348
3426
  const node = document.createElementNS(namespaces.svg, name);
3349
3427
  if (options.classes?.length) node.setAttribute("class", options.classes.join(" "));
@@ -3725,7 +3803,7 @@ function chart(target, config) {
3725
3803
  probe.setAttribute("width", width);
3726
3804
  probe.setAttribute("height", height);
3727
3805
  const px = (value) => typeof value === "number" ? "px" : "";
3728
- probe.setAttribute("style", `width: ${width}${px(effective.width)}; height: ${height}${px(effective.height)};`);
3806
+ setStyle(probe, `width: ${width}${px(effective.width)}; height: ${height}${px(effective.height)};`);
3729
3807
  container.appendChild(probe);
3730
3808
  const measured = {
3731
3809
  width: probe.clientWidth,
@@ -3874,7 +3952,7 @@ function axisNodes(axis, frame) {
3874
3952
  const options = frame.options ?? (axis.dimension === "x" ? frame.axisX : frame.axisY);
3875
3953
  const place = frame.place;
3876
3954
  const projected = axis.ticks.map((tick, i) => axis.project(tick, i));
3877
- const labelValues = axis.ticks.map(options.labelInterpolationFnc);
3955
+ const labelValues = axis.ticks.map(options.labelTransform);
3878
3956
  const gridOffset = axis.dimension === "x" ? area.y2 : area.x1;
3879
3957
  const gridLength = axis.dimension === "x" ? area.height : area.width;
3880
3958
  const grids = [];
@@ -3969,6 +4047,12 @@ function gridBackground(area, className) {
3969
4047
  }, className, { role: "gridBackground" });
3970
4048
  }
3971
4049
  //#endregion
4050
+ //#region src/layers/label.ts
4051
+ /** The label function to use; `labelTransform` wins when both are set. */
4052
+ function labelTransformOf(options) {
4053
+ return options.labelTransform ?? options.labelInterpolationFnc ?? ((value) => value);
4054
+ }
4055
+ //#endregion
3972
4056
  //#region src/scales/svgLabels.ts
3973
4057
  /**
3974
4058
  * SVG text labels: the label text wrapped into `tspan` lines and aligned
@@ -4067,7 +4151,7 @@ function svgLabels(measureText, kind, override = {}) {
4067
4151
  * container('(width < 400px)'),
4068
4152
  * {
4069
4153
  * showPoint: false,
4070
- * axisX: { labelInterpolationFnc: value => String(value)[0] }
4154
+ * axisX: { labelTransform: value => String(value)[0] }
4071
4155
  * }
4072
4156
  * ]
4073
4157
  * ]
@@ -4209,13 +4293,18 @@ function applyResponsive(base, entries, matching, merge) {
4209
4293
  * option merge, window responsive options, the preset state that plugins
4210
4294
  * (compat) read, and the grid and label layers.
4211
4295
  */
4296
+ /** An axis's options with its label function resolved from either name. */
4297
+ const drawn = (axis) => ({
4298
+ ...axis,
4299
+ labelTransform: labelTransformOf(axis)
4300
+ });
4212
4301
  function frame(options, context, svgLabel) {
4213
4302
  return {
4214
4303
  svgLabel,
4215
4304
  area: context.area,
4216
4305
  padding: context.padding,
4217
- axisX: options.axisX,
4218
- axisY: options.axisY,
4306
+ axisX: drawn(options.axisX),
4307
+ axisY: drawn(options.axisY),
4219
4308
  classNames: options.classNames
4220
4309
  };
4221
4310
  }
@@ -4243,10 +4332,10 @@ function labelLayer(options, order = ["x", "y"]) {
4243
4332
  type: "labels",
4244
4333
  coordinates: ["cartesian"],
4245
4334
  compose(context) {
4246
- const svgLabel = context.labels === "svg" ? svgLabels(context.measureText, options.labelKind ?? "line") : void 0;
4247
4335
  return [group(options.classNames.labelGroup, order.flatMap((dimension) => {
4248
4336
  const axis = context.axes[dimension];
4249
- return axis ? axisNodes(axis, frame(options, context, svgLabel)).labels : [];
4337
+ if (!axis) return [];
4338
+ return axisNodes(axis, frame(options, context, context.labels === "svg" ? svgLabels(context.measureText, options.labelKind ?? "line", options[dimension === "x" ? "axisX" : "axisY"].labelAlign) : void 0)).labels;
4250
4339
  }))];
4251
4340
  }
4252
4341
  };
@@ -4446,6 +4535,7 @@ const lineSpec = {
4446
4535
  config(data, preset) {
4447
4536
  const options = preset;
4448
4537
  const axisOptions = options;
4538
+ const { below = [], above = [] } = options.layers ?? {};
4449
4539
  return {
4450
4540
  data,
4451
4541
  axes: presetAxes(axesConfig$1(options)),
@@ -4457,6 +4547,7 @@ const lineSpec = {
4457
4547
  labels: options.labels,
4458
4548
  layers: [
4459
4549
  gridLayer(axisOptions),
4550
+ ...below,
4460
4551
  seriesGroup(line({
4461
4552
  lineSmooth: options.lineSmooth,
4462
4553
  showPoint: options.showPoint,
@@ -4467,6 +4558,7 @@ const lineSpec = {
4467
4558
  reverseData: options.reverseData,
4468
4559
  classNames: options.classNames
4469
4560
  })),
4561
+ ...above,
4470
4562
  labelLayer(axisOptions)
4471
4563
  ]
4472
4564
  };
@@ -4777,6 +4869,7 @@ function bar(options = {}) {
4777
4869
  };
4778
4870
  const horizontal = Boolean(options.horizontalBars);
4779
4871
  const seriesBarDistance = options.seriesBarDistance ?? 15;
4872
+ const offset = options.offset ?? 0;
4780
4873
  const stackMode = options.stackMode ?? "accumulate";
4781
4874
  const stacks = options.stackBars && stackMode !== "overlap" && stackMode !== true;
4782
4875
  return {
@@ -4837,6 +4930,7 @@ function bar(options = {}) {
4837
4930
  if (!labelAxis.stretch) projected[labelPos] += periodHalfLength * (horizontal ? -1 : 1);
4838
4931
  projected[labelPos] += options.stackBars || options.distributeSeries ? 0 : biPol * seriesBarDistance * (horizontal ? -1 : 1);
4839
4932
  }
4933
+ projected[labelPos] += offset;
4840
4934
  if (isAccumulateRelative) stack = Number(horizontal ? valueX : valueY) < 0 ? negStack : posStack;
4841
4935
  const previousStack = stack[valueIndex] || zeroPoint;
4842
4936
  stack[valueIndex] = previousStack - (zeroPoint - projected[counterPos]);
@@ -5037,6 +5131,7 @@ const barSpec = {
5037
5131
  ...seriesGroup(layer),
5038
5132
  extent: extentOf.extent
5039
5133
  };
5134
+ const { below = [], above = [] } = options.layers ?? {};
5040
5135
  return {
5041
5136
  data,
5042
5137
  axes: presetAxes(axesConfig(options)),
@@ -5048,7 +5143,9 @@ const barSpec = {
5048
5143
  labels: options.labels,
5049
5144
  layers: [
5050
5145
  gridLayer(axisOptions, order),
5146
+ ...below,
5051
5147
  bars,
5148
+ ...above,
5052
5149
  labelLayer({
5053
5150
  ...axisOptions,
5054
5151
  labelKind: options.horizontalBars ? "horizontalBar" : "bar"
@@ -5136,12 +5233,40 @@ function cssWidth(measureStyle, sliceClass) {
5136
5233
  const width = measureStyle?.("stroke-width", { classes: sliceClass.split(" ") });
5137
5234
  return width && /^[\d.]+px$/.test(width) ? parseFloat(width) : void 0;
5138
5235
  }
5139
- const CLASS_NAMES$1 = {
5236
+ const CLASS_NAMES$4 = {
5140
5237
  series: "ct-series",
5141
5238
  slicePie: "ct-slice-pie",
5142
5239
  sliceDonut: "ct-slice-donut",
5143
5240
  label: "ct-label"
5144
5241
  };
5242
+ /** The line height of a pie label with line breaks, in `em`. */
5243
+ const LINE_HEIGHT = 1.2;
5244
+ /**
5245
+ * A pie label's `text` at its anchor. One line is placed with `dx`/`dy`,
5246
+ * as in 1.x. A label with line breaks gets one `tspan` per line, one line
5247
+ * height apart, the block centered on the anchor (see #546).
5248
+ */
5249
+ function labelText(text, position, anchor, className, ctx) {
5250
+ const lines = text.split("\n");
5251
+ const meta = {
5252
+ role: "label",
5253
+ ctx
5254
+ };
5255
+ if (lines.length === 1) return node("text", {
5256
+ dx: position.x,
5257
+ dy: position.y,
5258
+ "text-anchor": anchor
5259
+ }, className, meta).text(text);
5260
+ const shift = Math.round(-(lines.length - 1) * LINE_HEIGHT * 50) / 100;
5261
+ return withFields(node("text", {
5262
+ x: position.x,
5263
+ y: position.y,
5264
+ "text-anchor": anchor
5265
+ }, className, meta), { children: lines.map((line, index) => node("tspan", {
5266
+ x: position.x,
5267
+ dy: `${index ? LINE_HEIGHT : shift}em`
5268
+ }).text(line)) });
5269
+ }
5145
5270
  /**
5146
5271
  * Creates a layer that draws a flat series as pie or donut slices with
5147
5272
  * labels, in the markup of the 1.x pie chart (a `g.ct-series` per slice,
@@ -5167,10 +5292,10 @@ const CLASS_NAMES$1 = {
5167
5292
  */
5168
5293
  function pie(options = {}) {
5169
5294
  const classNames = {
5170
- ...CLASS_NAMES$1,
5295
+ ...CLASS_NAMES$4,
5171
5296
  ...options.classNames
5172
5297
  };
5173
- const labelInterpolationFnc = options.labelInterpolationFnc ?? ((value) => value);
5298
+ const transform = labelTransformOf(options);
5174
5299
  return {
5175
5300
  type: "pie",
5176
5301
  chartType: options.donut ? "donut" : "pie",
@@ -5266,7 +5391,7 @@ function pie(options = {}) {
5266
5391
  y: center.y
5267
5392
  } : polarToCartesian(center.x, center.y, labelRadius, startAngle + (endAngle - startAngle) / 2);
5268
5393
  const rawValue = labels && !isFalseyButZero(labels[index]) ? labels[index] : values[index];
5269
- const interpolated = labelInterpolationFnc(rawValue, index);
5394
+ const interpolated = transform(rawValue, index);
5270
5395
  if (interpolated || interpolated === 0) {
5271
5396
  const text = String(interpolated);
5272
5397
  if (options.preventOverlappingLabelOffset) {
@@ -5274,24 +5399,17 @@ function pie(options = {}) {
5274
5399
  labelPositions.forEach((item) => moveLabel(labelPosition, item, options.preventOverlappingLabelOffset, length));
5275
5400
  labelPositions.push(labelPosition);
5276
5401
  }
5277
- labelNodes.push(node("text", {
5278
- dx: labelPosition.x,
5279
- dy: labelPosition.y,
5280
- "text-anchor": determineAnchorPosition(center, labelPosition, options.labelDirection ?? "neutral")
5281
- }, classNames.label, {
5282
- role: "label",
5283
- ctx: {
5284
- kind: "pie",
5285
- index,
5286
- seriesIndex: index,
5287
- valueIndex: 0,
5288
- text,
5289
- series,
5290
- meta: seriesMeta,
5291
- x: labelPosition.x,
5292
- y: labelPosition.y
5293
- }
5294
- }).text(text));
5402
+ labelNodes.push(labelText(text, labelPosition, determineAnchorPosition(center, labelPosition, options.labelDirection ?? "neutral"), classNames.label, {
5403
+ kind: "pie",
5404
+ index,
5405
+ seriesIndex: index,
5406
+ valueIndex: 0,
5407
+ text,
5408
+ series,
5409
+ meta: seriesMeta,
5410
+ x: labelPosition.x,
5411
+ y: labelPosition.y
5412
+ }));
5295
5413
  }
5296
5414
  }
5297
5415
  startAngle = endAngle;
@@ -5338,6 +5456,7 @@ const pieSpec = {
5338
5456
  defaults: pieDefaults,
5339
5457
  config(data, preset) {
5340
5458
  const options = preset;
5459
+ const { below = [], above = [] } = preset.layers ?? {};
5341
5460
  return {
5342
5461
  data,
5343
5462
  coordinates: "polar",
@@ -5346,20 +5465,25 @@ const pieSpec = {
5346
5465
  height: options.height,
5347
5466
  viewBox: options.viewBox,
5348
5467
  className: options.donut ? options.classNames.chartDonut : options.classNames.chartPie,
5349
- layers: [pie({
5350
- startAngle: options.startAngle,
5351
- total: options.total,
5352
- donut: options.donut,
5353
- donutWidth: options.donutWidth,
5354
- showLabel: options.showLabel,
5355
- labelOffset: options.labelOffset,
5356
- labelPosition: options.labelPosition,
5357
- labelInterpolationFnc: options.labelInterpolationFnc,
5358
- labelDirection: options.labelDirection,
5359
- ignoreEmptyValues: options.ignoreEmptyValues,
5360
- preventOverlappingLabelOffset: options.preventOverlappingLabelOffset,
5361
- classNames: options.classNames
5362
- })]
5468
+ layers: [
5469
+ ...below,
5470
+ pie({
5471
+ startAngle: options.startAngle,
5472
+ total: options.total,
5473
+ donut: options.donut,
5474
+ donutWidth: options.donutWidth,
5475
+ showLabel: options.showLabel,
5476
+ labelOffset: options.labelOffset,
5477
+ labelPosition: options.labelPosition,
5478
+ labelTransform: options.labelTransform,
5479
+ labelInterpolationFnc: options.labelInterpolationFnc,
5480
+ labelDirection: options.labelDirection,
5481
+ ignoreEmptyValues: options.ignoreEmptyValues,
5482
+ preventOverlappingLabelOffset: options.preventOverlappingLabelOffset,
5483
+ classNames: options.classNames
5484
+ }),
5485
+ ...above
5486
+ ]
5363
5487
  };
5364
5488
  }
5365
5489
  };
@@ -5715,16 +5839,22 @@ const seriesColors = [
5715
5839
  //#endregion
5716
5840
  //#region src/render/string.ts
5717
5841
  const escape = (value) => String(value).replace(/[&<>"']/g, (char) => escapingMap[char]);
5842
+ const nameStart = "A-Z_:a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
5843
+ const xmlName = new RegExp(`^[${nameStart}][${nameStart}\\-.0-9\\u00B7\\u0300-\\u036F\\u203F\\u2040]*$`);
5844
+ function checked(name, kind) {
5845
+ if (!xmlName.test(name)) throw new Error(`Chartist: "${name}" is not a valid ${kind} name`);
5846
+ return name;
5847
+ }
5718
5848
  function attributes(current, position) {
5719
5849
  const parts = [];
5720
5850
  if (position === "root") parts.push(`xmlns="${namespaces.svg}"`, `xmlns:ct="${namespaces.ct}"`);
5721
5851
  else if (position === "html-root") parts.push(`xmlns="${namespaces.xhtml}"`);
5722
5852
  if (current.classes.length) parts.push(`class="${escape(current.classes.join(" "))}"`);
5723
- for (const [name, value] of Object.entries(current.attrs)) if (name !== "xmlns") parts.push(`${name}="${escape(value)}"`);
5853
+ for (const [name, value] of Object.entries(current.attrs)) if (name !== "xmlns") parts.push(`${checked(name, "attribute")}="${escape(value)}"`);
5724
5854
  return parts.length ? ` ${parts.join(" ")}` : "";
5725
5855
  }
5726
5856
  function serialize$1(current, position) {
5727
- const open = `<${current.name}${attributes(current, position)}`;
5857
+ const open = `<${checked(current.name, "element")}${attributes(current, position)}`;
5728
5858
  const childPosition = current.name === "foreignObject" ? "html-root" : "other";
5729
5859
  const content = (current.content === void 0 ? "" : escape(current.content)) + current.children.map((child) => serialize$1(child, childPosition)).join("");
5730
5860
  return content ? `${open}>${content}</${current.name}>` : `${open}/>`;
@@ -5855,6 +5985,550 @@ function toScene(config, size) {
5855
5985
  }).scene;
5856
5986
  }
5857
5987
  //#endregion
5988
+ //#region src/layers/band.ts
5989
+ const CLASS_NAMES$3 = {
5990
+ band: "ct-band",
5991
+ area: "ct-band-area",
5992
+ inBand: "ct-in-band",
5993
+ outBand: "ct-out-band"
5994
+ };
5995
+ /** The series an edge follows, if it's a series. */
5996
+ function selectorOf(edge) {
5997
+ return typeof edge === "number" || isValues(edge) ? void 0 : typeof edge === "string" ? edge : edge.series;
5998
+ }
5999
+ function isValues(edge) {
6000
+ return Array.isArray(edge);
6001
+ }
6002
+ /** An edge's own values, normalized like a series' values. */
6003
+ function normalizedValues(values) {
6004
+ return values.map((value) => isNumeric(value) ? { y: Number(value) } : value && typeof value === "object" ? value : void 0);
6005
+ }
6006
+ /** The value of a normalized value along Y, or `undefined` for a hole. */
6007
+ function yOf(value) {
6008
+ const y = value !== null && typeof value === "object" ? value.y : value;
6009
+ return isNumeric(y) ? Number(y) : void 0;
6010
+ }
6011
+ function xOf(value) {
6012
+ return value !== null && typeof value === "object" ? value.x : void 0;
6013
+ }
6014
+ /**
6015
+ * The position of a series in the layer's data: the layer gets only the
6016
+ * series it selects, with their chart-wide indexes in `seriesIndexes`.
6017
+ */
6018
+ function localIndex(selector, context) {
6019
+ const { data, seriesIndexes } = context;
6020
+ if (typeof selector === "string") return data.series.findIndex((series) => series.name === selector);
6021
+ return seriesIndexes ? seriesIndexes.indexOf(selector) : selector;
6022
+ }
6023
+ function chartIndex(selector, context) {
6024
+ const index = localIndex(selector, context);
6025
+ return context.seriesIndexes?.[index] ?? index;
6026
+ }
6027
+ /**
6028
+ * Works out the band's edges from the data in the context.
6029
+ * @param constant Changes a constant edge before it's used, e.g. to keep it
6030
+ * within the axis range for drawing.
6031
+ */
6032
+ function bandEdges(options, context, constant = (value) => value) {
6033
+ const all = context.normalized?.series ?? [];
6034
+ const seriesOf = (selector) => all[localIndex(selector, context)] ?? [];
6035
+ if (options.series) {
6036
+ const group = options.series.map(seriesOf);
6037
+ return {
6038
+ constant: false,
6039
+ length: Math.max(0, ...group.map((values) => values.length)),
6040
+ at(valueIndex) {
6041
+ const defined = group.map((values) => values[valueIndex]).filter((value) => yOf(value) !== void 0);
6042
+ if (defined.length === 0) return;
6043
+ const ys = defined.map((value) => yOf(value));
6044
+ return {
6045
+ x: xOf(defined[0]),
6046
+ low: Math.min(...ys),
6047
+ high: Math.max(...ys)
6048
+ };
6049
+ }
6050
+ };
6051
+ }
6052
+ const edge = (option) => {
6053
+ if (option !== void 0 && isValues(option)) return { values: normalizedValues(option) };
6054
+ const selector = option === void 0 ? void 0 : selectorOf(option);
6055
+ return selector === void 0 ? { value: constant(option) } : { values: seriesOf(selector) };
6056
+ };
6057
+ const low = edge(options.low);
6058
+ const high = edge(options.high);
6059
+ const lengths = [low, high].flatMap((e) => "values" in e ? [e.values.length] : []);
6060
+ return {
6061
+ constant: lengths.length === 0,
6062
+ length: Math.max(0, ...lengths),
6063
+ at(valueIndex) {
6064
+ const lowValue = "values" in low ? low.values[valueIndex] : void 0;
6065
+ const highValue = "values" in high ? high.values[valueIndex] : void 0;
6066
+ const lowY = "value" in low ? low.value : yOf(lowValue);
6067
+ const highY = "value" in high ? high.value : yOf(highValue);
6068
+ if (lowY === void 0 || highY === void 0) return;
6069
+ return {
6070
+ x: xOf(lowValue) ?? xOf(highValue),
6071
+ low: lowY,
6072
+ high: highY
6073
+ };
6074
+ }
6075
+ };
6076
+ }
6077
+ /** The value a mark compares with the band: `y`, else `x`. */
6078
+ function markValue(value) {
6079
+ if (value !== null && typeof value === "object") {
6080
+ const { x, y } = value;
6081
+ return isNumeric(y) ? Number(y) : isNumeric(x) ? Number(x) : void 0;
6082
+ }
6083
+ return isNumeric(value) ? Number(value) : void 0;
6084
+ }
6085
+ function edgeContext(option, context) {
6086
+ if (option === void 0) return;
6087
+ if (isValues(option)) return { values: normalizedValues(option).map(yOf) };
6088
+ const selector = selectorOf(option);
6089
+ return selector === void 0 ? { value: option } : { seriesIndex: chartIndex(selector, context) };
6090
+ }
6091
+ /**
6092
+ * Creates a layer that fills the area between a low and a high edge
6093
+ * (`g.ct-band` with a `path.ct-band-area` per piece; the band breaks at
6094
+ * holes). Each edge is a constant value, values of its own (one per
6095
+ * label) or a series, or `series` names a group and the band covers their
6096
+ * minimum to their maximum at each value index. Constants and own values
6097
+ * widen the Y axis to include them. List the band
6098
+ * before the series layers to draw it behind them. It needs an X and a Y
6099
+ * axis, from {@link cartesianAxes}.
6100
+ *
6101
+ * @example
6102
+ * ```ts
6103
+ * import {
6104
+ * autoScaleAxis,
6105
+ * axisGrid,
6106
+ * axisLabels,
6107
+ * band,
6108
+ * cartesianAxes,
6109
+ * chart,
6110
+ * line,
6111
+ * stepAxis
6112
+ * } from 'chartist';
6113
+ *
6114
+ * const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
6115
+ *
6116
+ * chart('#chart', {
6117
+ * data: {
6118
+ * labels: days,
6119
+ * series: [{ name: 'visits', data: [12, 9, 7, 8, 5] }]
6120
+ * },
6121
+ * axes: cartesianAxes([
6122
+ * { id: 'day', side: 'bottom', type: stepAxis(), ticks: days },
6123
+ * { id: 'count', side: 'left', type: autoScaleAxis(), low: 0 }
6124
+ * ]),
6125
+ * layers: [
6126
+ * axisGrid(),
6127
+ * band({ low: 6, high: 10, mark: true }),
6128
+ * line(),
6129
+ * axisLabels()
6130
+ * ]
6131
+ * });
6132
+ * ```
6133
+ * @experimental
6134
+ */
6135
+ function band(options) {
6136
+ const hasEdges = options.low !== void 0 || options.high !== void 0;
6137
+ if (options.series && hasEdges) throw new Error("Layer \"band\": series can't be combined with low and high");
6138
+ if (!options.series && (options.low === void 0 || options.high === void 0)) throw new Error("Layer \"band\": the band needs low and high, or series");
6139
+ const classNames = {
6140
+ ...CLASS_NAMES$3,
6141
+ ...options.classNames
6142
+ };
6143
+ const edgeSelectors = options.series ? void 0 : [options.low, options.high].flatMap((edge) => {
6144
+ const selector = edge === void 0 ? void 0 : selectorOf(edge);
6145
+ return selector === void 0 ? [] : [selector];
6146
+ });
6147
+ const ownValues = options.series ? [] : [options.low, options.high].flatMap((edge) => typeof edge === "number" ? [edge] : edge !== void 0 && isValues(edge) ? normalizedValues(edge).flatMap((value) => {
6148
+ const y = yOf(value);
6149
+ return y === void 0 ? [] : [y];
6150
+ }) : []);
6151
+ const layer = {
6152
+ type: "band",
6153
+ coordinates: ["cartesian"],
6154
+ axes: {
6155
+ x: options.x,
6156
+ y: options.y
6157
+ },
6158
+ series: options.series ?? edgeSelectors,
6159
+ extent({ axes }) {
6160
+ return axes.y !== void 0 && ownValues.length > 0 ? { [axes.y]: {
6161
+ low: Math.min(...ownValues),
6162
+ high: Math.max(...ownValues)
6163
+ } } : {};
6164
+ },
6165
+ compose(context) {
6166
+ const { area, axes } = context;
6167
+ const { x: axisX, y: axisY } = axes;
6168
+ if (!axisX || !axisY || !context.normalized) throw new Error("Layer \"band\" needs an x and a y axis");
6169
+ const range = axisY.range;
6170
+ const clamp = (value) => range ? Math.min(Math.max(value, range.min), range.max) : value;
6171
+ const edges = bandEdges(options, context, clamp);
6172
+ let pieces;
6173
+ if (edges.constant) {
6174
+ const { low, high } = edges.at(0);
6175
+ const lowY = area.y1 - axisY.project(low);
6176
+ const highY = area.y1 - axisY.project(high);
6177
+ pieces = [new SvgPath(true).move(area.x1, highY).line(area.x2, highY).line(area.x2, lowY).line(area.x1, lowY)];
6178
+ } else {
6179
+ const upper = [];
6180
+ const lower = [];
6181
+ for (let valueIndex = 0; valueIndex < edges.length; valueIndex++) {
6182
+ const at = edges.at(valueIndex);
6183
+ upper.push(at && {
6184
+ x: at.x,
6185
+ y: at.high
6186
+ });
6187
+ lower.push(at && {
6188
+ x: at.x,
6189
+ y: at.low
6190
+ });
6191
+ }
6192
+ const shift = centerShift(axisX, options.align);
6193
+ const interpolation = interpolationOf(options.lineSmooth ?? true);
6194
+ const pathOf = (values) => seriesPath(values, {
6195
+ x: axisX,
6196
+ y: axisY
6197
+ }, area, shift, interpolation).splitByCommand("M").filter((piece) => piece.pathElements.length > 1);
6198
+ const lowerPieces = pathOf(lower);
6199
+ pieces = pathOf(upper).flatMap((upperPiece, index) => {
6200
+ const lowerPiece = lowerPieces[index];
6201
+ if (!lowerPiece || lowerPiece.pathElements.length !== upperPiece.pathElements.length) return [];
6202
+ const back = reversePath(lowerPiece);
6203
+ const start = back.pathElements[0];
6204
+ back.position(0).remove(1).line(start.x, start.y, false, start.data);
6205
+ return [SvgPath.join([upperPiece, back], true)];
6206
+ });
6207
+ }
6208
+ const edgesContext = options.series ? { series: options.series.map((s) => chartIndex(s, context)) } : {
6209
+ low: edgeContext(options.low, context),
6210
+ high: edgeContext(options.high, context)
6211
+ };
6212
+ const children = pieces.map((path) => node("path", { d: path.stringify() }, classNames.area, {
6213
+ role: "band",
6214
+ ctx: {
6215
+ ...edgesContext,
6216
+ path: path.clone()
6217
+ }
6218
+ }));
6219
+ return [withFields(node("g", {}, options.className ? `${classNames.band} ${options.className}` : classNames.band), { children })];
6220
+ }
6221
+ };
6222
+ if (!options.mark) return layer;
6223
+ return {
6224
+ ...layer,
6225
+ refine(current, context) {
6226
+ if (current.role !== "point" && current.role !== "bar") return;
6227
+ const value = markValue(current.ctx.value);
6228
+ const at = bandEdges(options, context).at(current.ctx.valueIndex);
6229
+ if (value === void 0 || !at) return;
6230
+ const inBand = value >= Math.min(at.low, at.high) && value <= Math.max(at.low, at.high);
6231
+ return current.addClass(inBand ? classNames.inBand : classNames.outBand);
6232
+ }
6233
+ };
6234
+ }
6235
+ //#endregion
6236
+ //#region src/layers/targetLine.ts
6237
+ /**
6238
+ * Target line layer: a line across the chart area at a constant value on
6239
+ * one axis, such as a goal, a limit or an average, with an optional label.
6240
+ */
6241
+ const CLASS_NAMES$2 = {
6242
+ line: "ct-target-line",
6243
+ label: "ct-target-label"
6244
+ };
6245
+ /** The distance in px between a label and its line or the area's edge. */
6246
+ const LABEL_INSET = 4;
6247
+ /**
6248
+ * Creates a layer that draws a line across the chart area at a constant
6249
+ * value on one axis, such as a goal or a limit (`line.ct-target-line`).
6250
+ * On a Y axis the line is horizontal, on an X axis vertical. It can have
6251
+ * a label, drawn as SVG text (`text.ct-target-label`) next to it inside
6252
+ * the chart area, and a class, e.g. `ct-series-b` for a series' color.
6253
+ * The axis widens to include the value, unless its own `low` or `high`
6254
+ * leave it out; a line outside the axis isn't drawn. For several lines,
6255
+ * add a layer per line.
6256
+ *
6257
+ * @example
6258
+ * ```ts
6259
+ * import {
6260
+ * autoScaleAxis,
6261
+ * axisGrid,
6262
+ * axisLabels,
6263
+ * cartesianAxes,
6264
+ * chart,
6265
+ * line,
6266
+ * stepAxis,
6267
+ * targetLine
6268
+ * } from 'chartist';
6269
+ *
6270
+ * const months = ['Jan', 'Feb', 'Mar', 'Apr'];
6271
+ *
6272
+ * chart('#chart', {
6273
+ * data: { labels: months, series: [[620, 810, 740, 930]] },
6274
+ * axes: cartesianAxes([
6275
+ * { id: 'month', side: 'bottom', type: stepAxis(), ticks: months },
6276
+ * { id: 'sales', side: 'left', type: autoScaleAxis(), low: 0 }
6277
+ * ]),
6278
+ * layers: [
6279
+ * axisGrid(),
6280
+ * line(),
6281
+ * targetLine({ value: 700, label: 'Min' }),
6282
+ * targetLine({ value: 1000, label: 'Goal', className: 'ct-series-b' }),
6283
+ * axisLabels()
6284
+ * ]
6285
+ * });
6286
+ * ```
6287
+ * @experimental
6288
+ */
6289
+ function targetLine(options) {
6290
+ const { value, label, className } = options;
6291
+ if (!Number.isFinite(value)) throw new Error("Layer \"targetLine\" needs a value");
6292
+ const classNames = {
6293
+ ...CLASS_NAMES$2,
6294
+ ...options.classNames
6295
+ };
6296
+ const atStart = options.labelPosition === "start";
6297
+ return {
6298
+ type: "targetLine",
6299
+ coordinates: ["cartesian"],
6300
+ series: [],
6301
+ extent({ axes }) {
6302
+ const id = options.axis ?? axes.y;
6303
+ return id === void 0 ? {} : { [id]: {
6304
+ low: value,
6305
+ high: value
6306
+ } };
6307
+ },
6308
+ compose({ area, axes, measureText }) {
6309
+ const all = axes.all ?? [];
6310
+ const axis = options.axis === void 0 ? all.find((candidate) => candidate.dimension === "y") : all.find((candidate) => candidate.id === options.axis);
6311
+ if (!axis) throw new Error(options.axis === void 0 ? "Layer \"targetLine\" needs a y axis" : `Chartist: unknown axis "${options.axis}" in layer "targetLine"`);
6312
+ const horizontal = axis.dimension === "y";
6313
+ const relative = axis.project(value, value);
6314
+ if (!Number.isFinite(relative) || relative < -1e-9 || relative > axis.length + 1e-9) return [];
6315
+ const ends = horizontal ? {
6316
+ x1: area.x1,
6317
+ y1: area.y1 - relative,
6318
+ x2: area.x2,
6319
+ y2: area.y1 - relative
6320
+ } : {
6321
+ x1: area.x1 + relative,
6322
+ y1: area.y2,
6323
+ x2: area.x1 + relative,
6324
+ y2: area.y1
6325
+ };
6326
+ const extra = className ? ` ${className}` : "";
6327
+ const lineNode = node("line", ends, `${classNames.line}${extra}`, {
6328
+ role: "targetLine",
6329
+ ctx: {
6330
+ axis,
6331
+ value,
6332
+ ...label !== void 0 && { label },
6333
+ ...ends
6334
+ }
6335
+ });
6336
+ if (label === void 0 || label === "") return [lineNode];
6337
+ const labelClasses = `${classNames.label}${extra}`;
6338
+ let position;
6339
+ if (horizontal) position = {
6340
+ x: atStart ? area.x1 + LABEL_INSET : area.x2 - LABEL_INSET,
6341
+ y: ends.y1 - LABEL_INSET,
6342
+ anchor: atStart ? "start" : "end"
6343
+ };
6344
+ else {
6345
+ const fontSize = measureText(label, { classes: labelClasses.split(" ") }).fontSize ?? 12;
6346
+ position = {
6347
+ x: ends.x1 + LABEL_INSET,
6348
+ y: atStart ? area.y1 - LABEL_INSET : area.y2 + LABEL_INSET + textAscent * fontSize,
6349
+ anchor: "start"
6350
+ };
6351
+ }
6352
+ return [lineNode, node("text", {
6353
+ x: position.x,
6354
+ y: position.y,
6355
+ "text-anchor": position.anchor
6356
+ }, labelClasses, {
6357
+ role: "targetLabel",
6358
+ ctx: {
6359
+ axis,
6360
+ value,
6361
+ text: label,
6362
+ x: position.x,
6363
+ y: position.y
6364
+ }
6365
+ }).text(label)];
6366
+ }
6367
+ };
6368
+ }
6369
+ //#endregion
6370
+ //#region src/layers/trendLine.ts
6371
+ /**
6372
+ * Trend line layer: a least-squares straight line through each series'
6373
+ * values, drawn in the series' color.
6374
+ */
6375
+ const CLASS_NAMES$1 = {
6376
+ series: "ct-series",
6377
+ line: "ct-trend-line"
6378
+ };
6379
+ /**
6380
+ * Fits a straight line through a series' values by least squares: X is
6381
+ * the value's index (`byIndex`, for step axes) or its `x`. Holes and
6382
+ * values without a number for X are skipped. Fewer than two values, or
6383
+ * values that all share one X, fit no line.
6384
+ */
6385
+ function fit(values, byIndex) {
6386
+ const xs = [];
6387
+ const ys = [];
6388
+ values.forEach((value, index) => {
6389
+ const y = isDataHoleValue(value) ? void 0 : getMultiValue(value, "y");
6390
+ const x = byIndex ? index : getMultiValue(value, "x");
6391
+ if (typeof x === "number" && Number.isFinite(x) && Number.isFinite(y)) {
6392
+ xs.push(x);
6393
+ ys.push(y);
6394
+ }
6395
+ });
6396
+ if (xs.length < 2) return;
6397
+ const mean = (list) => list.reduce((sum, value) => sum + value, 0) / list.length;
6398
+ const meanX = mean(xs);
6399
+ const meanY = mean(ys);
6400
+ let covariance = 0;
6401
+ let variance = 0;
6402
+ xs.forEach((x, i) => {
6403
+ covariance += (x - meanX) * (ys[i] - meanY);
6404
+ variance += (x - meanX) ** 2;
6405
+ });
6406
+ if (variance === 0) return;
6407
+ const slope = covariance / variance;
6408
+ return {
6409
+ slope,
6410
+ intercept: meanY - slope * meanX,
6411
+ x1: Math.min(...xs),
6412
+ x2: Math.max(...xs)
6413
+ };
6414
+ }
6415
+ /** True when every value that isn't a hole has a number for X. */
6416
+ function hasX(values) {
6417
+ const present = values.filter((value) => !isDataHoleValue(value));
6418
+ return present.length > 0 && present.every((value) => typeof getMultiValue(value, "x") === "number");
6419
+ }
6420
+ /**
6421
+ * Creates a layer that draws a trend line for each series: the straight
6422
+ * line that fits its values best (least squares), from its smallest to its
6423
+ * largest X, as a `line.ct-trend-line` in a series group
6424
+ * (`g.ct-series.ct-series-a`), so it takes the series' color. On a step
6425
+ * axis X is the value's index, on a scale axis the value's `x`. Holes are
6426
+ * skipped, and a series with fewer than two values gets no line. The Y
6427
+ * axis widens to include the line's ends. Add it next to the {@link line}
6428
+ * or {@link bar} layer that draws the values.
6429
+ *
6430
+ * @example
6431
+ * ```ts
6432
+ * import {
6433
+ * autoScaleAxis,
6434
+ * axisGrid,
6435
+ * axisLabels,
6436
+ * cartesianAxes,
6437
+ * chart,
6438
+ * line,
6439
+ * stepAxis,
6440
+ * trendLine
6441
+ * } from 'chartist';
6442
+ *
6443
+ * const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
6444
+ *
6445
+ * chart('#chart', {
6446
+ * data: {
6447
+ * labels: months,
6448
+ * series: [{ name: 'sales', data: [620, 810, 740, 930, 880, 1020] }]
6449
+ * },
6450
+ * axes: cartesianAxes([
6451
+ * { id: 'month', side: 'bottom', type: stepAxis(), ticks: months },
6452
+ * { id: 'eur', side: 'left', type: autoScaleAxis() }
6453
+ * ]),
6454
+ * layers: [axisGrid(), line(), trendLine(), axisLabels()]
6455
+ * });
6456
+ * ```
6457
+ * @experimental
6458
+ */
6459
+ function trendLine(options = {}) {
6460
+ const classNames = {
6461
+ ...CLASS_NAMES$1,
6462
+ ...options.classNames
6463
+ };
6464
+ return {
6465
+ type: "trendLine",
6466
+ coordinates: ["cartesian"],
6467
+ axes: {
6468
+ x: options.x,
6469
+ y: options.y
6470
+ },
6471
+ series: options.series,
6472
+ extent({ normalized, axes }) {
6473
+ if (axes.y === void 0) return {};
6474
+ const ends = normalized.series.flatMap((values) => {
6475
+ const fitted = fit(values, !hasX(values));
6476
+ return fitted ? [fitted.slope * fitted.x1 + fitted.intercept, fitted.slope * fitted.x2 + fitted.intercept] : [];
6477
+ });
6478
+ return ends.length > 0 ? { [axes.y]: {
6479
+ low: Math.min(...ends),
6480
+ high: Math.max(...ends)
6481
+ } } : {};
6482
+ },
6483
+ compose({ data, area, axes, normalized, seriesIndexes }) {
6484
+ const { x: axisX, y: axisY } = axes;
6485
+ if (!axisX || !axisY || !normalized) throw new Error("Layer \"trendLine\" needs an x and a y axis");
6486
+ const byIndex = axisX.kind === "step";
6487
+ const shift = centerShift(axisX, options.align);
6488
+ return data.series.flatMap((series, localIndex) => {
6489
+ const fitted = fit(normalized.series[localIndex], byIndex);
6490
+ if (!fitted) return [];
6491
+ const seriesIndex = seriesIndexes?.[localIndex] ?? localIndex;
6492
+ const { slope, intercept } = fitted;
6493
+ const at = (x) => ({
6494
+ x: area.x1 + shift + axisX.project(x, x),
6495
+ y: area.y1 - axisY.project(slope * x + intercept)
6496
+ });
6497
+ const start = at(fitted.x1);
6498
+ const end = at(fitted.x2);
6499
+ const ends = {
6500
+ x1: start.x,
6501
+ y1: start.y,
6502
+ x2: end.x,
6503
+ y2: end.y
6504
+ };
6505
+ const seriesContext = {
6506
+ series: {
6507
+ name: series.name,
6508
+ className: series.className,
6509
+ meta: series.meta,
6510
+ data: series.values
6511
+ },
6512
+ seriesIndex,
6513
+ meta: series.meta
6514
+ };
6515
+ return [withFields(node("g", {}, `${classNames.series} ${series.className || `${classNames.series}-${alphaNumerate(seriesIndex)}`}`, {
6516
+ role: "series",
6517
+ ctx: seriesContext
6518
+ }), { children: [node("line", ends, classNames.line, {
6519
+ role: "trendLine",
6520
+ ctx: {
6521
+ seriesIndex,
6522
+ slope,
6523
+ intercept,
6524
+ ...ends
6525
+ }
6526
+ })] })];
6527
+ });
6528
+ }
6529
+ };
6530
+ }
6531
+ //#endregion
5858
6532
  //#region src/layers/axes.ts
5859
6533
  const CLASS_NAMES = {
5860
6534
  grid: "ct-grid",
@@ -5880,7 +6554,7 @@ function drawnOptions(axis) {
5880
6554
  },
5881
6555
  showLabel: options.showLabel ?? true,
5882
6556
  showGrid: options.showGrid ?? true,
5883
- labelInterpolationFnc: options.labelInterpolationFnc ?? ((value) => value)
6557
+ labelTransform: labelTransformOf(options)
5884
6558
  };
5885
6559
  }
5886
6560
  function selectedAxes(context, ids) {
@@ -6022,12 +6696,15 @@ function axisLabels(options = {}) {
6022
6696
  coordinates: ["cartesian"],
6023
6697
  series: [],
6024
6698
  compose(context) {
6025
- const svgLabel = (options.labels ?? context.labels) === "svg" ? svgLabels(context.measureText, "line", options.align) : void 0;
6026
- return [withFields(node("g", {}, classNames.labelGroup), { children: selectedAxes(context, options.axes).flatMap((axis) => nodesOf(axis, context, classNames, svgLabel).labels) })];
6699
+ const svg = (options.labels ?? context.labels) === "svg";
6700
+ return [withFields(node("g", {}, classNames.labelGroup), { children: selectedAxes(context, options.axes).flatMap((axis) => nodesOf(axis, context, classNames, svg ? svgLabels(context.measureText, "line", {
6701
+ ...options.align,
6702
+ ...axis.options.labelAlign
6703
+ }) : void 0).labels) })];
6027
6704
  }
6028
6705
  };
6029
6706
  }
6030
6707
  //#endregion
6031
- export { AutoScaleAxis, Axis, BarChart, BaseChart, EPSILON, EventEmitter, FixedScaleAxis, interpolation_exports as Interpolation, LineChart, PieChart, StepAxis, Svg, SvgList, SvgPath, alphaNumerate, autoScaleAxis, axisGrid, axisLabels, axisUnits, 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, textAscent, times, toScene, withOptions };
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 };
6032
6709
 
6033
6710
  //# sourceMappingURL=index.js.map