@usetheo/ui 0.33.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.
@@ -1 +1 @@
1
- export { linScale, niceMax, pontosComMarcador, seriesPath, TrendChart, type TrendChartProps, type TrendPoint, type TrendSeries, } from "./trend-chart.js";
1
+ export { linScale, markedPoints, niceMax, seriesPath, TrendChart, type TrendChartProps, type TrendPoint, type TrendSeries, } from "./trend-chart.js";
@@ -50,14 +50,26 @@ 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
  *
56
67
  * Duas razões distintas para marcar, e a segunda só apareceu quando séries com lacuna passaram a
57
68
  * existir (M144):
58
69
  *
59
- * 1. **Série esparsa** (< `SPARSE_MARKER_MAX` baldes) — 1-4 pontos leem como um penhasco dramático;
60
- * o marcador ancora o leitor no dado real. Regra do M76, preservada.
70
+ * 1. **Série esparsa** (< `SPARSE_MARKER_MAX` pontos FINITOS) — 1-4 pontos leem como um penhasco
71
+ * dramático; o marcador ancora o leitor no dado real. Regra do M76 — que passou a contar dado,
72
+ * não slot, porque a densificação do M144 desfez a equivalência entre os dois.
61
73
  * 2. **Ponto isolado** — um ponto finito cercado de lacunas dos dois lados vira um `M` sozinho no
62
74
  * `path`, e um `M` sozinho **desenha nada**: bounding-box zero, invisível. O dado existe, está
63
75
  * correto, e o operador não vê. Medido no e2e do M144 (`d="M419.86,92.00 M448.00,36.00"`).
