@pushwoosh/kit-charts 0.0.15 → 0.0.17

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.
@@ -113,6 +113,19 @@ it, exactly as `pick(labels.x, distance)` does there.
113
113
  The x axis line is drawn after the series, so a bar or an area sitting on zero
114
114
  cannot paint over the line it stands on.
115
115
 
116
+ A y axis with `labels.enabled: false` reserves no gutter at all — only a rotated
117
+ title still takes room — so the plot starts at `chart.spacing`, where Highcharts
118
+ starts it, rather than a label column in from there.
119
+
120
+ An end the axis leaves unpadded puts its extreme point on the plot edge, where
121
+ half of the point's marker would fall outside the SVG and be cut. Such an end
122
+ keeps `MARKER_REACH` of room: the plot edge moves in to 5px from the canvas
123
+ unless the gutter, the spacing or `chart.marginLeft`/`marginRight` already leaves
124
+ at least that much, so a chart with `spacing: 0` and no gutter is 5px in rather
125
+ than flush. Only an end that can draw a marker pays it: `marker.enabled: false`
126
+ does not release the room — the hover dot is drawn regardless, as in Highcharts —
127
+ but `marker.states.hover.enabled: false` does, and then no dot is drawn either.
128
+
116
129
  `labels.allowOverlap` defaults to false on both axes and on data labels. A label
117
130
  whose box would run into one already placed loses its text — Highcharts hides
118
131
  the label and keeps the tick, and so do we.
@@ -7,7 +7,7 @@ import { AXIS_LABEL_GAP, AXIS_LABEL_DISTANCE, AXIS_TITLE_GAP, MARKER_REACH, VALU
7
7
  import { nodeText } from '../layout/node-text';
8
8
  import { computeSlots } from '../layout/point-width';
9
9
  import { layoutTitles } from '../layout/title-layout';
10
- import { resolveAxes, resolveSeries, isSeriesVisible } from '../options/normalize';
10
+ import { resolveAxes, resolveSeries, isSeriesVisible, showsHoverMarker, showsMarkers } from '../options/normalize';
11
11
  import { createBandScale } from '../scale/band';
12
12
  import { createColorScale } from '../scale/color';
13
13
  import { createLinearScale } from '../scale/linear';
@@ -475,7 +475,7 @@ export function buildChartModel({
475
475
  const yLabelsShown = !axisless && yAxes[0]?.visible !== false && yAxes[0]?.labels?.enabled !== false;
476
476
  const markerReach = resolved.some(({
477
477
  options: series
478
- }) => series.marker?.enabled !== false) ? MARKER_REACH : 0;
478
+ }) => showsMarkers(series.marker)) ? MARKER_REACH : 0;
479
479
  // The topmost tick label is centred on the plot's top edge, so half of it hangs
480
480
  // above the plot: without this the highest grid line and its label sit on the
481
481
  // chart border.
@@ -499,10 +499,13 @@ export function buildChartModel({
499
499
  })();
500
500
  const marginTop = spaceTop + titleHeight + (legendAtTop ? legendHeight : 0) + (labelsOnTop ? xLabelHeight : 0) + Math.max(labelHeadroom, topOverflow);
501
501
  const marginBottom = spaceBottom + (labelsOnTop ? 0 : xLabelHeight) + (legendAtTop ? 0 : legendHeight);
