@usetheo/ui 0.34.0 → 0.35.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.
@@ -50,6 +50,17 @@ export declare function niceMax(values: number[]): number;
50
50
  * marker is what makes it visible.
51
51
  */
52
52
  export declare function seriesPath(points: TrendPoint[], xScale: (x: number) => number, yScale: (y: number) => number): string;
53
+ /**
54
+ * Quais posicoes do eixo recebem rotulo.
55
+ *
56
+ * Rotular 31 pontos em ~450px e ilegivel; rotular so as pontas nao responde "quando foi o pico".
57
+ * A densidade sai da largura REAL (`viewW`, que o ResizeObserver ja rastreia), com o primeiro e o
58
+ * ultimo SEMPRE presentes: sao as bordas da janela, e sem eles o leitor nao sabe de quando ate
59
+ * quando o grafico fala.
60
+ *
61
+ * Fora do JSX porque e aritmetica, e aritmetica isolada e aritmetica que a mutacao alcanca.
62
+ */
63
+ export declare function xTicks(axisXs: number[], viewW: number): number[];
53
64
  /**
54
65
  * Quais pontos recebem marcador.
55
66
  *
@@ -86,6 +97,20 @@ export interface TrendChartProps extends HTMLAttributes<HTMLElement> {
86
97
  * Ignored when ≤ 0. Default: auto (`niceMax`).
87
98
  */
88
99
  yMax?: number;
100
+ /**
101
+ * Formata o `x` para o eixo e para a tabela acessivel. **Opcional de proposito.**
102
+ *
103
+ * O contrato do `x` e ambiguo — "bucket index or epoch-ms" — e na pratica quase unanime: das seis
104
+ * fabricas de serie do consumidor, cinco passam INDICE e uma passa epoch-ms. Inferir por magnitude
105
+ * funcionaria e seria a forma exata do defeito que este componente ja pagou (a regra de marcador
106
+ * do M76 chaveava em `points.length` em vez de perguntar o que queria saber, e a densificacao do
107
+ * M144 a desligou em silencio). Quem sabe o que o `x` significa e o chamador.
108
+ *
109
+ * Sem esta prop, nada muda: nenhum rotulo de eixo X e desenhado e a tabela segue numerando.
110
+ */
111
+ xFormatter?: (x: number) => string;
112
+ /** Cabecalho da primeira coluna da tabela acessivel. "Point" sobre datas seria desonesto. */
113
+ xLabel?: string;
89
114
  }
90
115
  declare const TrendChart: import("react").ForwardRefExoticComponent<TrendChartProps & import("react").RefAttributes<HTMLElement>>;
91
116
  export { TrendChart };
package/dist/index.js CHANGED
@@ -5109,6 +5109,17 @@ function seriesPath(points, xScale, yScale) {
5109
5109
  }
5110
5110
  var VIEW_W = 600;
5111
5111
  var PAD = { top: 8, right: 8, bottom: 4, left: 40 };
5112
+ var X_AXIS_PX = 14;
5113
+ var MIN_TICK_PX2 = 70;
5114
+ function xTicks(axisXs, viewW) {
5115
+ if (axisXs.length <= 2) return [...axisXs];
5116
+ const maxTicks = Math.max(2, Math.floor((viewW - PAD.left - PAD.right) / MIN_TICK_PX2));
5117
+ if (axisXs.length <= maxTicks) return [...axisXs];
5118
+ const passo = (axisXs.length - 1) / (maxTicks - 1);
5119
+ const escolhidos = /* @__PURE__ */ new Set();
5120
+ for (let i = 0; i < maxTicks; i++) escolhidos.add(Math.round(i * passo));
5121
+ return [...escolhidos].sort((a, b) => a - b).map((i) => axisXs[i]);
5122
+ }
5112
5123
  var SPARSE_MARKER_MAX = 5;