@@ -71,7 +83,7 @@ export declare function seriesPath(points: TrendPoint[], xScale: (x: number) =>
71
83
  * Fora do JSX de propósito: é a aritmética da decisão, e aritmética isolada é aritmética que a
72
84
  * mutação alcança.
73
85
  */
74
- export declare function pontosComMarcador(points: TrendPoint[]): TrendPoint[];
86
+ export declare function markedPoints(points: TrendPoint[]): TrendPoint[];
75
87
  export interface TrendChartProps extends HTMLAttributes<HTMLElement> {
76
88
  title: string;
77
89
  series: TrendSeries[];
@@ -85,6 +97,20 @@ export interface TrendChartProps extends HTMLAttributes<HTMLElement> {
85
97
  * Ignored when ≤ 0. Default: auto (`niceMax`).
86
98
  */
87
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;
88
114
  }
89
115
  declare const TrendChart: import("react").ForwardRefExoticComponent<TrendChartProps & import("react").RefAttributes<HTMLElement>>;
90
116
  export { TrendChart };
package/dist/index.js CHANGED
@@ -5109,10 +5109,22 @@ 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
- function pontosComMarcador(points) {
5124
+ function markedPoints(points) {
5114
5125
  const finito = (p) => Number.isFinite(p?.y);
5115
- if (points.length < SPARSE_MARKER_MAX) return points.filter(finito);
5126
+ const finitos = points.filter(finito);
5127
+ if (finitos.length < SPARSE_MARKER_MAX) return finitos;
5116
5128
  return points.filter((p, i) => finito(p) && !finito(points[i - 1]) && !finito(points[i + 1]));
5117
5129
  }
5118
5130
  var TrendChart = forwardRef(
@@ -5122,10 +5134,13 @@ var TrendChart = forwardRef(
5122
5134
  height = 180,
5123
5135
  valueFormatter = (v) => String(v),
5124
5136
  yMax: yMaxProp,
5137
+ xFormatter,
5138
+ xLabel,
5125
5139
  className,
5126
5140
  ...props
5127
5141
  }, ref) => {
5128
5142
  const [viewW, setViewW] = useState(VIEW_W);
5143
+ const [hoverIdx, setHoverIdx] = useState(null);
5129
5144
  const roRef = useRef(null);
5130
5145
  const setSvgNode = useCallback((node) => {
5131
5146
  roRef.current?.disconnect();
@@ -5171,72 +5186,149 @@ var TrendChart = forwardRef(
5171
5186
  const yMax = yMaxProp != null && yMaxProp > 0 ? yMaxProp : niceMax(allPoints.map((p) => p.y)) || 1;
5172
5187
  const axisXs = Array.from(new Set(allPoints.map((p) => p.x))).sort((a, b) => a - b);
5173
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);
5174
5190
  const xScale = linScale([xMin, xMax], [PAD.left, viewW - PAD.right]);
5175
- const yScale = linScale([0, yMax], [height - PAD.bottom, PAD.top]);
5191
+ const yScale = linScale([0, yMax], [height - padBottom, PAD.top]);
5176
5192
  return /* @__PURE__ */ jsxs("figure", { "data-slot": "trend-chart", ref, className: cn("space-y-1", className), ...props, children: [
5177
5193
  /* @__PURE__ */ jsx("figcaption", { className: "font-semibold text-label-caps text-muted-foreground uppercase", children: title }),
5178
5194
  /* @__PURE__ */ jsxs(
5179
- "svg",
5195
+ "div",
5180
5196
  {
5181
- ref: setSvgNode,
5182
- viewBox: `0 0 ${viewW} ${height}`,
5183
- preserveAspectRatio: "none",
5184
- role: "img",
5185
- "aria-label": `${title} trend`,
5186
- className: "w-full",
5187
- 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),
5188
5212
  children: [
5189
- /* @__PURE__ */ jsx(
5190
- "line",
5213
+ /* @__PURE__ */ jsxs(
5214
+ "svg",
5191
5215
  {
5192
- x1: PAD.left,
5193
- y1: height - PAD.bottom,
5194
- x2: viewW - PAD.right,
5195
- y2: height - PAD.bottom,
5196
- className: "stroke-border",
5197
- 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
+ ]
5198
5300
  }
5199
5301
  ),
5200
- /* @__PURE__ */ jsx(
5201
- "line",
5302
+ hoverIdx !== null && axisXs[hoverIdx] !== void 0 ? /* @__PURE__ */ jsxs(
5303
+ "div",
5202
5304
  {
5203
- x1: PAD.left,
5204
- y1: PAD.top,
5205
- x2: viewW - PAD.right,
5206
- y2: PAD.top,
5207
- className: "stroke-border/50",
5208
- strokeWidth: 1,
5209
- strokeDasharray: "2 3"
5210
- }
5211
- ),
5212
- /* @__PURE__ */ jsx("text", { x: 4, y: PAD.top + 8, className: "fill-muted-foreground text-[10px]", children: valueFormatter(yMax) }),
5213
- /* @__PURE__ */ jsx("text", { x: 4, y: height - PAD.bottom, className: "fill-muted-foreground text-[10px]", children: "0" }),
5214
- series.map((s) => /* @__PURE__ */ jsxs("g", { children: [
5215
- /* @__PURE__ */ jsx(
5216
- "path",
5217
- {
5218
- "data-slot": "trend-chart-line",
5219
- "data-series": s.name,
5220
- d: seriesPath(s.points, xScale, yScale),
5221
- fill: "none",
5222
- stroke: s.color,
5223
- strokeWidth: 1.5,
5224
- children: /* @__PURE__ */ jsx("title", { children: s.name })
5225
- }
5226
- ),
5227
- pontosComMarcador(s.points).map((p, i) => /* @__PURE__ */ jsx(
5228
- "circle",
5229
- {
5230
- "data-slot": "trend-chart-dot",
5231
- cx: xScale(p.x),
5232
- cy: yScale(p.y),
5233
- r: 2.5,
5234
- fill: s.color,
5235
- 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%)"
5236
5311
  },
5237
- `${s.name}-${p.x}-${i}`
5238
- ))
5239
- ] }, 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
5240
5332
  ]
5241
5333
  }
5242
5334
  ),
@@ -5264,11 +5356,11 @@ var TrendChart = forwardRef(
5264
5356
  " \u2014 data table"
5265
5357
  ] }),
5266
5358
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
5267
- /* @__PURE__ */ jsx("th", { children: "Point" }),
5359
+ /* @__PURE__ */ jsx("th", { children: xLabel ?? "Point" }),
5268
5360
  series.map((s) => /* @__PURE__ */ jsx("th", { children: s.name }, s.name))
5269
5361
  ] }) }),
5270
5362
  /* @__PURE__ */ jsx("tbody", { children: axisXs.map((x, i) => /* @__PURE__ */ jsxs("tr", { children: [
5271
- /* @__PURE__ */ jsx("td", { children: i + 1 }),
5363
+ /* @__PURE__ */ jsx("td", { children: xFormatter ? xFormatter(x) : i + 1 }),
5272
5364
  series.map((s, si) => {
5273
5365
  const y = yByX[si]?.get(x);
5274
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.33.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",