@galaxy-io/dls 1.5.14 → 1.5.15

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.
@@ -16,6 +16,31 @@ export type BarChartProps<TMetric extends string, TComponentKey extends string =
16
16
  /** Tint the active category's full band, marking the hover position even
17
17
  * where there is no data. @default true */
18
18
  showCursor?: boolean;
19
+ /**
20
+ * Floor, in px, for how short a *stack segment* may render — so a series
21
+ * that is a rounding error next to its neighbours still leaves a visible
22
+ * mark instead of disappearing. `0`, the default, draws segments at their
23
+ * true size.
24
+ *
25
+ * Opt-in because it is a lie, and a small one is worth being deliberate
26
+ * about: the pixels a sliver gains are taken from the other segments in the
27
+ * same stack, in proportion to how much slack each has. The stack's total
28
+ * height, its cap position and every value in the tooltip stay exactly
29
+ * right — only the split between segments shifts, by at most a couple of
30
+ * pixels per rescued sliver. Reach for it when presence/absence is the
31
+ * thing being read (an error series, a small cohort); leave it off when the
32
+ * chart is being read for proportion.
33
+ *
34
+ * Ignored on unstacked bars, which already have a 2px floor — there is no
35
+ * neighbour to take from, so it costs nothing there. Ignored, too, when a
36
+ * stack is so crowded that honouring it would pin every segment to the
37
+ * minimum: at that point the bar would stop describing its data, and the
38
+ * chart is better off saying so. Fold the tail into an "Other" component
39
+ * instead.
40
+ *
41
+ * @default 0
42
+ */
43
+ minSegmentLength?: number;
19
44
  /**
20
45
  * Fires when a bar segment or a category is clicked. Supplying it is what
21
46
  * makes the plot clickable.
@@ -48,5 +73,5 @@ export type BarChartProps<TMetric extends string, TComponentKey extends string =
48
73
  * `bars` in a group render side by side, several `components` in a bar stack,
49
74
  * and a chart doing both at once needs no extra configuration.
50
75
  */
51
- declare const BarChart: <TMetric extends string, TComponentKey extends string = string>({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization, showCursor, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: BarChartProps<TMetric, TComponentKey>) => import("react").JSX.Element;
76
+ declare const BarChart: <TMetric extends string, TComponentKey extends string = string>({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization, showCursor, minSegmentLength, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid, showLegend, legendMaxItems, noTooltip, tooltipRenderer, tooltipMaxItems, isLoading, contentWhenEmpty, ariaLabel, ariaDescription, }: BarChartProps<TMetric, TComponentKey>) => import("react").JSX.Element;
52
77
  export default BarChart;
@@ -17,7 +17,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
17
17
  * `bars` in a group render side by side, several `components` in a bar stack,
18
18
  * and a chart doing both at once needs no extra configuration.
19
19
  */
20
- var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization = BarChartNormalization.NONE, showCursor = true, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
20
+ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRatio, margin, normalization = BarChartNormalization.NONE, showCursor = true, minSegmentLength = 0, onSelect, selection, valueUnit, valueFormatter, labelFormatter, valueDomain, categoryLabelMode, categoryAxisTitle, valueAxisTitle, showGrid = true, showLegend = true, legendMaxItems, noTooltip = false, tooltipRenderer, tooltipMaxItems, isLoading = false, contentWhenEmpty, ariaLabel, ariaDescription }) => {
21
21
  const containerRef = useRef(null);
22
22
  const colors = useSeriesColorResolver(series);
23
23
  /**
@@ -78,7 +78,8 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
78
78
  normalization,
79
79
  isStacked,
80
80
  domain,
81
- resolveColor
81
+ resolveColor,
82
+ minSegmentLength
82
83
  }), [
83
84
  groups,
84
85
  plotWidth,
@@ -86,7 +87,8 @@ var BarChart = ({ series, groups, width, height, fillWidth, fillHeight, aspectRa
86
87
  normalization,
87
88
  isStacked,
88
89
  domain,
89
- resolveColor
90
+ resolveColor,
91
+ minSegmentLength
90
92
  ]);
91
93
  const isEmpty = groups.length === 0;
92
94
  const legendItems = useMemo(() => getBarLegendItems({
@@ -106,6 +106,12 @@ interface BuildBarChartGeometryArgs<TMetric extends string, TComponentKey extend
106
106
  * record to answer, which this module has no business importing.
107
107
  */
108
108
  resolveColor: (args: ResolveBarColorArgs<TMetric>) => string;
109
+ /**
110
+ * Minimum rendered length for a stack segment, in px. `0` disables it.
111
+ *
112
+ * See the prop of the same name on `BarChart` for what it costs.
113
+ */
114
+ minSegmentLength: number;
109
115
  }
