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