@optilogic/charts 1.3.8 → 1.3.10
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.cjs +34 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +34 -12
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/specialized/gantt-chart.tsx +56 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optilogic/charts",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"description": "Chart components for Optilogic - LineChart and BarChart built on Recharts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@optilogic/core": "1.
|
|
27
|
+
"@optilogic/core": "1.6.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"build": "tsup",
|
|
61
61
|
"dev": "tsup --watch",
|
|
62
62
|
"typecheck": "tsc --noEmit",
|
|
63
|
-
"lint": "eslint src
|
|
63
|
+
"lint": "eslint src",
|
|
64
64
|
"clean": "rm -rf dist .turbo"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -38,6 +38,12 @@ export interface GanttChartProps {
|
|
|
38
38
|
const ROW_HEIGHT_DEFAULT = 36;
|
|
39
39
|
const LABEL_WIDTH_DEFAULT = 160;
|
|
40
40
|
const HEADER_HEIGHT = 32;
|
|
41
|
+
/** Minimum horizontal space an axis label needs before labels start to collide. */
|
|
42
|
+
const MIN_LABEL_SPACING = 64;
|
|
43
|
+
/** Fallback chart width used before the container has been measured. */
|
|
44
|
+
const FALLBACK_CHART_WIDTH = 640;
|
|
45
|
+
/** Floor for the time-axis width so very narrow containers stay legible (scrolls). */
|
|
46
|
+
const MIN_CHART_WIDTH = 320;
|
|
41
47
|
|
|
42
48
|
function getTimeBounds(tasks: GanttTask[], milestones: GanttMilestone[]) {
|
|
43
49
|
let min = Infinity;
|
|
@@ -110,6 +116,7 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
110
116
|
ref,
|
|
111
117
|
) {
|
|
112
118
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
119
|
+
const [containerWidth, setContainerWidth] = React.useState(0);
|
|
113
120
|
const [hoveredId, setHoveredId] = React.useState<string | null>(null);
|
|
114
121
|
const [tooltipInfo, setTooltipInfo] = React.useState<{
|
|
115
122
|
task: GanttTask;
|
|
@@ -151,12 +158,37 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
151
158
|
[bounds, timeScale],
|
|
152
159
|
);
|
|
153
160
|
|
|
161
|
+
// Measure the container so the time axis fills the available width instead
|
|
162
|
+
// of a fixed guess — keeps bars, gridlines, and labels aligned at any size.
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
const el = containerRef.current;
|
|
165
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
166
|
+
const update = () => setContainerWidth(el.clientWidth);
|
|
167
|
+
update();
|
|
168
|
+
const observer = new ResizeObserver(update);
|
|
169
|
+
observer.observe(el);
|
|
170
|
+
return () => observer.disconnect();
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
const chartWidth = Math.max(
|
|
174
|
+
(containerWidth || labelWidth + FALLBACK_CHART_WIDTH) - labelWidth,
|
|
175
|
+
MIN_CHART_WIDTH,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Show at most one label per MIN_LABEL_SPACING px; thin the rest so daily
|
|
179
|
+
// ticks over a long span don't overlap into an unreadable smear. Gridlines
|
|
180
|
+
// are kept for every tick — only the text is thinned.
|
|
181
|
+
const labelStep = Math.max(
|
|
182
|
+
1,
|
|
183
|
+
Math.ceil(ticks.length / Math.max(1, Math.floor(chartWidth / MIN_LABEL_SPACING))),
|
|
184
|
+
);
|
|
185
|
+
|
|
154
186
|
const chartHeight =
|
|
155
187
|
typeof height === "number" ? height : undefined;
|
|
156
188
|
const svgHeight = HEADER_HEIGHT + orderedTasks.length * rowHeight + 8;
|
|
157
189
|
|
|
158
|
-
function xOf(time: number,
|
|
159
|
-
return ((time - bounds.min) / bounds.span) *
|
|
190
|
+
function xOf(time: number, width: number) {
|
|
191
|
+
return ((time - bounds.min) / bounds.span) * width;
|
|
160
192
|
}
|
|
161
193
|
|
|
162
194
|
return (
|
|
@@ -170,9 +202,9 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
170
202
|
style={{ height }}
|
|
171
203
|
>
|
|
172
204
|
<svg
|
|
173
|
-
width=
|
|
205
|
+
width={labelWidth + chartWidth}
|
|
174
206
|
height={Math.max(svgHeight, chartHeight ?? svgHeight)}
|
|
175
|
-
style={{ minWidth: labelWidth +
|
|
207
|
+
style={{ minWidth: labelWidth + MIN_CHART_WIDTH }}
|
|
176
208
|
>
|
|
177
209
|
{/* Label background */}
|
|
178
210
|
<rect
|
|
@@ -193,7 +225,8 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
193
225
|
{/* Time axis header */}
|
|
194
226
|
<g>
|
|
195
227
|
{ticks.map((tick, i) => {
|
|
196
|
-
const x = labelWidth + xOf(tick.pos,
|
|
228
|
+
const x = labelWidth + xOf(tick.pos, chartWidth);
|
|
229
|
+
const showLabel = i % labelStep === 0;
|
|
197
230
|
return (
|
|
198
231
|
<g key={i}>
|
|
199
232
|
<line
|
|
@@ -204,14 +237,16 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
204
237
|
stroke="hsl(var(--divider))"
|
|
205
238
|
strokeDasharray="2 4"
|
|
206
239
|
/>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
{showLabel && (
|
|
241
|
+
<text
|
|
242
|
+
x={x + 4}
|
|
243
|
+
y={HEADER_HEIGHT - 10}
|
|
244
|
+
fontSize={9}
|
|
245
|
+
fill="hsl(var(--muted-foreground))"
|
|
246
|
+
>
|
|
247
|
+
{tick.label}
|
|
248
|
+
</text>
|
|
249
|
+
)}
|
|
215
250
|
</g>
|
|
216
251
|
);
|
|
217
252
|
})}
|
|
@@ -220,8 +255,10 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
220
255
|
{/* Rows */}
|
|
221
256
|
{orderedTasks.map((task, i) => {
|
|
222
257
|
const y = HEADER_HEIGHT + i * rowHeight;
|
|
223
|
-
const barX = labelWidth + xOf(task.start.getTime(),
|
|
224
|
-
const barW =
|
|
258
|
+
const barX = labelWidth + xOf(task.start.getTime(), chartWidth);
|
|
259
|
+
const barW =
|
|
260
|
+
xOf(task.end.getTime(), chartWidth) -
|
|
261
|
+
xOf(task.start.getTime(), chartWidth);
|
|
225
262
|
const barH = rowHeight * 0.55;
|
|
226
263
|
const barY = y + (rowHeight - barH) / 2;
|
|
227
264
|
const color = task.color ?? getChartColor(i);
|
|
@@ -306,9 +343,9 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
306
343
|
if (fromIdx === undefined) return null;
|
|
307
344
|
const toIdx = taskIndex.get(task.id)!;
|
|
308
345
|
const fromTask = orderedTasks[fromIdx];
|
|
309
|
-
const fromEndX = labelWidth + xOf(fromTask.end.getTime(),
|
|
346
|
+
const fromEndX = labelWidth + xOf(fromTask.end.getTime(), chartWidth);
|
|
310
347
|
const fromY = HEADER_HEIGHT + fromIdx * rowHeight + rowHeight / 2;
|
|
311
|
-
const toStartX = labelWidth + xOf(task.start.getTime(),
|
|
348
|
+
const toStartX = labelWidth + xOf(task.start.getTime(), chartWidth);
|
|
312
349
|
const toY = HEADER_HEIGHT + toIdx * rowHeight + rowHeight / 2;
|
|
313
350
|
const midX = (fromEndX + toStartX) / 2;
|
|
314
351
|
|
|
@@ -328,8 +365,8 @@ const GanttChart = React.forwardRef<HTMLDivElement, GanttChartProps>(
|
|
|
328
365
|
)}
|
|
329
366
|
|
|
330
367
|
{/* Milestones */}
|
|
331
|
-
{milestones.map((m
|
|
332
|
-
const x = labelWidth + xOf(m.date.getTime(),
|
|
368
|
+
{milestones.map((m) => {
|
|
369
|
+
const x = labelWidth + xOf(m.date.getTime(), chartWidth);
|
|
333
370
|
return (
|
|
334
371
|
<g key={m.id}>
|
|
335
372
|
<line
|