110
116
  export interface ResolveBarColorArgs<TMetric extends string> {
111
117
  metric: TMetric;
@@ -116,5 +122,5 @@ export interface ResolveBarColorArgs<TMetric extends string> {
116
122
  /** The component's own palette override, if it declared one. */
117
123
  explicit?: ChartPalette;
118
124
  }
119
- export declare function buildBarChartGeometry<TMetric extends string, TComponentKey extends string = string>({ groups, plotWidth, plotHeight, normalization, isStacked, domain, resolveColor, }: BuildBarChartGeometryArgs<TMetric, TComponentKey>): BarChartGeometry<TMetric, TComponentKey>;
125
+ export declare function buildBarChartGeometry<TMetric extends string, TComponentKey extends string = string>({ groups, plotWidth, plotHeight, normalization, isStacked, domain, resolveColor, minSegmentLength, }: BuildBarChartGeometryArgs<TMetric, TComponentKey>): BarChartGeometry<TMetric, TComponentKey>;
120
126
  export {};
@@ -111,7 +111,86 @@ function getBarValueDomain(groups, normalization) {
111
111
  }
112
112
  return [min, max];
113
113
  }
114
- function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, isStacked, domain, resolveColor }) {
114
+ /**
115
+ * Grow every under-sized segment to its minimum, paying for it out of the
116
+ * segments that have room to spare.
117
+ *
118
+ * `lengths` and `minima` are one side of one stack, in baseline-outward order.
119
+ * Returns `null` when nothing needs doing, and — deliberately — also when the
120
+ * bill cannot be paid: if the surplus across the whole side is smaller than the
121
+ * total deficit, every segment would end up pinned at its minimum and the bar
122
+ * would no longer be a picture of its data. A stack that crowded is telling the
123
+ * caller to drop the minimum or fold the tail into an "Other" component, and
124
+ * silently redrawing it as a ladder of equal blocks would hide that.
125
+ */
126
+ function distributeMinSegmentLengths(lengths, minima) {
127
+ const result = [...lengths];
128
+ let deficit = 0;
129
+ let surplus = 0;
130
+ lengths.forEach((length, index) => {
131
+ if (length < minima[index]) {
132
+ deficit += minima[index] - length;
133
+ result[index] = minima[index];
134
+ } else surplus += length - minima[index];
135
+ });
136
+ if (deficit === 0 || surplus < deficit) return null;
137
+ const donors = result.map((length, index) => ({
138
+ index,
139
+ slack: length - minima[index]
140
+ })).filter((donor) => donor.slack > 0).sort((a, b) => b.slack - a.slack);
141
+ let remainingSlack = surplus;
142
+ for (const donor of donors) {
143
+ if (deficit <= 0) break;
144
+ const share = Math.min(donor.slack, Math.ceil(donor.slack / remainingSlack * deficit));
145
+ result[donor.index] -= share;
146
+ remainingSlack -= donor.slack;
147
+ deficit -= share;
148
+ }
149
+ return result;
150
+ }
151
+ /**
152
+ * Snapped pixel spans for one bar's segments, with {@link minSegmentLength}
153
+ * enforced.
154
+ *
155
+ * The enforcement re-lays the affected side of the stack *cumulatively* from the
156
+ * baseline outward, rather than nudging each segment's own edges. That is the
157
+ * only way to grow a sliver without breaking the invariant the rounding above
158
+ * exists to protect: each segment starts exactly where the previous one ended,
159
+ * so there is still one shared edge per seam and no overlap. And because the
160
+ * redistribution is zero-sum, the side's total extent is unchanged — the cap
161
+ * still lands on the same axis value it did before.
162
+ */
163
+ function resolveSegmentExtents(intervals, valueScale, minSegmentLength) {
164
+ const extents = intervals.map(({ from, to }) => ({
165
+ fromY: Math.round(valueScale(from)),
166
+ toY: Math.round(valueScale(to))
167
+ }));
168
+ if (minSegmentLength <= 0) return extents;
169
+ const adjustSide = (sign) => {
170
+ const indices = intervals.map((interval, index) => ({
171
+ interval,
172
+ index
173
+ })).filter(({ interval }) => Math.sign(interval.value) === sign).map(({ index }) => index);
174
+ if (indices.length === 0) return;
175
+ const adjusted = distributeMinSegmentLengths(indices.map((index) => Math.abs(extents[index].fromY - extents[index].toY)), indices.map((_, position) => position === 0 ? minSegmentLength : minSegmentLength + 2));
176
+ if (!adjusted) return;
177
+ const direction = sign > 0 ? -1 : 1;
178
+ const first = extents[indices[0]];
179
+ let cursor = sign > 0 ? first.fromY : first.toY;
180
+ indices.forEach((index, position) => {
181
+ const next = cursor + direction * adjusted[position];
182
+ extents[index] = {
183
+ fromY: cursor,
184
+ toY: next
185
+ };
186
+ cursor = next;
187
+ });
188
+ };
189
+ adjustSide(1);
190
+ adjustSide(-1);
191
+ return extents;
192
+ }
193
+ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, isStacked, domain, resolveColor, minSegmentLength }) {
115
194
  const valueScale = buildValueScale(domain, [plotHeight, 0]);
116
195
  const baselineY = valueScale(0);
117
196
  const rects = [];
@@ -139,6 +218,8 @@ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, i
139
218
  const barX = barsStartX + barIndex * (barWidth + effectiveBarGap);
140
219
  const values = bar.components.map((component) => sanitizeValue(component.value));
141
220
  const intervals = stackComponents(values, normalization);
221
+ const isStackedBar = bar.components.length > 1;
222
+ const extents = resolveSegmentExtents(intervals, valueScale, isStackedBar ? minSegmentLength : 0);
142
223
  let firstPositive = -1;
143
224
  let lastPositive = -1;
144
225
  let firstNegative = -1;
@@ -154,15 +235,13 @@ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, i
154
235
  }
155
236
  });
156
237
  bar.components.forEach((component, componentIndex) => {
157
- const { value, from, to } = intervals[componentIndex];
238
+ const { value } = intervals[componentIndex];
158
239
  if (value === 0) return;
159
240
  const isNegative = value < 0;
160
241
  const isCap = componentIndex === (isNegative ? lastNegative : lastPositive);
161
- const fromY = Math.round(valueScale(from));
162
- const toY = Math.round(valueScale(to));
242
+ const { fromY, toY } = extents[componentIndex];
163
243
  const topY = Math.min(fromY, toY);
164
244
  const rawHeight = Math.abs(fromY - toY);
165
- const isStackedBar = bar.components.length > 1;
166
245
  const shave = componentIndex === (isNegative ? firstNegative : firstPositive) ? 0 : 2;
167
246
  const height = Math.max(isStackedBar ? 0 : 2, rawHeight - shave);
168
247
  const y = isNegative ? topY + shave : topY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galaxy-io/dls",
3
- "version": "1.5.14",
3
+ "version": "1.5.15",
4
4
  "description": "Galaxy Design Language System",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",