@pushwoosh/kit-charts 0.0.12 → 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 +12 -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 +32 -6
- package/core/types/options.d.ts +5 -0
- package/package.json +1 -1
package/ai-documentation.md
CHANGED
|
@@ -98,6 +98,18 @@ the caller left open until exactly that many ticks fit, so a count of 2 on a
|
|
|
98
98
|
filling the plot. An end pinned with `max` cannot be stretched, so a tick amount
|
|
99
99
|
alongside one is ignored.
|
|
100
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
|
+
|
|
101
113
|
`labels.allowOverlap` defaults to false on both axes and on data labels. A label
|
|
102
114
|
whose box would run into one already placed loses its text — Highcharts hides
|
|
103
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,
|
|
@@ -410,10 +435,11 @@ export function buildChartModel({
|
|
|
410
435
|
renderer
|
|
411
436
|
}) => renderer.scales.x === 'band');
|
|
412
437
|
const pad = bandOnContinuous ? halfStepPadding(resolved) : 0;
|
|
413
|
-
const
|
|
438
|
+
const padded = pad > 0 ? {
|
|
414
439
|
min: acc.x.min - pad,
|
|
415
440
|
max: acc.x.max + pad
|
|
416
441
|
} : acc.x;
|
|
442
|
+
const xDomain = xKind === 'band' ? padded : axisPadding(xAxisOptions, padded);
|
|
417
443
|
const yDomains = acc.y;
|
|
418
444
|
const spacing = resolveSpacing(options.chart);
|
|
419
445
|
const [spaceTop, spaceRight, spaceBottom, spaceLeft] = spacing;
|
|
@@ -422,8 +448,8 @@ export function buildChartModel({
|
|
|
422
448
|
// A tick label is drawn a tick's length plus a fixed drop away from the axis
|
|
423
449
|
// line, and `labels.y` pushes it further still. Reserving only its text height
|
|
424
450
|
// leaves it hanging over the chart edge, where the svg clips it.
|
|
425
|
-
const
|
|
426
|
-
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;
|
|
427
453
|
const legendEnabled = options.legend?.enabled !== false;
|
|
428
454
|
const legend = legendEnabled ? all.flatMap(({
|
|
429
455
|
renderer,
|
|
@@ -487,7 +513,7 @@ export function buildChartModel({
|
|
|
487
513
|
}), axis.tickAmount ?? 5, undefined, 'y', numberFormat).map(tick => tick.label);
|
|
488
514
|
const widest = labels.reduce((widest_, label) => Math.max(widest_, measureText(label, axis.labels?.style)), 0);
|
|
489
515
|
const title = axis.title?.text && (axis.title.rotation ?? -90) !== 0 ? textHeight(axis.title.style) + AXIS_TITLE_GAP : 0;
|
|
490
|
-
return Math.ceil(widest) + AXIS_GUTTER_PADDING + title;
|
|
516
|
+
return Math.ceil(widest) + AXIS_GUTTER_PADDING + (axis.labels?.padding ?? AXIS_LABEL_PADDING) + title;
|
|
491
517
|
};
|
|
492
518
|
const gutter = axisless || yAxes[0]?.visible === false ? 0 : measureGutter(32);
|
|
493
519
|
const legendAlign = options.legend?.align ?? 'left';
|
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;
|