@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.
@@ -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.
@@ -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;
@@ -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, X_AXIS_LABEL_DROP, X_AXIS_TITLE_GAP } from '../layout/metrics';
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 * (tickLength + (onTop ? AXIS_LABEL_GAP : X_AXIS_LABEL_DROP)) + (options.labels?.y ?? 0);
129
+ const labelY = baseline + direction * xLabelOffset(options, onTop);
129
130
  return _jsxs("g", {
130
- children: [options.lineWidth === 0 ? null : _jsx("line", {
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
@@ -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,
@@ -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. */
@@ -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 [min, max] = settled(extent, [0, 1]);
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 xDomain = pad > 0 ? {
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 xLabelPush = labelsOnTop ? Math.max(0, -(xAxisOptions.labels?.y ?? 0)) : Math.max(0, xAxisOptions.labels?.y ?? 0);
426
- const xLabelHeight = axisless || xAxisOptions.labels?.enabled === false ? 0 : textHeight(labelStyle) + (xAxisOptions.tickLength ?? 0) + (labelsOnTop ? AXIS_LABEL_GAP : X_AXIS_LABEL_DROP) + xLabelPush;
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';
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/kit-charts",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Small React chart library on SVG, with a Highcharts-shaped configuration model",
5
5
  "license": "MIT",
6
6
  "repository": {