@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optilogic/charts",
3
- "version": "1.3.8",
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.4.0"
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 --ext .ts,.tsx",
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, chartWidth: number) {
159
- return ((time - bounds.min) / bounds.span) * chartWidth;
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="100%"
205
+ width={labelWidth + chartWidth}
174
206
  height={Math.max(svgHeight, chartHeight ?? svgHeight)}
175
- style={{ minWidth: labelWidth + 400 }}
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, 1000);
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
- <text
208
- x={x + 4}
209
- y={HEADER_HEIGHT - 10}
210
- fontSize={9}
211
- fill="hsl(var(--muted-foreground))"
212
- >
213
- {tick.label}
214
- </text>
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(), 1000);
224
- const barW = xOf(task.end.getTime(), 1000) - xOf(task.start.getTime(), 1000);
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(), 1000);
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(), 1000);
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, i) => {
332
- const x = labelWidth + xOf(m.date.getTime(), 1000);
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