502
- const measureGutter = estimate => {
502
+ const measureGutter = () => {
503
503
  const axis = yAxes[0] ?? {};
504
+ const title = axis.title?.text && (axis.title.rotation ?? -90) !== 0 ? textHeight(axis.title.style) + AXIS_TITLE_GAP : 0;
505
+ // Highcharts measures the label column and nothing else, so an axis without
506
+ // labels leaves the plot flush with the chart edge rather than indented.
504
507
  if (axis.labels?.enabled === false) {
505
- return estimate;
508
+ return title;
506
509
  }
507
510
  const labels = axis.categories?.length ? [...axis.categories] : axisTicks(axis, createLinearScale({
508
511
  domain: (() => {
@@ -515,17 +518,26 @@ export function buildChartModel({
515
518
  allowDecimals: axis.allowDecimals
516
519
  }), axis.tickAmount ?? 5, undefined, 'y', numberFormat).map(tick => tick.label);
517
520
  const widest = labels.reduce((widest_, label) => Math.max(widest_, measureText(label, axis.labels?.style)), 0);
518
- const title = axis.title?.text && (axis.title.rotation ?? -90) !== 0 ? textHeight(axis.title.style) + AXIS_TITLE_GAP : 0;
519
521
  // Highcharts reserves the widest label plus the distance it is drawn at; the two
520
522
  // must be one number or the plot edge and the label column drift apart.
521
523
  return Math.ceil(widest) + yLabelGap(axis) + title;
522
524
  };
523
- const gutter = axisless || yAxes[0]?.visible === false ? 0 : measureGutter(32);
525
+ const gutter = axisless || yAxes[0]?.visible === false ? 0 : measureGutter();
524
526
  const legendAlign = options.legend?.align ?? 'left';
525
527
  const legendBesideLeft = legendMetrics.beside && legendAlign === 'left';
528
+ // A point on the plot edge draws half its marker outside the canvas, where the
529
+ // svg cuts it; an end the axis pads keeps that room already.
530
+ const edgeMarkers = !axisless && xKind !== 'band' && resolved.some(({
531
+ renderer,
532
+ options: series
533
+ }) => renderer.scales.x !== 'band' && (showsMarkers(series.marker) || showsHoverMarker(series.marker)));
534
+ const hoverReach = edgeMarkers ? MARKER_REACH : 0;
535
+ const domainMin = xAxisOptions.min ?? xDomain.min;
536
+ const domainMax = xAxisOptions.max ?? xDomain.max;
537
+ const edgeRoom = (margin, flush) => flush ? Math.max(margin, hoverReach) : margin;
526
538
  // Highcharts margins replace the computed edge distance rather than adding to it.
527
- const left = options.chart?.marginLeft ?? spaceLeft + gutter + (legendBesideLeft ? legendMetrics.width : 0);
528
- const right = options.chart?.marginRight ?? spaceRight + (legendMetrics.beside && !legendBesideLeft ? legendMetrics.width : 0);
539
+ const left = options.chart?.marginLeft ?? edgeRoom(spaceLeft + gutter + (legendBesideLeft ? legendMetrics.width : 0), domainMin === acc.x.min);
540
+ const right = options.chart?.marginRight ?? edgeRoom(spaceRight + (legendMetrics.beside && !legendBesideLeft ? legendMetrics.width : 0), domainMax === acc.x.max);
529
541
  const top = options.chart?.marginTop ?? marginTop;
530
542
  const bottom = options.chart?.marginBottom ?? marginBottom;
531
543
  const plot = {
@@ -1,4 +1,4 @@
1
- import { type AxisOptions, type ChartOptions, type SeriesOptionsBase } from '../types/options';
1
+ import { type AxisOptions, type ChartOptions, type MarkerOptions, type SeriesOptionsBase } from '../types/options';
2
2
  export declare const toAxisArray: (axis: AxisOptions | readonly AxisOptions[] | undefined) => AxisOptions[];
3
3
  export declare function resolveSeries(options: ChartOptions): SeriesOptionsBase[];
4
4
  export declare function resolveAxes(options: ChartOptions): {
@@ -6,3 +6,7 @@ export declare function resolveAxes(options: ChartOptions): {
6
6
  yAxes: AxisOptions[];
7
7
  };
8
8
  export declare const isSeriesVisible: (series: SeriesOptionsBase) => boolean;
9
+ /** A marker at every point of the series. */
10
+ export declare const showsMarkers: (marker: MarkerOptions | undefined) => boolean;
11
+ /** The dot under the pointer, which Highcharts draws whatever `marker.enabled` says. */
12
+ export declare const showsHoverMarker: (marker: MarkerOptions | undefined) => boolean;
@@ -40,4 +40,8 @@ export function resolveAxes(options) {
40
40
  yAxes: toAxisArray(options.yAxis)
41
41
  };
42
42
  }
43
- export const isSeriesVisible = series => series.visible !== false;
43
+ export const isSeriesVisible = series => series.visible !== false;
44
+ /** A marker at every point of the series. */
45
+ export const showsMarkers = marker => marker?.enabled !== false && marker?.symbol !== 'none';
46
+ /** The dot under the pointer, which Highcharts draws whatever `marker.enabled` says. */
47
+ export const showsHoverMarker = marker => marker?.states?.hover?.enabled !== false;
@@ -90,7 +90,12 @@ const LADDER = [{
90
90
  }];
91
91
  const pickRungIndex = (span, count) => {
92
92
  const target = span / Math.max(1, count);
93
- const found = LADDER.findIndex(rung => rung.approx >= target);
93
+ // Highcharts keeps a rung until the midpoint to the next one, so a month of data read at
94
+ // three days a tick still steps by two days instead of jumping to whole weeks.
95
+ const found = LADDER.findIndex((rung, index) => {
96
+ const next = LADDER[index + 1];
97
+ return !next || target <= (rung.approx + next.approx) / 2;
98
+ });
94
99
  return found === -1 ? LADDER.length - 1 : found;
95
100
  };
96
101
  const alignUp = (from, rung) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/kit-charts",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Small React chart library on SVG, with a Highcharts-shaped configuration model",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { drawable, projectSeries, toAreaPath, toLinePath } from '../../core/geometry/path';
3
+ import { showsHoverMarker } from '../../core/options/normalize';
3
4
  import { includeZero } from '../shared/domain';
4
5
  import { nearestByX } from '../shared/hit';
5
6
  import { seriesLegend } from '../shared/legend';
@@ -41,7 +42,7 @@ export const area = {
41
42
  marker: ctx.series.marker,
42
43
  projected: projected,
43
44
  color: color
44
- }), hovered ? _jsx("circle", {
45
+ }), hovered && showsHoverMarker(ctx.series.marker) ? _jsx("circle", {
45
46
  cx: hovered.px,
46
47
  cy: hovered.py,
47
48
  r: ctx.series.marker?.states?.hover?.radius ?? 4,
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { drawable, projectSeries, toLinePath } from '../../core/geometry/path';
3
+ import { showsHoverMarker } from '../../core/options/normalize';
3
4
  import { nearestByX } from '../shared/hit';
4
5
  import { seriesLegend } from '../shared/legend';
5
6
  import { Markers } from '../shared/Markers';
@@ -33,7 +34,7 @@ export const line = {
33
34
  marker: ctx.series.marker,
34
35
  projected: projected,
35
36
  color: color
36
- }), hovered ? _jsx("circle", {
37
+ }), hovered && showsHoverMarker(ctx.series.marker) ? _jsx("circle", {
37
38
  cx: hovered.px,
38
39
  cy: hovered.py,
39
40
  r: ctx.series.marker?.states?.hover?.radius ?? 4,
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { showsMarkers } from '../../core/options/normalize';
2
3
  const markerRadius = marker => marker?.radius ?? (marker?.width ? marker.width / 2 : 3);
3
4
  export function Markers({
4
5
  marker,
5
6
  projected,
6
7
  color
7
8
  }) {
8
- if (marker?.enabled === false || marker?.symbol === 'none') {
9
+ if (!showsMarkers(marker)) {
9
10
  return null;
10
11
  }
11
12
  const radius = markerRadius(marker);