@pushwoosh/kit-charts 0.0.11 → 0.0.13
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/ai-documentation.md +18 -0
- package/core/Chart/Axes.d.ts +2 -0
- package/core/Chart/Axes.js +26 -10
- package/core/Chart/Chart.js +4 -1
- package/core/layout/metrics.d.ts +2 -0
- package/core/layout/metrics.js +2 -0
- package/core/model/build-model.d.ts +2 -0
- package/core/model/build-model.js +33 -6
- package/core/scale/linear.d.ts +2 -0
- package/core/scale/linear.js +18 -4
- package/core/types/options.d.ts +5 -0
- package/package.json +1 -1
package/ai-documentation.md
CHANGED
|
@@ -92,6 +92,24 @@ own `formatter` or `format` wins over it.
|
|
|
92
92
|
the step is chosen from the candidates that stay integral at its magnitude, and
|
|
93
93
|
never falls below 1.
|
|
94
94
|
|
|
95
|
+
`tickAmount` is Highcharts' demand and not a hint: the y axis stretches the end
|
|
96
|
+
the caller left open until exactly that many ticks fit, so a count of 2 on a
|
|
97
|
+
`tickAmount: 5` axis still draws 0..4 and reads against a stable grid rather than
|
|
98
|
+
filling the plot. An end pinned with `max` cannot be stretched, so a tick amount
|
|
99
|
+
alongside one is ignored.
|
|
100
|
+
|
|
101
|
+
An x axis measures `labels.y` from the axis line itself, as Highcharts does: a
|
|
102
|
+
config that sets it replaces the default drop rather than adding to it. Left
|
|
103
|
+
unset, the label falls the tick's length plus a fixed drop below the line.
|
|
104
|
+
|
|
105
|
+
`minPadding`/`maxPadding` add a share of the data range beyond an end the caller
|
|
106
|
+
left open — Highcharts' axis air, off unless asked for. `labels.padding` is the
|
|
107
|
+
gap Highcharts leaves between the y label column and the plot; it defaults to 5
|
|
108
|
+
and widens the gutter, so a migrated config keeps its left edge.
|
|
109
|
+
|
|
110
|
+
The x axis line is drawn after the series, so a bar or an area sitting on zero
|
|
111
|
+
cannot paint over the line it stands on.
|
|
112
|
+
|
|
95
113
|
`labels.allowOverlap` defaults to false on both axes and on data labels. A label
|
|
96
114
|
whose box would run into one already placed loses its text — Highcharts hides
|
|
97
115
|
the label and keeps the tick, and so do we.
|
package/core/Chart/Axes.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface XAxisProps {
|
|
|
11
11
|
axis: AxisModel;
|
|
12
12
|
}
|
|
13
13
|
export declare function XAxis({ plot, axis }: XAxisProps): ReactElement | null;
|
|
14
|
+
/** Drawn after the series: a body sitting on zero would paint over its own axis. */
|
|
15
|
+
export declare function XAxisLine({ plot, axis }: XAxisProps): ReactElement | null;
|
|
14
16
|
interface YAxisProps {
|
|
15
17
|
plot: Rect;
|
|
16
18
|
axis: AxisModel;
|
package/core/Chart/Axes.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Hinted } from './Hinted';
|
|
3
3
|
import { DEFAULT_FONT_SIZE, measureText } from '../layout/measure-text';
|
|
4
|
-
import { AXIS_LABEL_GAP, AXIS_TITLE_GAP, crisp, TEXT_BASELINE_SHIFT,
|
|
4
|
+
import { AXIS_LABEL_GAP, AXIS_TITLE_GAP, crisp, TEXT_BASELINE_SHIFT, X_AXIS_TITLE_GAP } from '../layout/metrics';
|
|
5
|
+
import { xLabelOffset } from '../model/build-model';
|
|
5
6
|
import { DEFAULT_AXIS_COLOR, DEFAULT_GRID_COLOR, DEFAULT_MUTED_TEXT_COLOR } from '../model/palette';
|
|
6
7
|
const textProps = style => ({
|
|
7
8
|
fill: style?.color ?? DEFAULT_MUTED_TEXT_COLOR,
|
|
@@ -125,16 +126,9 @@ export function XAxis({
|
|
|
125
126
|
const tickLength = options.tickLength ?? 0;
|
|
126
127
|
const tickWidth = options.tickWidth ?? 1;
|
|
127
128
|
const labelsEnabled = options.labels?.enabled !== false;
|
|
128
|
-
const labelY = baseline + direction * (
|
|
129
|
+
const labelY = baseline + direction * xLabelOffset(options, onTop);
|
|
129
130
|
return _jsxs("g", {
|
|
130
|
-
children: [
|
|
131
|
-
x1: plot.x,
|
|
132
|
-
x2: plot.x + plot.width,
|
|
133
|
-
y1: crisp(baseline, options.lineWidth ?? 1),
|
|
134
|
-
y2: crisp(baseline, options.lineWidth ?? 1),
|
|
135
|
-
stroke: lineColor,
|
|
136
|
-
strokeWidth: options.lineWidth ?? 1
|
|
137
|
-
}), axis.ticks.map(tick => {
|
|
131
|
+
children: [axis.ticks.map(tick => {
|
|
138
132
|
const content = tickContent(tick);
|
|
139
133
|
return _jsxs("g", {
|
|
140
134
|
children: [tickLength > 0 && tickWidth > 0 ? _jsx("line", {
|
|
@@ -162,6 +156,28 @@ export function XAxis({
|
|
|
162
156
|
})]
|
|
163
157
|
});
|
|
164
158
|
}
|
|
159
|
+
/** Drawn after the series: a body sitting on zero would paint over its own axis. */
|
|
160
|
+
export function XAxisLine({
|
|
161
|
+
plot,
|
|
162
|
+
axis
|
|
163
|
+
}) {
|
|
164
|
+
const {
|
|
165
|
+
options
|
|
166
|
+
} = axis;
|
|
167
|
+
const width = options.lineWidth ?? 1;
|
|
168
|
+
if (options.visible === false || width === 0) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const baseline = options.opposite === true ? plot.y - (options.offset ?? 0) : plot.y + plot.height + (options.offset ?? 0);
|
|
172
|
+
return _jsx("line", {
|
|
173
|
+
x1: plot.x,
|
|
174
|
+
x2: plot.x + plot.width,
|
|
175
|
+
y1: crisp(baseline, width),
|
|
176
|
+
y2: crisp(baseline, width),
|
|
177
|
+
stroke: options.lineColor ?? DEFAULT_AXIS_COLOR,
|
|
178
|
+
strokeWidth: width
|
|
179
|
+
});
|
|
180
|
+
}
|
|
165
181
|
export function YAxis({
|
|
166
182
|
plot,
|
|
167
183
|
axis
|
package/core/Chart/Chart.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Grid, XAxis, YAxis } from './Axes';
|
|
3
|
+
import { Grid, XAxis, XAxisLine, YAxis } from './Axes';
|
|
4
4
|
import { Canvas, Centred, Container, PinnedAxes, ResetZoom, ScrollBox, SeriesLayer, TooltipCell, TooltipDivider, TooltipHeadCell, TooltipName, TooltipRow, TooltipSeriesCell, TooltipSeriesInner, TooltipSwatch, TooltipTable, TooltipTitle, TooltipValue, ZoomBand } from './Chart.styles';
|
|
5
5
|
import { ChartErrorBoundary } from './ChartErrorBoundary';
|
|
6
6
|
import { Legend } from './Legend';
|
|
@@ -575,6 +575,9 @@ function ChartBody({
|
|
|
575
575
|
"aria-hidden": series.hidden,
|
|
576
576
|
children: series.renderer.render(ctx)
|
|
577
577
|
}, `series-${series.index}`) : null;
|
|
578
|
+
}), model.axisless ? null : _jsx(XAxisLine, {
|
|
579
|
+
plot: model.plot,
|
|
580
|
+
axis: model.xAxis
|
|
578
581
|
}), decorations('over'), drag ? _jsx(ZoomBand, {
|
|
579
582
|
x: Math.min(drag.from, drag.to),
|
|
580
583
|
y: model.plot.y,
|
package/core/layout/metrics.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare const AXIS_LABEL_GAP = 8;
|
|
|
12
12
|
export declare const X_AXIS_LABEL_DROP = 14;
|
|
13
13
|
/** Air added to the tick label column when measuring the y axis gutter. */
|
|
14
14
|
export declare const AXIS_GUTTER_PADDING = 10;
|
|
15
|
+
/** `labels.padding` when the axis does not set it — the Highcharts default. */
|
|
16
|
+
export declare const AXIS_LABEL_PADDING = 5;
|
|
15
17
|
/** A rotated axis title to the column of tick labels it stands beside. */
|
|
16
18
|
export declare const AXIS_TITLE_GAP = 6;
|
|
17
19
|
/** An x axis title below its labels. */
|
package/core/layout/metrics.js
CHANGED
|
@@ -12,6 +12,8 @@ export const AXIS_LABEL_GAP = 8;
|
|
|
12
12
|
export const X_AXIS_LABEL_DROP = 14;
|
|
13
13
|
/** Air added to the tick label column when measuring the y axis gutter. */
|
|
14
14
|
export const AXIS_GUTTER_PADDING = 10;
|
|
15
|
+
/** `labels.padding` when the axis does not set it — the Highcharts default. */
|
|
16
|
+
export const AXIS_LABEL_PADDING = 5;
|
|
15
17
|
/** A rotated axis title to the column of tick labels it stands beside. */
|
|
16
18
|
export const AXIS_TITLE_GAP = 6;
|
|
17
19
|
/** An x axis title below its labels. */
|
|
@@ -58,6 +58,8 @@ export interface ChartModel {
|
|
|
58
58
|
export declare const hiddenKey: (seriesIndex: number, pointIndex?: number) => string;
|
|
59
59
|
declare const isBand: (scale: AnyScale) => scale is BandScale;
|
|
60
60
|
declare const isContinuous: (scale: AnyScale) => scale is ContinuousScale;
|
|
61
|
+
/** Axis line to the label baseline; `labels.y` replaces the drop, as in Highcharts. */
|
|
62
|
+
export declare function xLabelOffset(axis: AxisOptions, onTop: boolean): number;
|
|
61
63
|
export interface BuildModelInput {
|
|
62
64
|
options: ChartOptions;
|
|
63
65
|
size: Size;
|
|
@@ -3,7 +3,7 @@ import { normalizePoints } from '../data/points';
|
|
|
3
3
|
import { applyValueFormat, formatTime } from '../format/time';
|
|
4
4
|
import { measureLegend } from '../layout/legend-layout';
|
|
5
5
|
import { measureText, textHeight } from '../layout/measure-text';
|
|
6
|
-
import { AXIS_GUTTER_PADDING, AXIS_LABEL_GAP, AXIS_TITLE_GAP, MARKER_REACH, VALUE_LABEL_HEADROOM, X_AXIS_LABEL_DROP } from '../layout/metrics';
|
|
6
|
+
import { AXIS_GUTTER_PADDING, AXIS_LABEL_GAP, AXIS_LABEL_PADDING, AXIS_TITLE_GAP, MARKER_REACH, VALUE_LABEL_HEADROOM, X_AXIS_LABEL_DROP } from '../layout/metrics';
|
|
7
7
|
import { nodeText } from '../layout/node-text';
|
|
8
8
|
import { computeSlots } from '../layout/point-width';
|
|
9
9
|
import { layoutTitles } from '../layout/title-layout';
|
|
@@ -88,6 +88,27 @@ const grow = (extent, value) => {
|
|
|
88
88
|
extent.min = Math.min(extent.min, value);
|
|
89
89
|
extent.max = Math.max(extent.max, value);
|
|
90
90
|
};
|
|
91
|
+
/** Axis line to the label baseline; `labels.y` replaces the drop, as in Highcharts. */
|
|
92
|
+
export function xLabelOffset(axis, onTop) {
|
|
93
|
+
const explicit = axis.labels?.y;
|
|
94
|
+
if (explicit !== undefined) {
|
|
95
|
+
return onTop ? -explicit : explicit;
|
|
96
|
+
}
|
|
97
|
+
return (axis.tickLength ?? 0) + (onTop ? AXIS_LABEL_GAP : X_AXIS_LABEL_DROP);
|
|
98
|
+
}
|
|
99
|
+
/** Highcharts' air around a series: a share of the range beyond each open end. */
|
|
100
|
+
function axisPadding(axis, domain) {
|
|
101
|
+
const span = domain.max - domain.min;
|
|
102
|
+
const low = axis.min === undefined ? axis.minPadding ?? 0 : 0;
|
|
103
|
+
const high = axis.max === undefined ? axis.maxPadding ?? 0 : 0;
|
|
104
|
+
if (!Number.isFinite(span) || span <= 0 || low === 0 && high === 0) {
|
|
105
|
+
return domain;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
min: domain.min - span * low,
|
|
109
|
+
max: domain.max + span * high
|
|
110
|
+
};
|
|
111
|
+
}
|
|
91
112
|
const settled = (extent, fallback) => Number.isFinite(extent.min) && Number.isFinite(extent.max) ? [extent.min, extent.max] : fallback;
|
|
92
113
|
function buildXScale(axis, kind, extent, categories, range, hint) {
|
|
93
114
|
if (kind === 'band') {
|
|
@@ -128,7 +149,11 @@ function buildYScale(axis, extent, range, hint, linked) {
|
|
|
128
149
|
if (linked) {
|
|
129
150
|
return linked;
|
|
130
151
|
}
|
|
131
|
-
const
|
|
152
|
+
const padded = axisPadding(axis, {
|
|
153
|
+
min: extent.min,
|
|
154
|
+
max: extent.max
|
|
155
|
+
});
|
|
156
|
+
const [min, max] = settled(padded, [0, 1]);
|
|
132
157
|
return createLinearScale({
|
|
133
158
|
domain: [axis.min ?? min, axis.max ?? max],
|
|
134
159
|
range,
|
|
@@ -139,6 +164,7 @@ function buildYScale(axis, extent, range, hint, linked) {
|
|
|
139
164
|
max: axis.max === undefined
|
|
140
165
|
},
|
|
141
166
|
tickHint: hint,
|
|
167
|
+
tickCount: axis.tickAmount,
|
|
142
168
|
allowDecimals: axis.allowDecimals
|
|
143
169
|
});
|
|
144
170
|
}
|
|
@@ -409,10 +435,11 @@ export function buildChartModel({
|
|
|
409
435
|
renderer
|
|
410
436
|
}) => renderer.scales.x === 'band');
|
|
411
437
|
const pad = bandOnContinuous ? halfStepPadding(resolved) : 0;
|
|
412
|
-
const
|
|
438
|
+
const padded = pad > 0 ? {
|
|
413
439
|
min: acc.x.min - pad,
|
|
414
440
|
max: acc.x.max + pad
|
|
415
441
|
} : acc.x;
|
|
442
|
+
const xDomain = xKind === 'band' ? padded : axisPadding(xAxisOptions, padded);
|
|
416
443
|
const yDomains = acc.y;
|
|
417
444
|
const spacing = resolveSpacing(options.chart);
|
|
418
445
|
const [spaceTop, spaceRight, spaceBottom, spaceLeft] = spacing;
|
|
@@ -421,8 +448,8 @@ export function buildChartModel({
|
|
|
421
448
|
// A tick label is drawn a tick's length plus a fixed drop away from the axis
|
|
422
449
|
// line, and `labels.y` pushes it further still. Reserving only its text height
|
|
423
450
|
// leaves it hanging over the chart edge, where the svg clips it.
|
|
424
|
-
const
|
|
425
|
-
const xLabelHeight = axisless || xAxisOptions.labels?.enabled === false ? 0 : textHeight(labelStyle) +
|
|
451
|
+
const xLabelDrop = Math.max(0, xLabelOffset(xAxisOptions, labelsOnTop));
|
|
452
|
+
const xLabelHeight = axisless || xAxisOptions.labels?.enabled === false ? 0 : textHeight(labelStyle) + xLabelDrop;
|
|
426
453
|
const legendEnabled = options.legend?.enabled !== false;
|
|
427
454
|
const legend = legendEnabled ? all.flatMap(({
|
|
428
455
|
renderer,
|
|
@@ -486,7 +513,7 @@ export function buildChartModel({
|
|
|
486
513
|
}), axis.tickAmount ?? 5, undefined, 'y', numberFormat).map(tick => tick.label);
|
|
487
514
|
const widest = labels.reduce((widest_, label) => Math.max(widest_, measureText(label, axis.labels?.style)), 0);
|
|
488
515
|
const title = axis.title?.text && (axis.title.rotation ?? -90) !== 0 ? textHeight(axis.title.style) + AXIS_TITLE_GAP : 0;
|
|
489
|
-
return Math.ceil(widest) + AXIS_GUTTER_PADDING + title;
|
|
516
|
+
return Math.ceil(widest) + AXIS_GUTTER_PADDING + (axis.labels?.padding ?? AXIS_LABEL_PADDING) + title;
|
|
490
517
|
};
|
|
491
518
|
const gutter = axisless || yAxes[0]?.visible === false ? 0 : measureGutter(32);
|
|
492
519
|
const legendAlign = options.legend?.align ?? 'left';
|
package/core/scale/linear.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface LinearScaleOptions {
|
|
|
9
9
|
range: readonly [number, number];
|
|
10
10
|
nice?: boolean | NiceEnds;
|
|
11
11
|
tickHint?: number;
|
|
12
|
+
/** Highcharts' `tickAmount`: stretch the open end so exactly this many ticks fit. */
|
|
13
|
+
tickCount?: number;
|
|
12
14
|
/** False keeps every tick a whole number, as a count axis needs. */
|
|
13
15
|
allowDecimals?: boolean;
|
|
14
16
|
}
|
package/core/scale/linear.js
CHANGED
|
@@ -31,18 +31,32 @@ export function createLinearScale(options) {
|
|
|
31
31
|
range,
|
|
32
32
|
nice = false,
|
|
33
33
|
tickHint = 5,
|
|
34
|
+
tickCount,
|
|
34
35
|
allowDecimals = true
|
|
35
36
|
} = options;
|
|
36
37
|
let [min, max] = options.domain;
|
|
38
|
+
const niceMin = nice === true || typeof nice === 'object' && nice.min === true;
|
|
39
|
+
const niceMax = nice === true || typeof nice === 'object' && nice.max === true;
|
|
37
40
|
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
38
41
|
[min, max] = [0, 1];
|
|
39
42
|
} else if (min === max) {
|
|
40
43
|
const pad = Math.abs(min) || 1;
|
|
41
|
-
|
|
44
|
+
// An end the caller pinned stays put: padding both would take a `min: 0`
|
|
45
|
+
// count axis below zero on a day where every value is zero.
|
|
46
|
+
[min, max] = [min - (niceMin || !niceMax ? pad : 0), max + (niceMax || !niceMin ? pad : 0)];
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
if (tickCount !== undefined && tickCount > 1 && niceMax) {
|
|
49
|
+
let step = niceStep((max - min) / (tickCount - 1), allowDecimals);
|
|
50
|
+
if (niceMin) {
|
|
51
|
+
min = Math.floor(min / step) * step;
|
|
52
|
+
}
|
|
53
|
+
// Rounding the low end down lengthens the run, which can leave the last tick
|
|
54
|
+
// short of the data; widen the step until the fixed number of them covers it.
|
|
55
|
+
while (min + (tickCount - 1) * step < max) {
|
|
56
|
+
step = niceStep(step * 1.5, allowDecimals);
|
|
57
|
+
}
|
|
58
|
+
max = min + (tickCount - 1) * step;
|
|
59
|
+
} else if (niceMin || niceMax) {
|
|
46
60
|
const step = niceStep((max - min) / tickHint, allowDecimals);
|
|
47
61
|
if (niceMin) {
|
|
48
62
|
min = Math.floor(min / step) * step;
|
package/core/types/options.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export interface AxisLabelsOptions {
|
|
|
88
88
|
align?: 'left' | 'center' | 'right';
|
|
89
89
|
x?: number;
|
|
90
90
|
y?: number;
|
|
91
|
+
/** Air between the label column and the plot, as Highcharts reserves it. */
|
|
92
|
+
padding?: number;
|
|
91
93
|
style?: TextStyle;
|
|
92
94
|
}
|
|
93
95
|
/** `zIndex` at or above this draws over the series instead of under it. */
|
|
@@ -132,6 +134,9 @@ export interface AxisOptions {
|
|
|
132
134
|
categories?: readonly string[];
|
|
133
135
|
min?: number;
|
|
134
136
|
max?: number;
|
|
137
|
+
/** Share of the data range added beyond each end, Highcharts' axis padding. */
|
|
138
|
+
minPadding?: number;
|
|
139
|
+
maxPadding?: number;
|
|
135
140
|
visible?: boolean;
|
|
136
141
|
opposite?: boolean;
|
|
137
142
|
reversed?: boolean;
|