5113
5124
  function markedPoints(points) {
5114
5125
  const finito = (p) => Number.isFinite(p?.y);
@@ -5123,10 +5134,13 @@ var TrendChart = forwardRef(
5123
5134
  height = 180,
5124
5135
  valueFormatter = (v) => String(v),
5125
5136
  yMax: yMaxProp,
5137
+ xFormatter,
5138
+ xLabel,
5126
5139
  className,
5127
5140
  ...props
5128
5141
  }, ref) => {
5129
5142
  const [viewW, setViewW] = useState(VIEW_W);
5143
+ const [hoverIdx, setHoverIdx] = useState(null);
5130
5144
  const roRef = useRef(null);
5131
5145
  const setSvgNode = useCallback((node) => {
5132
5146
  roRef.current?.disconnect();
@@ -5172,72 +5186,149 @@ var TrendChart = forwardRef(
5172
5186
  const yMax = yMaxProp != null && yMaxProp > 0 ? yMaxProp : niceMax(allPoints.map((p) => p.y)) || 1;
5173
5187
  const axisXs = Array.from(new Set(allPoints.map((p) => p.x))).sort((a, b) => a - b);
5174
5188
  const yByX = series.map((s) => new Map(s.points.map((p) => [p.x, p.y])));
5189
+ const padBottom = PAD.bottom + (xFormatter ? X_AXIS_PX : 0);
5175
5190
  const xScale = linScale([xMin, xMax], [PAD.left, viewW - PAD.right]);
5176
- const yScale = linScale([0, yMax], [height - PAD.bottom, PAD.top]);
5191
+ const yScale = linScale([0, yMax], [height - padBottom, PAD.top]);
5177
5192
  return /* @__PURE__ */ jsxs("figure", { "data-slot": "trend-chart", ref, className: cn("space-y-1", className), ...props, children: [
5178
5193
  /* @__PURE__ */ jsx("figcaption", { className: "font-semibold text-label-caps text-muted-foreground uppercase", children: title }),
5179
5194
  /* @__PURE__ */ jsxs(
5180
- "svg",
5195
+ "div",
5181
5196
  {
5182
- ref: setSvgNode,
5183
- viewBox: `0 0 ${viewW} ${height}`,
5184
- preserveAspectRatio: "none",
5185
- role: "img",
5186
- "aria-label": `${title} trend`,
5187
- className: "w-full",
5188
- style: { height },
5197
+ "data-slot": "trend-chart-surface",
5198
+ className: "relative",
5199
+ onMouseMove: (e) => {
5200
+ const caixa = e.currentTarget.getBoundingClientRect();
5201
+ if (caixa.width === 0 || axisXs.length === 0) return;
5202
+ const frac = (e.clientX - caixa.left) / caixa.width;
5203
+ const alvo = xMin + frac * (xMax - xMin);
5204
+ let melhor = 0;
5205
+ for (let i = 1; i < axisXs.length; i++) {
5206
+ if (Math.abs(axisXs[i] - alvo) < Math.abs(axisXs[melhor] - alvo))
5207
+ melhor = i;
5208
+ }
5209
+ setHoverIdx(melhor);
5210
+ },
5211
+ onMouseLeave: () => setHoverIdx(null),
5189
5212
  children: [
5190
- /* @__PURE__ */ jsx(
5191
- "line",
5213
+ /* @__PURE__ */ jsxs(
5214
+ "svg",
5192
5215
  {
5193
- x1: PAD.left,
5194
- y1: height - PAD.bottom,
5195
- x2: viewW - PAD.right,
5196
- y2: height - PAD.bottom,
5197
- className: "stroke-border",
5198
- strokeWidth: 1
5216
+ ref: setSvgNode,
5217
+ viewBox: `0 0 ${viewW} ${height}`,
5218
+ preserveAspectRatio: "none",
5219
+ role: "img",
5220
+ "aria-label": `${title} trend`,
5221
+ className: "w-full",
5222
+ style: { height },
5223
+ children: [
5224
+ /* @__PURE__ */ jsx(
5225
+ "line",
5226
+ {
5227
+ x1: PAD.left,
5228
+ y1: height - padBottom,
5229
+ x2: viewW - PAD.right,
5230
+ y2: height - padBottom,
5231
+ className: "stroke-border",
5232
+ strokeWidth: 1
5233
+ }
5234
+ ),
5235
+ /* @__PURE__ */ jsx(
5236
+ "line",
5237
+ {
5238
+ x1: PAD.left,
5239
+ y1: PAD.top,
5240
+ x2: viewW - PAD.right,
5241
+ y2: PAD.top,
5242
+ className: "stroke-border/50",
5243
+ strokeWidth: 1,
5244
+ strokeDasharray: "2 3"
5245
+ }
5246
+ ),
5247
+ /* @__PURE__ */ jsx("text", { x: 4, y: PAD.top + 8, className: "fill-muted-foreground text-[10px]", children: valueFormatter(yMax) }),
5248
+ /* @__PURE__ */ jsx("text", { x: 4, y: height - padBottom, className: "fill-muted-foreground text-[10px]", children: "0" }),
5249
+ xFormatter ? xTicks(axisXs, viewW).map((x, i, todos) => /* @__PURE__ */ jsx(
5250
+ "text",
5251
+ {
5252
+ "data-slot": "trend-chart-xtick",
5253
+ x: xScale(x),
5254
+ y: height - 2,
5255
+ textAnchor: i === 0 ? "start" : i === todos.length - 1 ? "end" : "middle",
5256
+ className: "fill-muted-foreground text-[10px]",
5257
+ children: xFormatter(x)
5258
+ },
5259
+ x
5260
+ )) : null,
5261
+ series.map((s) => /* @__PURE__ */ jsxs("g", { children: [
5262
+ /* @__PURE__ */ jsx(
5263
+ "path",
5264
+ {
5265
+ "data-slot": "trend-chart-line",
5266
+ "data-series": s.name,
5267
+ d: seriesPath(s.points, xScale, yScale),
5268
+ fill: "none",
5269
+ stroke: s.color,
5270
+ strokeWidth: 1.5,
5271
+ children: /* @__PURE__ */ jsx("title", { children: s.name })
5272
+ }
5273
+ ),
5274
+ markedPoints(s.points).map((p, i) => /* @__PURE__ */ jsx(
5275
+ "circle",
5276
+ {
5277
+ "data-slot": "trend-chart-dot",
5278
+ cx: xScale(p.x),
5279
+ cy: yScale(p.y),
5280
+ r: 4,
5281
+ fill: s.color,
5282
+ children: /* @__PURE__ */ jsx("title", { children: s.name })
5283
+ },
5284
+ `${s.name}-${p.x}-${i}`
5285
+ ))
5286
+ ] }, s.name)),
5287
+ hoverIdx !== null && axisXs[hoverIdx] !== void 0 ? /* @__PURE__ */ jsx(
5288
+ "line",
5289
+ {
5290
+ "data-slot": "trend-chart-crosshair",
5291
+ x1: xScale(axisXs[hoverIdx]),
5292
+ y1: PAD.top,
5293
+ x2: xScale(axisXs[hoverIdx]),
5294
+ y2: height - padBottom,
5295
+ className: "stroke-muted-foreground/40",
5296
+ strokeWidth: 1
5297
+ }
5298
+ ) : null
5299
+ ]
5199
5300
  }
5200
5301
  ),
5201
- /* @__PURE__ */ jsx(
5202
- "line",
5302
+ hoverIdx !== null && axisXs[hoverIdx] !== void 0 ? /* @__PURE__ */ jsxs(
5303
+ "div",
5203
5304
  {
5204
- x1: PAD.left,
5205
- y1: PAD.top,
5206
- x2: viewW - PAD.right,
5207
- y2: PAD.top,
5208
- className: "stroke-border/50",
5209
- strokeWidth: 1,
5210
- strokeDasharray: "2 3"
5211
- }
5212
- ),
5213
- /* @__PURE__ */ jsx("text", { x: 4, y: PAD.top + 8, className: "fill-muted-foreground text-[10px]", children: valueFormatter(yMax) }),
5214
- /* @__PURE__ */ jsx("text", { x: 4, y: height - PAD.bottom, className: "fill-muted-foreground text-[10px]", children: "0" }),
5215
- series.map((s) => /* @__PURE__ */ jsxs("g", { children: [
5216
- /* @__PURE__ */ jsx(
5217
- "path",
5218
- {
5219
- "data-slot": "trend-chart-line",
5220
- "data-series": s.name,
5221
- d: seriesPath(s.points, xScale, yScale),
5222
- fill: "none",
5223
- stroke: s.color,
5224
- strokeWidth: 1.5,
5225
- children: /* @__PURE__ */ jsx("title", { children: s.name })
5226
- }
5227
- ),
5228
- markedPoints(s.points).map((p, i) => /* @__PURE__ */ jsx(
5229
- "circle",
5230
- {
5231
- "data-slot": "trend-chart-dot",
5232
- cx: xScale(p.x),
5233
- cy: yScale(p.y),
5234
- r: 4,
5235
- fill: s.color,
5236
- children: /* @__PURE__ */ jsx("title", { children: s.name })
5305
+ "data-slot": "trend-chart-tooltip",
5306
+ "aria-hidden": "true",
5307
+ className: "pointer-events-none absolute top-0 z-10 rounded border bg-popover px-2 py-1 text-[10px] text-popover-foreground shadow",
5308
+ style: {
5309
+ left: `${xScale(axisXs[hoverIdx]) / viewW * 100}%`,
5310
+ transform: "translateX(-50%)"
5237
5311
  },
5238
- `${s.name}-${p.x}-${i}`
5239
- ))
5240
- ] }, s.name))
5312
+ children: [
5313
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: xFormatter ? xFormatter(axisXs[hoverIdx]) : `#${hoverIdx + 1}` }),
5314
+ series.map((s, si) => {
5315
+ const y = yByX[si]?.get(axisXs[hoverIdx]);
5316
+ return /* @__PURE__ */ jsxs("div", { className: "whitespace-nowrap", children: [
5317
+ /* @__PURE__ */ jsx(
5318
+ "span",
5319
+ {
5320
+ className: "mr-1 inline-block size-2 rounded-full align-middle",
5321
+ style: { background: s.color }
5322
+ }
5323
+ ),
5324
+ s.name,
5325
+ ": ",
5326
+ y !== void 0 && Number.isFinite(y) ? valueFormatter(y) : "\u2014"
5327
+ ] }, s.name);
5328
+ })
5329
+ ]
5330
+ }
5331
+ ) : null
5241
5332
  ]
5242
5333
  }
5243
5334
  ),
@@ -5265,11 +5356,11 @@ var TrendChart = forwardRef(
5265
5356
  " \u2014 data table"
5266
5357
  ] }),
5267
5358
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
5268
- /* @__PURE__ */ jsx("th", { children: "Point" }),
5359
+ /* @__PURE__ */ jsx("th", { children: xLabel ?? "Point" }),
5269
5360
  series.map((s) => /* @__PURE__ */ jsx("th", { children: s.name }, s.name))
5270
5361
  ] }) }),
5271
5362
  /* @__PURE__ */ jsx("tbody", { children: axisXs.map((x, i) => /* @__PURE__ */ jsxs("tr", { children: [
5272
- /* @__PURE__ */ jsx("td", { children: i + 1 }),
5363
+ /* @__PURE__ */ jsx("td", { children: xFormatter ? xFormatter(x) : i + 1 }),
5273
5364
  series.map((s, si) => {
5274
5365
  const y = yByX[si]?.get(x);
5275
5366
  return /* @__PURE__ */ jsx("td", { children: y !== void 0 && Number.isFinite(y) ? valueFormatter(y) : "\u2014" }, s.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usetheo/ui",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",