react-scale-break-chart 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,686 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var clsx = require('clsx');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var clsx__default = /*#__PURE__*/_interopDefault(clsx);
10
+
11
+ // src/ScaleBreakBarChart.tsx
12
+
13
+ // src/utils.ts
14
+ function computeAxisBreak(data, series, chartHeight, breakThreshold) {
15
+ if (data.length === 0 || series.length === 0) {
16
+ return { hasBreak: false, yMin: 0, lowerMax: 0, upperMin: 0, upperMax: 0, lowerPx: chartHeight, breakBandPx: 0, upperPx: 0, transitionPx: 0 };
17
+ }
18
+ const seriesMaxes = series.map(
19
+ (s) => Math.max(0, ...data.map((d) => Number(d[s.dataKey]) || 0))
20
+ );
21
+ const globalMax = Math.max(...seriesMaxes, 1);
22
+ const sortedDesc = [...seriesMaxes].sort((a, b) => b - a);
23
+ const largestSeriesMax = sortedDesc[0];
24
+ const secondLargestSeriesMax = sortedDesc[1] || 0;
25
+ const topDominant = series.length > 1 && secondLargestSeriesMax > 0 && largestSeriesMax / secondLargestSeriesMax > breakThreshold;
26
+ const sortedAsc = [...seriesMaxes].filter((v) => v > 0).sort((a, b) => a - b);
27
+ const bottomDwarfed = sortedAsc.length >= 2 && sortedAsc[0] > 0 && sortedAsc[1] / sortedAsc[0] > breakThreshold;
28
+ if (!topDominant && !bottomDwarfed) {
29
+ const allValues = data.flatMap(
30
+ (d) => series.map((s) => Number(d[s.dataKey]) || 0)
31
+ ).filter((v) => v > 0);
32
+ const globalMin = allValues.length > 0 ? Math.min(...allValues) : 0;
33
+ let yMin = 0;
34
+ let yMax = globalMax;
35
+ if (globalMin > 0 && globalMax > 0 && globalMin / globalMax > 0.8) {
36
+ const dataRange = globalMax - globalMin;
37
+ if (dataRange > 0) {
38
+ yMin = globalMin - dataRange * 1.5;
39
+ yMax = globalMax + dataRange * 0.8;
40
+ } else {
41
+ yMin = globalMin * 0.95;
42
+ yMax = globalMax * 1.05;
43
+ }
44
+ yMin = Math.max(0, yMin);
45
+ const snapMag = Math.pow(10, Math.floor(Math.log10(Math.max(dataRange, yMax * 0.01))));
46
+ yMin = Math.floor(yMin / snapMag) * snapMag;
47
+ } else {
48
+ yMax = globalMax * 1.05;
49
+ }
50
+ return { hasBreak: false, yMin, lowerMax: yMax, upperMin: yMax, upperMax: yMax, lowerPx: chartHeight, breakBandPx: 0, upperPx: 0, transitionPx: 0 };
51
+ }
52
+ let lowerMax;
53
+ if (bottomDwarfed && topDominant) {
54
+ lowerMax = Math.min(sortedAsc[0] * 1.5, secondLargestSeriesMax * 1.3);
55
+ } else if (bottomDwarfed) {
56
+ lowerMax = sortedAsc[0] * 1.5;
57
+ } else {
58
+ lowerMax = secondLargestSeriesMax * 1.3;
59
+ }
60
+ const dominantIdx = seriesMaxes.indexOf(Math.max(...seriesMaxes));
61
+ const dominantValues = data.map((d) => Number(d[series[dominantIdx].dataKey]) || 0).filter((v) => v > lowerMax);
62
+ let upperMin = lowerMax;
63
+ if (dominantValues.length > 1) {
64
+ const minDom = Math.min(...dominantValues);
65
+ const maxDom = Math.max(...dominantValues);
66
+ const domRange = maxDom - minDom;
67
+ if (domRange > 0 && minDom > 0 && domRange / minDom < 0.1) {
68
+ upperMin = minDom - domRange * 1.5;
69
+ const snapMag = Math.pow(10, Math.floor(Math.log10(Math.max(domRange, 1))));
70
+ upperMin = Math.floor(upperMin / snapMag) * snapMag;
71
+ upperMin = Math.max(upperMin, lowerMax);
72
+ }
73
+ }
74
+ let computedUpperMax;
75
+ if (upperMin > lowerMax) {
76
+ const zoomedRange = globalMax - upperMin;
77
+ computedUpperMax = globalMax + zoomedRange * 0.15;
78
+ } else {
79
+ computedUpperMax = globalMax * 1.15;
80
+ }
81
+ const breakBandPx = 14;
82
+ const lowerPx = chartHeight * 0.4;
83
+ const upperPx = chartHeight - lowerPx - breakBandPx;
84
+ const transitionPx = upperMin > lowerMax ? upperPx * 0.15 : 0;
85
+ return {
86
+ hasBreak: true,
87
+ yMin: 0,
88
+ lowerMax,
89
+ upperMin,
90
+ upperMax: computedUpperMax,
91
+ lowerPx,
92
+ breakBandPx,
93
+ upperPx,
94
+ transitionPx
95
+ };
96
+ }
97
+ function computeGroupedLayouts(data, series, axisBreak) {
98
+ return data.map((point) => {
99
+ const bars = series.map((s, i) => {
100
+ const value = Number(point[s.dataKey]) || 0;
101
+ if (!axisBreak.hasBreak || value <= axisBreak.lowerMax) {
102
+ const range = axisBreak.lowerMax - axisBreak.yMin;
103
+ const height = range > 0 ? (value - axisBreak.yMin) / range * axisBreak.lowerPx : 0;
104
+ return {
105
+ seriesIndex: i,
106
+ value,
107
+ color: s.color,
108
+ totalHeight: Math.max(0, height),
109
+ lowerHeight: Math.max(0, height),
110
+ upperHeight: 0,
111
+ hasBreak: false
112
+ };
113
+ }
114
+ const lowerHeight = axisBreak.lowerPx;
115
+ const upperRange = axisBreak.upperMax - axisBreak.upperMin;
116
+ let upperHeight;
117
+ if (axisBreak.transitionPx > 0) {
118
+ const remainingPx = axisBreak.upperPx - axisBreak.transitionPx;
119
+ if (value <= axisBreak.upperMin) {
120
+ const gapRange = axisBreak.upperMin - axisBreak.lowerMax;
121
+ const gapFrac = gapRange > 0 ? (value - axisBreak.lowerMax) / gapRange : 1;
122
+ upperHeight = Math.sqrt(gapFrac) * axisBreak.transitionPx;
123
+ } else {
124
+ const zoomedFrac = upperRange > 0 ? (value - axisBreak.upperMin) / upperRange : 0;
125
+ upperHeight = axisBreak.transitionPx + zoomedFrac * remainingPx;
126
+ }
127
+ } else {
128
+ const upperFraction = upperRange > 0 ? Math.max(0, (value - axisBreak.upperMin) / upperRange) : 0;
129
+ upperHeight = upperFraction * axisBreak.upperPx;
130
+ }
131
+ const totalHeight = lowerHeight + axisBreak.breakBandPx + upperHeight;
132
+ return {
133
+ seriesIndex: i,
134
+ value,
135
+ color: s.color,
136
+ totalHeight,
137
+ lowerHeight,
138
+ upperHeight,
139
+ hasBreak: true
140
+ };
141
+ });
142
+ return { name: point.name, bars };
143
+ });
144
+ }
145
+ var PADDING_TOP = 12;
146
+ var PADDING_BOTTOM = 10;
147
+ var LEGEND_AREA_BASE = 36;
148
+ var LEGEND_AREA_ROTATED_EXTRA = 40;
149
+ var PADDING_RIGHT = 12;
150
+ var BAR_GAP_RATIO = 0.15;
151
+ var INNER_BAR_GAP = 3;
152
+ var AXIS_LABEL_FONT_SIZE = 12;
153
+ var VALUE_FONT_SIZE = 9;
154
+ var Y_AXIS_WIDTH = 76;
155
+ var ZIGZAG_OVERFLOW = 5;
156
+ function computeYTicks(maxVal, tickCount, minVal = 0) {
157
+ if (maxVal <= minVal) return [minVal];
158
+ const range = maxVal - minVal;
159
+ const rough = range / (tickCount - 1);
160
+ const mag = Math.pow(10, Math.floor(Math.log10(rough)));
161
+ const nice = [1, 2, 2.5, 5, 10].find((n) => n * mag >= rough) || 10;
162
+ const step = nice * mag;
163
+ const ticks = [];
164
+ const start = Math.floor(minVal / step) * step;
165
+ for (let v = start; v <= maxVal + step * 0.01; v += step) {
166
+ ticks.push(v);
167
+ }
168
+ if (ticks.length > tickCount + 2) ticks.splice(tickCount + 2);
169
+ return ticks;
170
+ }
171
+ function hexToRgb(hex) {
172
+ const h = hex.replace("#", "");
173
+ return [
174
+ parseInt(h.substring(0, 2), 16),
175
+ parseInt(h.substring(2, 4), 16),
176
+ parseInt(h.substring(4, 6), 16)
177
+ ];
178
+ }
179
+ function darkenHex(hex, amount) {
180
+ const [r, g, b] = hexToRgb(hex);
181
+ const dr = Math.max(0, Math.round(r * (1 - amount)));
182
+ const dg = Math.max(0, Math.round(g * (1 - amount)));
183
+ const db = Math.max(0, Math.round(b * (1 - amount)));
184
+ return `#${dr.toString(16).padStart(2, "0")}${dg.toString(16).padStart(2, "0")}${db.toString(16).padStart(2, "0")}`;
185
+ }
186
+ function luminance(hex) {
187
+ const [r, g, b] = hexToRgb(hex);
188
+ const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(
189
+ (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
190
+ );
191
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
192
+ }
193
+ function contrastText(barColor) {
194
+ return luminance(barColor) > 0.4 ? "#1a1a1a" : "#f5f5f5";
195
+ }
196
+ function buildBreakPaths(x, yCenter, width, barColor, overflow = ZIGZAG_OVERFLOW, gap = 10, amplitude = 5, toothWidth = 12, slope = 6) {
197
+ const extX = x - overflow;
198
+ const extWidth = width + overflow * 2;
199
+ const teeth = Math.max(2, Math.ceil(extWidth / toothWidth));
200
+ const step = extWidth / teeth;
201
+ const slopePerPx = slope / extWidth;
202
+ const upperPts = [];
203
+ const yUpperLeft = yCenter - gap / 2;
204
+ upperPts.push({ x: extX, y: yUpperLeft });
205
+ for (let i = 0; i < teeth; i++) {
206
+ const midX = extX + step * i + step / 2;
207
+ const endX = extX + step * (i + 1);
208
+ const midSlope = (midX - extX) * slopePerPx;
209
+ const endSlope = (endX - extX) * slopePerPx;
210
+ upperPts.push({ x: midX, y: yUpperLeft - midSlope - amplitude });
211
+ upperPts.push({ x: endX, y: yUpperLeft - endSlope });
212
+ }
213
+ const lowerPts = [];
214
+ const yLowerLeft = yCenter + gap / 2;
215
+ lowerPts.push({ x: extX, y: yLowerLeft });
216
+ for (let i = 0; i < teeth; i++) {
217
+ const midX = extX + step * i + step / 2;
218
+ const endX = extX + step * (i + 1);
219
+ const midSlope = (midX - extX) * slopePerPx;
220
+ const endSlope = (endX - extX) * slopePerPx;
221
+ lowerPts.push({ x: midX, y: yLowerLeft - midSlope + amplitude });
222
+ lowerPts.push({ x: endX, y: yLowerLeft - endSlope });
223
+ }
224
+ const toPath = (pts) => `M ${pts[0].x} ${pts[0].y}` + pts.slice(1).map((p) => ` L ${p.x} ${p.y}`).join("");
225
+ const fillD = toPath(upperPts) + ` L ${lowerPts[lowerPts.length - 1].x} ${lowerPts[lowerPts.length - 1].y}` + [...lowerPts].reverse().slice(1).map((p) => ` L ${p.x} ${p.y}`).join("") + " Z";
226
+ return {
227
+ upper: toPath(upperPts),
228
+ lower: toPath(lowerPts),
229
+ fill: fillD,
230
+ fillColor: darkenHex(barColor, 0.25),
231
+ strokeColor: darkenHex(barColor, 0.4)
232
+ };
233
+ }
234
+ function ScaleBreakBarChart({
235
+ data,
236
+ series,
237
+ height = 350,
238
+ breakThreshold = 5,
239
+ formatValue = String,
240
+ className
241
+ }) {
242
+ const isAutoHeight = height === "auto";
243
+ const containerRef = react.useRef(null);
244
+ const [containerWidth, setContainerWidth] = react.useState(0);
245
+ const [containerHeight, setContainerHeight] = react.useState(0);
246
+ const [hoveredBar, setHoveredBar] = react.useState(null);
247
+ const [tooltipPos, setTooltipPos] = react.useState(null);
248
+ react.useEffect(() => {
249
+ const el = containerRef.current;
250
+ if (!el) return;
251
+ const ro = new ResizeObserver((entries) => {
252
+ for (const entry of entries) {
253
+ setContainerWidth(entry.contentRect.width);
254
+ if (isAutoHeight) setContainerHeight(entry.contentRect.height);
255
+ }
256
+ });
257
+ ro.observe(el);
258
+ const rect = el.getBoundingClientRect();
259
+ setContainerWidth(rect.width);
260
+ if (isAutoHeight) setContainerHeight(rect.height);
261
+ return () => ro.disconnect();
262
+ }, [isAutoHeight]);
263
+ const resolvedHeight = isAutoHeight ? containerHeight : height;
264
+ const svgHeight = resolvedHeight;
265
+ const chartLeft = Y_AXIS_WIDTH;
266
+ const chartWidth = containerWidth - chartLeft - PADDING_RIGHT;
267
+ const needsRotatedLabels = react.useMemo(() => {
268
+ const avgLabelLen = data.reduce((s, d) => s + d.name.length, 0) / (data.length || 1);
269
+ const periodCnt = data.length || 1;
270
+ const slotW = chartWidth / periodCnt;
271
+ const charPx = (periodCnt > 6 ? 9 : AXIS_LABEL_FONT_SIZE) * 0.55;
272
+ return avgLabelLen * charPx > slotW * 0.85;
273
+ }, [data, chartWidth]);
274
+ const legendArea = LEGEND_AREA_BASE + (needsRotatedLabels ? LEGEND_AREA_ROTATED_EXTRA : 0);
275
+ const chartAreaTop = PADDING_TOP;
276
+ const chartAreaBottom = svgHeight - PADDING_BOTTOM - legendArea;
277
+ const chartHeight = chartAreaBottom - chartAreaTop;
278
+ const axisBreak = react.useMemo(
279
+ () => computeAxisBreak(data, series, chartHeight, breakThreshold),
280
+ [data, series, chartHeight, breakThreshold]
281
+ );
282
+ const layouts = react.useMemo(
283
+ () => computeGroupedLayouts(data, series, axisBreak),
284
+ [data, series, axisBreak]
285
+ );
286
+ const lowerRegionBottom = chartAreaBottom;
287
+ const lowerRegionTop = chartAreaBottom - axisBreak.lowerPx;
288
+ const breakBandTop = lowerRegionTop - axisBreak.breakBandPx;
289
+ const lowerTicks = react.useMemo(
290
+ () => computeYTicks(axisBreak.lowerMax, axisBreak.hasBreak ? 6 : 8, axisBreak.yMin),
291
+ [axisBreak.lowerMax, axisBreak.hasBreak, axisBreak.yMin]
292
+ );
293
+ const upperTicks = react.useMemo(() => {
294
+ if (!axisBreak.hasBreak) return [];
295
+ const lo = axisBreak.upperMin;
296
+ const hi = axisBreak.upperMax;
297
+ const range = hi - lo;
298
+ if (range <= 0) return [];
299
+ const rough = range / 3;
300
+ const mag = Math.pow(10, Math.floor(Math.log10(rough)));
301
+ const nice = [1, 2, 2.5, 5, 10].find((n) => n * mag >= rough) || 10;
302
+ const step = nice * mag;
303
+ const ticks = [];
304
+ const start = Math.ceil(lo / step) * step;
305
+ for (let v = start; v <= hi + step * 0.01; v += step) {
306
+ if (v > lo) ticks.push(v);
307
+ }
308
+ return ticks;
309
+ }, [axisBreak]);
310
+ const valueToY = react.useCallback((value) => {
311
+ if (!axisBreak.hasBreak || value <= axisBreak.lowerMax) {
312
+ const range = axisBreak.lowerMax - axisBreak.yMin;
313
+ if (range === 0) return lowerRegionBottom;
314
+ return lowerRegionBottom - (value - axisBreak.yMin) / range * axisBreak.lowerPx;
315
+ }
316
+ const upperRange = axisBreak.upperMax - axisBreak.upperMin;
317
+ if (upperRange === 0) return breakBandTop;
318
+ if (axisBreak.transitionPx > 0) {
319
+ const remainingPx = axisBreak.upperPx - axisBreak.transitionPx;
320
+ const zoomedFrac = Math.max(0, (value - axisBreak.upperMin) / upperRange);
321
+ return breakBandTop - axisBreak.transitionPx - zoomedFrac * remainingPx;
322
+ }
323
+ const upperFraction = Math.max(0, (value - axisBreak.upperMin) / upperRange);
324
+ return breakBandTop - upperFraction * axisBreak.upperPx;
325
+ }, [axisBreak, lowerRegionBottom, breakBandTop]);
326
+ const periodCount = data.length || 1;
327
+ const groupSlotWidth = chartWidth / periodCount;
328
+ const groupBarArea = groupSlotWidth * (1 - BAR_GAP_RATIO);
329
+ const singleBarWidth = (groupBarArea - INNER_BAR_GAP * (series.length - 1)) / series.length;
330
+ const groupPadding = (groupSlotWidth - groupBarArea) / 2;
331
+ const handleMouseEnter = react.useCallback(
332
+ (period, bar, e) => {
333
+ setHoveredBar({ period, bar });
334
+ const rect = containerRef.current?.getBoundingClientRect();
335
+ if (rect) setTooltipPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
336
+ },
337
+ []
338
+ );
339
+ const handleMouseMove = react.useCallback((e) => {
340
+ const rect = containerRef.current?.getBoundingClientRect();
341
+ if (rect) setTooltipPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
342
+ }, []);
343
+ const handleMouseLeave = react.useCallback(() => {
344
+ setHoveredBar(null);
345
+ setTooltipPos(null);
346
+ }, []);
347
+ const fontFamily = "system-ui, -apple-system, sans-serif";
348
+ if (containerWidth === 0 || isAutoHeight && containerHeight === 0) {
349
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: clsx__default.default("w-full", className), style: isAutoHeight ? { height: "100%", minHeight: 200 } : { height } });
350
+ }
351
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: clsx__default.default("w-full", className), style: { position: "relative", ...isAutoHeight ? { height: "100%", minHeight: 200 } : { height } }, children: [
352
+ /* @__PURE__ */ jsxRuntime.jsxs(
353
+ "svg",
354
+ {
355
+ width: containerWidth,
356
+ height: svgHeight,
357
+ viewBox: `0 0 ${containerWidth} ${svgHeight}`,
358
+ style: { userSelect: "none" },
359
+ children: [
360
+ lowerTicks.map((tick) => {
361
+ const yPx = valueToY(tick);
362
+ if (isNaN(yPx)) return null;
363
+ return /* @__PURE__ */ jsxRuntime.jsx(
364
+ "text",
365
+ {
366
+ x: chartLeft - 6,
367
+ y: yPx,
368
+ textAnchor: "end",
369
+ dominantBaseline: "central",
370
+ fontSize: AXIS_LABEL_FONT_SIZE,
371
+ fill: "#999",
372
+ style: { pointerEvents: "none", fontFamily },
373
+ children: formatValue(tick)
374
+ },
375
+ `ylow-${tick}`
376
+ );
377
+ }),
378
+ upperTicks.map((tick) => {
379
+ const yPx = valueToY(tick);
380
+ if (yPx < chartAreaTop || yPx > chartAreaBottom) return null;
381
+ return /* @__PURE__ */ jsxRuntime.jsx(
382
+ "text",
383
+ {
384
+ x: chartLeft - 6,
385
+ y: yPx,
386
+ textAnchor: "end",
387
+ dominantBaseline: "central",
388
+ fontSize: AXIS_LABEL_FONT_SIZE,
389
+ fill: "#999",
390
+ style: { pointerEvents: "none", fontFamily },
391
+ children: formatValue(tick)
392
+ },
393
+ `yupp-${tick}`
394
+ );
395
+ }),
396
+ /* @__PURE__ */ jsxRuntime.jsx(
397
+ "line",
398
+ {
399
+ x1: chartLeft,
400
+ y1: chartAreaTop,
401
+ x2: chartLeft,
402
+ y2: chartAreaBottom,
403
+ stroke: "#999",
404
+ strokeWidth: 1,
405
+ opacity: 0.3
406
+ }
407
+ ),
408
+ layouts.map((_group, gIdx) => {
409
+ const groupX = chartLeft + gIdx * groupSlotWidth;
410
+ return _group.bars.map((_bar, bIdx) => {
411
+ const barX = groupX + groupPadding + bIdx * (singleBarWidth + INNER_BAR_GAP);
412
+ const centerX = barX + singleBarWidth / 2;
413
+ return /* @__PURE__ */ jsxRuntime.jsx(
414
+ "line",
415
+ {
416
+ x1: centerX,
417
+ y1: chartAreaTop,
418
+ x2: centerX,
419
+ y2: chartAreaBottom,
420
+ stroke: "#999",
421
+ strokeWidth: 0.8,
422
+ strokeDasharray: "4 4",
423
+ opacity: 0.2
424
+ },
425
+ `vgrid-${gIdx}-${bIdx}`
426
+ );
427
+ });
428
+ }),
429
+ lowerTicks.map((tick, idx) => {
430
+ const yPx = valueToY(tick);
431
+ if (isNaN(yPx)) return null;
432
+ const isBaseline = idx === 0 || tick === 0;
433
+ return /* @__PURE__ */ jsxRuntime.jsx(
434
+ "line",
435
+ {
436
+ x1: chartLeft,
437
+ y1: yPx,
438
+ x2: containerWidth - PADDING_RIGHT,
439
+ y2: yPx,
440
+ stroke: "#999",
441
+ strokeWidth: isBaseline ? 1 : 0.8,
442
+ strokeDasharray: isBaseline ? void 0 : "6 4",
443
+ opacity: isBaseline ? 0.4 : 0.2
444
+ },
445
+ `hgrid-low-${tick}`
446
+ );
447
+ }),
448
+ upperTicks.map((tick) => {
449
+ const yPx = valueToY(tick);
450
+ if (isNaN(yPx) || yPx < chartAreaTop || yPx > chartAreaBottom) return null;
451
+ return /* @__PURE__ */ jsxRuntime.jsx(
452
+ "line",
453
+ {
454
+ x1: chartLeft,
455
+ y1: yPx,
456
+ x2: containerWidth - PADDING_RIGHT,
457
+ y2: yPx,
458
+ stroke: "#999",
459
+ strokeWidth: 0.8,
460
+ strokeDasharray: "6 4",
461
+ opacity: 0.2
462
+ },
463
+ `hgrid-upp-${tick}`
464
+ );
465
+ }),
466
+ layouts.map((group, gIdx) => {
467
+ const groupX = chartLeft + gIdx * groupSlotWidth;
468
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
469
+ group.bars.map((bar, bIdx) => {
470
+ const barX = groupX + groupPadding + bIdx * (singleBarWidth + INNER_BAR_GAP);
471
+ const isHovered = hoveredBar?.period === gIdx && hoveredBar?.bar === bIdx;
472
+ const anyHovered = hoveredBar !== null;
473
+ if (!bar.hasBreak) {
474
+ const barTop2 = lowerRegionBottom - bar.lowerHeight;
475
+ return /* @__PURE__ */ jsxRuntime.jsxs(
476
+ "g",
477
+ {
478
+ onMouseEnter: (e) => handleMouseEnter(gIdx, bIdx, e),
479
+ onMouseMove: handleMouseMove,
480
+ onMouseLeave: handleMouseLeave,
481
+ style: { cursor: "pointer", opacity: anyHovered && !isHovered ? 0.4 : 1, transition: "opacity 150ms ease" },
482
+ children: [
483
+ /* @__PURE__ */ jsxRuntime.jsx(
484
+ "rect",
485
+ {
486
+ x: barX,
487
+ y: barTop2,
488
+ width: singleBarWidth,
489
+ height: Math.max(bar.lowerHeight, 0),
490
+ fill: bar.color,
491
+ rx: 2
492
+ }
493
+ ),
494
+ bar.lowerHeight >= 18 && singleBarWidth >= 30 && /* @__PURE__ */ jsxRuntime.jsx(
495
+ "text",
496
+ {
497
+ x: barX + singleBarWidth / 2,
498
+ y: barTop2 + bar.lowerHeight / 2,
499
+ textAnchor: "middle",
500
+ dominantBaseline: "central",
501
+ fontSize: VALUE_FONT_SIZE,
502
+ fill: contrastText(bar.color),
503
+ opacity: 0.85,
504
+ style: { pointerEvents: "none", fontFamily },
505
+ fontWeight: 500,
506
+ children: formatValue(bar.value)
507
+ }
508
+ )
509
+ ]
510
+ },
511
+ bIdx
512
+ );
513
+ }
514
+ const barTop = breakBandTop - bar.upperHeight;
515
+ const breakCenterY = lowerRegionTop - axisBreak.breakBandPx / 2;
516
+ return /* @__PURE__ */ jsxRuntime.jsxs(
517
+ "g",
518
+ {
519
+ onMouseEnter: (e) => handleMouseEnter(gIdx, bIdx, e),
520
+ onMouseMove: handleMouseMove,
521
+ onMouseLeave: handleMouseLeave,
522
+ style: { cursor: "pointer", opacity: anyHovered && !isHovered ? 0.4 : 1, transition: "opacity 150ms ease" },
523
+ children: [
524
+ /* @__PURE__ */ jsxRuntime.jsx(
525
+ "rect",
526
+ {
527
+ x: barX,
528
+ y: barTop,
529
+ width: singleBarWidth,
530
+ height: Math.max(lowerRegionBottom - barTop, 0),
531
+ fill: bar.color,
532
+ rx: 2
533
+ }
534
+ ),
535
+ (() => {
536
+ const bp = buildBreakPaths(barX, breakCenterY, singleBarWidth, bar.color, ZIGZAG_OVERFLOW, axisBreak.breakBandPx * 0.45, 4, 10, 16);
537
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
538
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: bp.fill, fill: bp.fillColor, opacity: 0.92 }),
539
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: bp.upper, fill: "none", stroke: bp.strokeColor, strokeWidth: 1.2, strokeLinejoin: "bevel", opacity: 0.7 }),
540
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: bp.lower, fill: "none", stroke: bp.strokeColor, strokeWidth: 1.2, strokeLinejoin: "bevel", opacity: 0.7 })
541
+ ] });
542
+ })(),
543
+ singleBarWidth >= 30 && /* @__PURE__ */ jsxRuntime.jsx(
544
+ "text",
545
+ {
546
+ x: barX + singleBarWidth / 2,
547
+ y: barTop + bar.upperHeight * 0.5,
548
+ textAnchor: "middle",
549
+ dominantBaseline: "central",
550
+ fontSize: VALUE_FONT_SIZE,
551
+ fill: contrastText(bar.color),
552
+ opacity: 0.85,
553
+ style: { pointerEvents: "none", fontFamily },
554
+ fontWeight: 500,
555
+ children: formatValue(bar.value)
556
+ }
557
+ )
558
+ ]
559
+ },
560
+ bIdx
561
+ );
562
+ }),
563
+ (() => {
564
+ const labelFontSize = periodCount > 8 ? 9 : periodCount > 6 ? 10 : AXIS_LABEL_FONT_SIZE;
565
+ if (needsRotatedLabels) {
566
+ return /* @__PURE__ */ jsxRuntime.jsx(
567
+ "text",
568
+ {
569
+ x: groupX + groupSlotWidth / 2,
570
+ y: chartAreaBottom + 6,
571
+ textAnchor: "end",
572
+ fontSize: labelFontSize,
573
+ fill: "#999",
574
+ style: { pointerEvents: "none", fontFamily },
575
+ transform: `rotate(-55, ${groupX + groupSlotWidth / 2}, ${chartAreaBottom + 6})`,
576
+ children: group.name
577
+ }
578
+ );
579
+ }
580
+ return /* @__PURE__ */ jsxRuntime.jsx(
581
+ "text",
582
+ {
583
+ x: groupX + groupSlotWidth / 2,
584
+ y: chartAreaBottom + 14,
585
+ textAnchor: "middle",
586
+ fontSize: labelFontSize,
587
+ fill: "#999",
588
+ style: { pointerEvents: "none", fontFamily },
589
+ children: group.name
590
+ }
591
+ );
592
+ })()
593
+ ] }, group.name);
594
+ }),
595
+ axisBreak.hasBreak && (() => {
596
+ const breakY = lowerRegionTop - axisBreak.breakBandPx / 2;
597
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
598
+ /* @__PURE__ */ jsxRuntime.jsx(
599
+ "line",
600
+ {
601
+ x1: chartLeft,
602
+ y1: breakY - 3,
603
+ x2: chartLeft,
604
+ y2: breakY + 3,
605
+ stroke: "#fff",
606
+ strokeWidth: 4
607
+ }
608
+ ),
609
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: chartLeft - 4, y1: breakY - 3, x2: chartLeft + 4, y2: breakY - 1, stroke: "#888", strokeWidth: 1 }),
610
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: chartLeft - 4, y1: breakY + 1, x2: chartLeft + 4, y2: breakY + 3, stroke: "#888", strokeWidth: 1 })
611
+ ] });
612
+ })(),
613
+ (() => {
614
+ const LEGEND_ICON = 10;
615
+ const LEGEND_GAP = 6;
616
+ const LEGEND_PAD = 16;
617
+ const LEGEND_FONT = 10;
618
+ const charW = LEGEND_FONT * 0.58;
619
+ const itemWidths = series.map((s) => LEGEND_ICON + LEGEND_GAP + s.label.length * charW + LEGEND_PAD);
620
+ const totalW = itemWidths.reduce((a, b) => a + b, 0);
621
+ const startX = chartLeft + chartWidth / 2 - totalW / 2;
622
+ const legendY = svgHeight - legendArea / 2 + (needsRotatedLabels ? LEGEND_AREA_ROTATED_EXTRA / 2 : 0);
623
+ let curX = startX;
624
+ return series.map((s, i) => {
625
+ const x = curX;
626
+ curX += itemWidths[i];
627
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
628
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x, y: legendY - 5, width: LEGEND_ICON, height: LEGEND_ICON, rx: 2, fill: s.color }),
629
+ /* @__PURE__ */ jsxRuntime.jsx(
630
+ "text",
631
+ {
632
+ x: x + LEGEND_ICON + LEGEND_GAP,
633
+ y: legendY,
634
+ dominantBaseline: "central",
635
+ fontSize: LEGEND_FONT,
636
+ fill: "#999",
637
+ style: { pointerEvents: "none", fontFamily },
638
+ children: s.label
639
+ }
640
+ )
641
+ ] }, s.dataKey);
642
+ });
643
+ })()
644
+ ]
645
+ }
646
+ ),
647
+ hoveredBar !== null && tooltipPos && layouts[hoveredBar.period] && (() => {
648
+ const bar = layouts[hoveredBar.period].bars[hoveredBar.bar];
649
+ if (!bar) return null;
650
+ return /* @__PURE__ */ jsxRuntime.jsxs(
651
+ "div",
652
+ {
653
+ style: {
654
+ position: "absolute",
655
+ zIndex: 50,
656
+ pointerEvents: "none",
657
+ backgroundColor: "#fff",
658
+ border: "1px solid #e0e0e0",
659
+ borderRadius: 8,
660
+ boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
661
+ padding: "12px",
662
+ fontSize: 14,
663
+ left: Math.min(tooltipPos.x + 12, containerWidth - 200),
664
+ top: Math.max(tooltipPos.y - 20, 0),
665
+ fontFamily
666
+ },
667
+ children: [
668
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontWeight: 600, marginBottom: 4 }, children: layouts[hoveredBar.period].name }),
669
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 }, children: [
670
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { display: "inline-block", width: 10, height: 10, borderRadius: 2, backgroundColor: bar.color } }),
671
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "#888" }, children: [
672
+ series[bar.seriesIndex].label,
673
+ ":"
674
+ ] }),
675
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 500, marginLeft: "auto", paddingLeft: 12 }, children: formatValue(bar.value) })
676
+ ] })
677
+ ]
678
+ }
679
+ );
680
+ })()
681
+ ] });
682
+ }
683
+
684
+ exports.ScaleBreakBarChart = ScaleBreakBarChart;
685
+ //# sourceMappingURL=index.js.map
686
+ //# sourceMappingURL=index.js.map