@1doc/1ds-react 0.0.1 → 2.7.1
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/CHANGELOG.md +667 -0
- package/README.md +138 -6
- package/package.json +43 -10
- package/src/index.ts +161 -16
- package/src/teste/AttachmentCard.test.tsx +161 -0
- package/src/teste/Chart.test.tsx +233 -0
- package/src/teste/Combobox.test.tsx +251 -0
- package/src/teste/DataTable.test.tsx +70 -0
- package/src/teste/DatePicker.test.tsx +86 -0
- package/src/teste/Drawer.test.tsx +265 -0
- package/src/teste/Dropdown.test.tsx +217 -0
- package/src/teste/Input.test.tsx +64 -0
- package/src/teste/Kanban.test.tsx +333 -0
- package/src/teste/MultiSelect.test.tsx +73 -0
- package/src/teste/Podium.test.tsx +107 -0
- package/src/teste/Popover.test.tsx +221 -0
- package/src/teste/SegmentedControl.test.tsx +183 -0
- package/src/teste/Timeline.test.tsx +100 -0
- package/src/teste/Tree.test.tsx +74 -0
- package/src/teste/setup.ts +7 -0
- package/src/utils/bandeiras.ts +22 -0
- package/src/utils/chart.ts +682 -0
- package/src/utils/density.ts +27 -0
- package/src/utils/overlay-stack.ts +26 -0
- package/src/utils/theme.ts +63 -47
- package/src/utils/toast.ts +89 -0
- package/vitest.config.ts +28 -0
- package/dist/components/Alert/Alert.css +0 -188
- package/dist/components/Alert/Alert.d.ts +0 -65
- package/dist/components/Alert/Alert.js +0 -56
- package/dist/components/Avatar/Avatar.css +0 -88
- package/dist/components/Avatar/Avatar.d.ts +0 -34
- package/dist/components/Avatar/Avatar.js +0 -16
- package/dist/components/Avatar/AvatarGroup.d.ts +0 -12
- package/dist/components/Avatar/AvatarGroup.js +0 -8
- package/dist/components/Badge/Badge.css +0 -155
- package/dist/components/Badge/Badge.d.ts +0 -46
- package/dist/components/Badge/Badge.js +0 -17
- package/dist/components/Button/Button.css +0 -199
- package/dist/components/Button/Button.d.ts +0 -23
- package/dist/components/Button/Button.js +0 -62
- package/dist/components/Checkbox/Checkbox.css +0 -122
- package/dist/components/Checkbox/Checkbox.d.ts +0 -21
- package/dist/components/Checkbox/Checkbox.js +0 -49
- package/dist/components/Divider/Divider.css +0 -81
- package/dist/components/Divider/Divider.d.ts +0 -18
- package/dist/components/Divider/Divider.js +0 -26
- package/dist/components/Grid/Grid.css +0 -154
- package/dist/components/Grid/Grid.d.ts +0 -21
- package/dist/components/Grid/Grid.js +0 -53
- package/dist/components/Icon/Icon.css +0 -41
- package/dist/components/Icon/Icon.d.ts +0 -12
- package/dist/components/Icon/Icon.js +0 -29
- package/dist/components/Input/Input.css +0 -190
- package/dist/components/Input/Input.d.ts +0 -32
- package/dist/components/Input/Input.js +0 -58
- package/dist/components/Link/Link.css +0 -76
- package/dist/components/Link/Link.d.ts +0 -15
- package/dist/components/Link/Link.js +0 -19
- package/dist/components/Logo/Logo.css +0 -32
- package/dist/components/Logo/Logo.d.ts +0 -10
- package/dist/components/Logo/Logo.js +0 -16
- package/dist/components/Modal/Modal.css +0 -155
- package/dist/components/Modal/Modal.d.ts +0 -36
- package/dist/components/Modal/Modal.js +0 -55
- package/dist/components/Radio/Radio.css +0 -114
- package/dist/components/Radio/Radio.d.ts +0 -19
- package/dist/components/Radio/Radio.js +0 -39
- package/dist/components/Select/Select.css +0 -291
- package/dist/components/Select/Select.d.ts +0 -38
- package/dist/components/Select/Select.js +0 -246
- package/dist/components/Spinner/Spinner.css +0 -60
- package/dist/components/Spinner/Spinner.d.ts +0 -16
- package/dist/components/Spinner/Spinner.js +0 -16
- package/dist/components/Switch/Switch.css +0 -127
- package/dist/components/Switch/Switch.d.ts +0 -18
- package/dist/components/Switch/Switch.js +0 -57
- package/dist/components/Tabs/Tab.d.ts +0 -19
- package/dist/components/Tabs/Tab.js +0 -76
- package/dist/components/Tabs/TabContent.d.ts +0 -15
- package/dist/components/Tabs/TabContent.js +0 -11
- package/dist/components/Tabs/Tabs.css +0 -124
- package/dist/components/Tabs/Tabs.d.ts +0 -20
- package/dist/components/Tabs/Tabs.js +0 -14
- package/dist/components/Tabs/TabsContent.d.ts +0 -16
- package/dist/components/Tabs/TabsContent.js +0 -12
- package/dist/components/Tabs/tabs.context.d.ts +0 -2
- package/dist/components/Tabs/tabs.context.js +0 -2
- package/dist/components/Tag/Tag.css +0 -140
- package/dist/components/Tag/Tag.d.ts +0 -63
- package/dist/components/Tag/Tag.js +0 -48
- package/dist/components/Text/Text.css +0 -1213
- package/dist/components/Text/Text.d.ts +0 -70
- package/dist/components/Text/Text.js +0 -89
- package/dist/components/Textarea/Textarea.css +0 -133
- package/dist/components/Textarea/Textarea.d.ts +0 -28
- package/dist/components/Textarea/Textarea.js +0 -72
- package/dist/index.d.ts +0 -25
- package/dist/index.js +0 -27
- package/dist/utils/theme.d.ts +0 -30
- package/dist/utils/theme.js +0 -106
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geometria dos gráficos do 1DS (BarChart, LineChart, DonutChart).
|
|
3
|
+
*
|
|
4
|
+
* Módulo puro, sem DOM: recebe dados e medidas, devolve coordenadas prontas
|
|
5
|
+
* para o SVG. Mora fora dos `.lite.tsx` por dois motivos:
|
|
6
|
+
*
|
|
7
|
+
* - o Mitosis não aceita função em escopo de módulo dentro de um `.lite.tsx`
|
|
8
|
+
* (tenta ler a declaração como JSON5 e explode);
|
|
9
|
+
* - os três gráficos compartilham a escala, o formatador e a medição de
|
|
10
|
+
* texto — cópia em três arquivos seria três lugares para corrigir.
|
|
11
|
+
*
|
|
12
|
+
* Nada aqui escolhe cor. A cor de cada série é uma classe CSS
|
|
13
|
+
* (`umds-chart__serie--N`) que aponta para um token semântico — é assim que o
|
|
14
|
+
* gráfico acompanha o tema escuro sem nenhuma linha de JavaScript.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Um ponto de uma série única: rótulo da categoria e valor. */
|
|
18
|
+
export interface ChartPonto {
|
|
19
|
+
label: string;
|
|
20
|
+
value: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Uma série nomeada, com um valor por categoria. */
|
|
24
|
+
export interface ChartSerieBruta {
|
|
25
|
+
name: string;
|
|
26
|
+
values: Array<number | null>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Quantas séries (ou fatias) cabem com cor própria.
|
|
31
|
+
*
|
|
32
|
+
* O DS não tem paleta categórica: as cinco cores saem dos tokens semânticos
|
|
33
|
+
* que já existem e que passaram no validador de daltonismo nos temas 1doc e
|
|
34
|
+
* 1doc-dark. Uma sexta série não ganha cor gerada — gráfico com mais séries
|
|
35
|
+
* pede small multiples ou agrupamento em "Outros".
|
|
36
|
+
*/
|
|
37
|
+
export const CHART_MAX_SERIES = 5;
|
|
38
|
+
|
|
39
|
+
/** Altura de linha do texto dos eixos (12px da escala oficial). */
|
|
40
|
+
const FONTE_EIXO = 12;
|
|
41
|
+
/** Largura média de um caractere de Open Sans a 12px, com folga. */
|
|
42
|
+
const LARGURA_CARACTERE = 7;
|
|
43
|
+
export interface ChartFormato {
|
|
44
|
+
valueFormatter?: (value: number) => string;
|
|
45
|
+
unit?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Formata um valor para leitura: formatador do consumidor ou pt-BR + unidade. */
|
|
49
|
+
export function formatarValor(valor: number | null | undefined, formato: ChartFormato): string {
|
|
50
|
+
if (valor === null || valor === undefined || Number.isNaN(valor)) return "—";
|
|
51
|
+
if (typeof formato.valueFormatter === "function") return formato.valueFormatter(valor);
|
|
52
|
+
const numero = valor.toLocaleString("pt-BR", {
|
|
53
|
+
maximumFractionDigits: 2
|
|
54
|
+
});
|
|
55
|
+
const unidade = formato.unit || "";
|
|
56
|
+
if (!unidade) return numero;
|
|
57
|
+
// "%" encosta no número (12%); qualquer outra unidade leva espaço (12 dias).
|
|
58
|
+
return unidade === "%" ? `${numero}%` : `${numero} ${unidade}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* O mesmo valor, para leitor de tela: "—" é lido como "travessão" (ou nada),
|
|
63
|
+
* então ausência de dado vira "sem dado".
|
|
64
|
+
*/
|
|
65
|
+
export function valorAcessivel(valor: number | null | undefined, formato: ChartFormato): string {
|
|
66
|
+
if (valor === null || valor === undefined || Number.isNaN(valor)) return "sem dado";
|
|
67
|
+
return formatarValor(valor, formato);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Estimativa da largura de um texto de eixo, sem medir no DOM. */
|
|
71
|
+
export function larguraDoTexto(texto: string): number {
|
|
72
|
+
return (texto || "").length * LARGURA_CARACTERE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Arredonda o erro de ponto flutuante de somas como 0,1 + 0,2. */
|
|
76
|
+
function limpar(numero: number): number {
|
|
77
|
+
return Math.round(numero * 1e9) / 1e9;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Passo "redondo" (1, 2, 2,5, 5 × 10^n) mais próximo acima do bruto. */
|
|
81
|
+
function passoRedondo(bruto: number): number {
|
|
82
|
+
if (bruto <= 0 || !Number.isFinite(bruto)) return 1;
|
|
83
|
+
const expoente = Math.floor(Math.log10(bruto));
|
|
84
|
+
const base = Math.pow(10, expoente);
|
|
85
|
+
const fracao = bruto / base;
|
|
86
|
+
const redondo = fracao <= 1 ? 1 : fracao <= 2 ? 2 : fracao <= 2.5 ? 2.5 : fracao <= 5 ? 5 : 10;
|
|
87
|
+
return redondo * base;
|
|
88
|
+
}
|
|
89
|
+
export interface ChartEscala {
|
|
90
|
+
min: number;
|
|
91
|
+
max: number;
|
|
92
|
+
ticks: number[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Escala do eixo Y com números redondos, sempre incluindo o zero: barra que
|
|
97
|
+
* não nasce do zero mente sobre a proporção.
|
|
98
|
+
*/
|
|
99
|
+
export function escalaRedonda(valores: number[], divisoes = 4): ChartEscala {
|
|
100
|
+
let min = 0;
|
|
101
|
+
let max = 0;
|
|
102
|
+
for (const v of valores) {
|
|
103
|
+
if (v < min) min = v;
|
|
104
|
+
if (v > max) max = v;
|
|
105
|
+
}
|
|
106
|
+
if (min === max) max = min + 1;
|
|
107
|
+
const passo = passoRedondo((max - min) / divisoes);
|
|
108
|
+
const inicio = Math.floor(limpar(min / passo)) * passo;
|
|
109
|
+
const fim = Math.ceil(limpar(max / passo)) * passo;
|
|
110
|
+
const ticks: number[] = [];
|
|
111
|
+
for (let t = inicio; t <= fim + passo / 2; t += passo) ticks.push(limpar(t));
|
|
112
|
+
return {
|
|
113
|
+
min: limpar(inicio),
|
|
114
|
+
max: limpar(fim),
|
|
115
|
+
ticks
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export interface ChartDados {
|
|
119
|
+
categorias: string[];
|
|
120
|
+
series: ChartSerieBruta[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Junta as duas formas de entrada numa só: `data` (série única de
|
|
125
|
+
* `{ label, value }`) ou `series` + `labels`. Corta em CHART_MAX_SERIES.
|
|
126
|
+
*/
|
|
127
|
+
export function normalizarDados(data: ChartPonto[] | undefined, series: ChartSerieBruta[] | undefined, labels: string[] | undefined, nomePadrao: string): ChartDados {
|
|
128
|
+
if (series && series.length > 0) {
|
|
129
|
+
const cortadas = series.slice(0, CHART_MAX_SERIES);
|
|
130
|
+
let n = 0;
|
|
131
|
+
for (const s of cortadas) n = Math.max(n, (s.values || []).length);
|
|
132
|
+
const categorias: string[] = [];
|
|
133
|
+
for (let i = 0; i < n; i++) {
|
|
134
|
+
categorias.push(labels && labels[i] !== undefined ? String(labels[i]) : String(i + 1));
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
categorias,
|
|
138
|
+
series: cortadas.map(s => ({
|
|
139
|
+
name: s.name,
|
|
140
|
+
values: categorias.map((_, i) => {
|
|
141
|
+
const v = (s.values || [])[i];
|
|
142
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
143
|
+
})
|
|
144
|
+
}))
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const pontos = data || [];
|
|
148
|
+
return {
|
|
149
|
+
categorias: pontos.map(p => String(p.label)),
|
|
150
|
+
series: pontos.length ? [{
|
|
151
|
+
name: nomePadrao,
|
|
152
|
+
values: pontos.map(p => typeof p.value === "number" && Number.isFinite(p.value) ? p.value : null)
|
|
153
|
+
}] : []
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Existe ao menos um número para desenhar? */
|
|
158
|
+
export function temValores(dados: ChartDados): boolean {
|
|
159
|
+
return dados.series.some(s => s.values.some(v => v !== null));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Plano cartesiano (barras e linhas) ───────────────────────────────
|
|
163
|
+
|
|
164
|
+
export interface ChartTick {
|
|
165
|
+
y: number;
|
|
166
|
+
texto: string;
|
|
167
|
+
}
|
|
168
|
+
export interface ChartRotuloX {
|
|
169
|
+
x: number;
|
|
170
|
+
texto: string;
|
|
171
|
+
}
|
|
172
|
+
export interface ChartLinhaDoTooltip {
|
|
173
|
+
serie: number;
|
|
174
|
+
nome: string;
|
|
175
|
+
texto: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Área de interação de uma categoria: posição em % e o que o tooltip lê. */
|
|
179
|
+
export interface ChartAlvo {
|
|
180
|
+
indice: number;
|
|
181
|
+
categoria: string;
|
|
182
|
+
esquerda: string;
|
|
183
|
+
largura: string;
|
|
184
|
+
lado: "direita" | "esquerda";
|
|
185
|
+
descricao: string;
|
|
186
|
+
linhas: ChartLinhaDoTooltip[];
|
|
187
|
+
}
|
|
188
|
+
export interface ChartPlano {
|
|
189
|
+
largura: number;
|
|
190
|
+
altura: number;
|
|
191
|
+
topo: number;
|
|
192
|
+
base: number;
|
|
193
|
+
esquerda: number;
|
|
194
|
+
direita: number;
|
|
195
|
+
banda: number;
|
|
196
|
+
y0: number;
|
|
197
|
+
ticks: ChartTick[];
|
|
198
|
+
rotulosX: ChartRotuloX[];
|
|
199
|
+
alvos: ChartAlvo[];
|
|
200
|
+
escalaY: (valor: number) => number;
|
|
201
|
+
centroX: (indice: number) => number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Monta o plano: margens medidas pelo texto dos ticks, escala Y redonda,
|
|
206
|
+
* rótulos do X rareados quando não cabem, e uma área de interação por
|
|
207
|
+
* categoria.
|
|
208
|
+
*/
|
|
209
|
+
export function montarPlano(dados: ChartDados, largura: number, altura: number, formato: ChartFormato): ChartPlano {
|
|
210
|
+
const valores: number[] = [];
|
|
211
|
+
for (const s of dados.series) for (const v of s.values) if (v !== null) valores.push(v);
|
|
212
|
+
const escala = escalaRedonda(valores);
|
|
213
|
+
const textosTick = escala.ticks.map(t => formatarValor(t, formato));
|
|
214
|
+
let maiorTick = 0;
|
|
215
|
+
for (const t of textosTick) maiorTick = Math.max(maiorTick, larguraDoTexto(t));
|
|
216
|
+
const topo = FONTE_EIXO / 2 + 2;
|
|
217
|
+
const esquerda = Math.ceil(maiorTick) + 8;
|
|
218
|
+
const n = Math.max(1, dados.categorias.length);
|
|
219
|
+
// O último rótulo do X é centrado na última banda e pode passar da borda
|
|
220
|
+
// direita; a margem cresce só o que ele passar.
|
|
221
|
+
const bandaPrevia = Math.max(1, largura - esquerda - 8) / n;
|
|
222
|
+
const ultimo = larguraDoTexto(dados.categorias[dados.categorias.length - 1] || "");
|
|
223
|
+
const direita = Math.max(8, Math.ceil(ultimo / 2 - bandaPrevia / 2) + 2);
|
|
224
|
+
const base = altura - (FONTE_EIXO + 12);
|
|
225
|
+
const larguraUtil = Math.max(1, largura - esquerda - direita);
|
|
226
|
+
const banda = larguraUtil / n;
|
|
227
|
+
const escalaY = (valor: number) => topo + (escala.max - valor) / (escala.max - escala.min) * (base - topo);
|
|
228
|
+
const centroX = (indice: number) => esquerda + banda * indice + banda / 2;
|
|
229
|
+
const ticks = escala.ticks.map((t, i) => ({
|
|
230
|
+
y: escalaY(t),
|
|
231
|
+
texto: textosTick[i]
|
|
232
|
+
}));
|
|
233
|
+
let maiorRotulo = 0;
|
|
234
|
+
for (const c of dados.categorias) maiorRotulo = Math.max(maiorRotulo, larguraDoTexto(c));
|
|
235
|
+
const pulo = Math.max(1, Math.ceil((maiorRotulo + 8) / banda));
|
|
236
|
+
const rotulosX: ChartRotuloX[] = [];
|
|
237
|
+
dados.categorias.forEach((c, i) => {
|
|
238
|
+
if (i % pulo === 0) rotulosX.push({
|
|
239
|
+
x: centroX(i),
|
|
240
|
+
texto: c
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
const alvos: ChartAlvo[] = dados.categorias.map((categoria, i) => {
|
|
244
|
+
const linhas = dados.series.map((s, j) => ({
|
|
245
|
+
serie: j + 1,
|
|
246
|
+
nome: s.name,
|
|
247
|
+
texto: formatarValor(s.values[i], formato)
|
|
248
|
+
}));
|
|
249
|
+
const partes = dados.series.map(s => {
|
|
250
|
+
const texto = valorAcessivel(s.values[i], formato);
|
|
251
|
+
return dados.series.length > 1 ? `${s.name}: ${texto}` : texto;
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
indice: i,
|
|
255
|
+
categoria,
|
|
256
|
+
esquerda: `${(esquerda + banda * i) / largura * 100}%`,
|
|
257
|
+
largura: `${banda / largura * 100}%`,
|
|
258
|
+
lado: i < n / 2 ? "direita" : "esquerda",
|
|
259
|
+
descricao: `${categoria}: ${partes.join("; ")}`,
|
|
260
|
+
linhas
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
largura,
|
|
265
|
+
altura,
|
|
266
|
+
topo,
|
|
267
|
+
base,
|
|
268
|
+
esquerda,
|
|
269
|
+
direita,
|
|
270
|
+
banda,
|
|
271
|
+
y0: escalaY(0),
|
|
272
|
+
ticks,
|
|
273
|
+
rotulosX,
|
|
274
|
+
alvos,
|
|
275
|
+
escalaY,
|
|
276
|
+
centroX
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Raio da ponta da barra, em px. Espelha `--umds-primitive-border-radius-sm`
|
|
282
|
+
* (8px), o raio universal do DS. Precisa ser número porque o arredondamento
|
|
283
|
+
* é geometria do caminho SVG, calculada aqui — não é um token novo.
|
|
284
|
+
*/
|
|
285
|
+
export const RAIO_DA_BARRA = 8;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Caminho de uma barra: quadrada na linha de base, arredondada na ponta do
|
|
289
|
+
* dado com o raio canônico. Valor negativo arredonda embaixo. O raio nunca
|
|
290
|
+
* passa de metade da largura (barra fina não vira pílula) nem de metade da
|
|
291
|
+
* altura (barra curta não estoura a linha de base).
|
|
292
|
+
*/
|
|
293
|
+
export function caminhoDaBarra(x: number, largura: number, y0: number, y: number): string {
|
|
294
|
+
const altura = Math.abs(y0 - y);
|
|
295
|
+
if (altura < 0.5 || largura <= 0) return "";
|
|
296
|
+
const r = Math.min(RAIO_DA_BARRA, largura / 2, altura / 2);
|
|
297
|
+
const x1 = x + largura;
|
|
298
|
+
if (y <= y0) {
|
|
299
|
+
return `M${x},${y0}V${y + r}A${r},${r} 0 0 1 ${x + r},${y}H${x1 - r}A${r},${r} 0 0 1 ${x1},${y + r}V${y0}Z`;
|
|
300
|
+
}
|
|
301
|
+
return `M${x},${y0}V${y - r}A${r},${r} 0 0 0 ${x + r},${y}H${x1 - r}A${r},${r} 0 0 0 ${x1},${y - r}V${y0}Z`;
|
|
302
|
+
}
|
|
303
|
+
export interface ChartBarra {
|
|
304
|
+
chave: string;
|
|
305
|
+
serie: number;
|
|
306
|
+
indice: number;
|
|
307
|
+
d: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Barras verticais, agrupadas lado a lado quando há mais de uma série. */
|
|
311
|
+
export function montarBarras(dados: ChartDados, plano: ChartPlano): ChartBarra[] {
|
|
312
|
+
const m = Math.max(1, dados.series.length);
|
|
313
|
+
const vao = 2;
|
|
314
|
+
const espessura = Math.max(2, Math.min(24, (plano.banda * 0.72 - vao * (m - 1)) / m));
|
|
315
|
+
const grupo = espessura * m + vao * (m - 1);
|
|
316
|
+
const barras: ChartBarra[] = [];
|
|
317
|
+
dados.categorias.forEach((_, i) => {
|
|
318
|
+
const inicio = plano.esquerda + plano.banda * i + (plano.banda - grupo) / 2;
|
|
319
|
+
dados.series.forEach((s, j) => {
|
|
320
|
+
const v = s.values[i];
|
|
321
|
+
if (v === null) return;
|
|
322
|
+
const d = caminhoDaBarra(inicio + j * (espessura + vao), espessura, plano.y0, plano.escalaY(v));
|
|
323
|
+
if (d) barras.push({
|
|
324
|
+
chave: `${j}-${i}`,
|
|
325
|
+
serie: j + 1,
|
|
326
|
+
indice: i,
|
|
327
|
+
d
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
return barras;
|
|
332
|
+
}
|
|
333
|
+
export interface ChartPontoDaLinha {
|
|
334
|
+
chave: string;
|
|
335
|
+
serie: number;
|
|
336
|
+
indice: number;
|
|
337
|
+
x: number;
|
|
338
|
+
y: number;
|
|
339
|
+
}
|
|
340
|
+
export interface ChartTraco {
|
|
341
|
+
chave: string;
|
|
342
|
+
serie: number;
|
|
343
|
+
d: string;
|
|
344
|
+
pontos: ChartPontoDaLinha[];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Uma linha por série; valor ausente (`null`) interrompe o traço. */
|
|
348
|
+
export function montarLinhas(dados: ChartDados, plano: ChartPlano): ChartTraco[] {
|
|
349
|
+
return dados.series.map((s, j) => {
|
|
350
|
+
let d = "";
|
|
351
|
+
let caneta = false;
|
|
352
|
+
const pontos: ChartPontoDaLinha[] = [];
|
|
353
|
+
s.values.forEach((v, i) => {
|
|
354
|
+
if (v === null) {
|
|
355
|
+
caneta = false;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const x = plano.centroX(i);
|
|
359
|
+
const y = plano.escalaY(v);
|
|
360
|
+
d += `${caneta ? "L" : "M"}${x},${y}`;
|
|
361
|
+
caneta = true;
|
|
362
|
+
pontos.push({
|
|
363
|
+
chave: `${j}-${i}`,
|
|
364
|
+
serie: j + 1,
|
|
365
|
+
indice: i,
|
|
366
|
+
x,
|
|
367
|
+
y
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
chave: String(j),
|
|
372
|
+
serie: j + 1,
|
|
373
|
+
d,
|
|
374
|
+
pontos
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Frase curta para o `aria-label` do gráfico: o que é e a faixa de valores. */
|
|
380
|
+
export function resumoCartesiano(tipo: string, dados: ChartDados, formato: ChartFormato): string {
|
|
381
|
+
let min = Infinity;
|
|
382
|
+
let max = -Infinity;
|
|
383
|
+
for (const s of dados.series) for (const v of s.values) if (v !== null) {
|
|
384
|
+
if (v < min) min = v;
|
|
385
|
+
if (v > max) max = v;
|
|
386
|
+
}
|
|
387
|
+
const n = dados.categorias.length;
|
|
388
|
+
const m = dados.series.length;
|
|
389
|
+
const series = m > 1 ? `, ${m} séries (${dados.series.map(s => s.name).join(", ")})` : "";
|
|
390
|
+
const faixa = min === Infinity ? "" : ` Valores de ${formatarValor(min, formato)} a ${formatarValor(max, formato)}.`;
|
|
391
|
+
return `${tipo} com ${n} ${n === 1 ? "categoria" : "categorias"}${series}.${faixa}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Rosca ────────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
export interface ChartFatia {
|
|
397
|
+
chave: string;
|
|
398
|
+
/** Posição da cor (1 a CHART_MAX_SERIES); 0 é a fatia "Outros", em neutro. */
|
|
399
|
+
serie: number;
|
|
400
|
+
indice: number;
|
|
401
|
+
nome: string;
|
|
402
|
+
valor: number;
|
|
403
|
+
texto: string;
|
|
404
|
+
percentual: string;
|
|
405
|
+
d: string;
|
|
406
|
+
}
|
|
407
|
+
export interface ChartRosca {
|
|
408
|
+
total: number;
|
|
409
|
+
fatias: ChartFatia[];
|
|
410
|
+
}
|
|
411
|
+
function pontoNoCirculo(c: number, r: number, angulo: number): string {
|
|
412
|
+
return `${c + r * Math.cos(angulo)},${c + r * Math.sin(angulo)}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Setor de anel entre dois ângulos (radianos, zero às 12h, sentido horário). */
|
|
416
|
+
export function caminhoDoSetor(c: number, raio: number, interno: number, a0: number, a1: number): string {
|
|
417
|
+
// Um setor de 360° degenera (início = fim); desenha como duas metades.
|
|
418
|
+
if (a1 - a0 >= Math.PI * 2 - 1e-6) {
|
|
419
|
+
const meio = a0 + Math.PI;
|
|
420
|
+
return caminhoDoSetor(c, raio, interno, a0, meio) + caminhoDoSetor(c, raio, interno, meio, a1);
|
|
421
|
+
}
|
|
422
|
+
const g0 = a0 - Math.PI / 2;
|
|
423
|
+
const g1 = a1 - Math.PI / 2;
|
|
424
|
+
const grande = a1 - a0 > Math.PI ? 1 : 0;
|
|
425
|
+
return `M${pontoNoCirculo(c, raio, g0)}` + `A${raio},${raio} 0 ${grande} 1 ${pontoNoCirculo(c, raio, g1)}` + `L${pontoNoCirculo(c, interno, g1)}` + `A${interno},${interno} 0 ${grande} 0 ${pontoNoCirculo(c, interno, g0)}Z`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Fatias da rosca. Mais que CHART_MAX_SERIES partes: as primeiras ficam e o
|
|
430
|
+
* resto vira uma fatia só, `rotuloOutros`. Valor negativo ou ausente conta
|
|
431
|
+
* como zero — parte de um todo não pode ser negativa.
|
|
432
|
+
*/
|
|
433
|
+
export function montarRosca(data: ChartPonto[] | undefined, tamanho: number, formato: ChartFormato, rotuloOutros: string): ChartRosca {
|
|
434
|
+
const limpos = (data || []).map(p => ({
|
|
435
|
+
nome: String(p.label),
|
|
436
|
+
valor: typeof p.value === "number" && p.value > 0 ? p.value : 0
|
|
437
|
+
}));
|
|
438
|
+
let partes = limpos;
|
|
439
|
+
let agrupou = false;
|
|
440
|
+
if (limpos.length > CHART_MAX_SERIES) {
|
|
441
|
+
const ficam = limpos.slice(0, CHART_MAX_SERIES - 1);
|
|
442
|
+
const resto = limpos.slice(CHART_MAX_SERIES - 1).reduce((soma, p) => soma + p.valor, 0);
|
|
443
|
+
partes = [...ficam, {
|
|
444
|
+
nome: rotuloOutros,
|
|
445
|
+
valor: resto
|
|
446
|
+
}];
|
|
447
|
+
agrupou = true;
|
|
448
|
+
}
|
|
449
|
+
const total = partes.reduce((soma, p) => soma + p.valor, 0);
|
|
450
|
+
const c = tamanho / 2;
|
|
451
|
+
const raio = c - 1;
|
|
452
|
+
const interno = raio * 0.64;
|
|
453
|
+
let angulo = 0;
|
|
454
|
+
const fatias = partes.map((p, i) => {
|
|
455
|
+
const fracao = total > 0 ? p.valor / total : 0;
|
|
456
|
+
const a0 = angulo;
|
|
457
|
+
const a1 = angulo + fracao * Math.PI * 2;
|
|
458
|
+
angulo = a1;
|
|
459
|
+
return {
|
|
460
|
+
chave: String(i),
|
|
461
|
+
serie: agrupou && i === partes.length - 1 ? 0 : i + 1,
|
|
462
|
+
indice: i,
|
|
463
|
+
nome: p.nome,
|
|
464
|
+
valor: p.valor,
|
|
465
|
+
texto: formatarValor(p.valor, formato),
|
|
466
|
+
percentual: `${(fracao * 100).toLocaleString("pt-BR", {
|
|
467
|
+
maximumFractionDigits: 1
|
|
468
|
+
})}%`,
|
|
469
|
+
d: fracao > 0 ? caminhoDoSetor(c, raio, interno, a0, a1) : ""
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
return {
|
|
473
|
+
total,
|
|
474
|
+
fatias
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ─── Modelos prontos para o template ─────────────────────────────────
|
|
479
|
+
//
|
|
480
|
+
// O template de um `.lite.tsx` só lê campos destes objetos. Nenhum getter do
|
|
481
|
+
// componente lê outro getter (na geração Vue isso perde o `.value`), então
|
|
482
|
+
// tudo o que o desenho precisa é calculado aqui, de uma vez.
|
|
483
|
+
|
|
484
|
+
export interface ChartEntradaCartesiana extends ChartFormato {
|
|
485
|
+
data?: ChartPonto[];
|
|
486
|
+
series?: ChartSerieBruta[];
|
|
487
|
+
labels?: string[];
|
|
488
|
+
seriesName?: string;
|
|
489
|
+
title?: string;
|
|
490
|
+
ariaLabel?: string;
|
|
491
|
+
showLegend?: any;
|
|
492
|
+
showGrid?: any;
|
|
493
|
+
}
|
|
494
|
+
export interface ChartGradeLinha {
|
|
495
|
+
chave: string;
|
|
496
|
+
x1: number;
|
|
497
|
+
x2: number;
|
|
498
|
+
y: number;
|
|
499
|
+
xTexto: number;
|
|
500
|
+
texto: string;
|
|
501
|
+
}
|
|
502
|
+
export interface ChartRotuloXPosicionado {
|
|
503
|
+
chave: string;
|
|
504
|
+
x: number;
|
|
505
|
+
y: number;
|
|
506
|
+
texto: string;
|
|
507
|
+
}
|
|
508
|
+
export interface ChartAlvoPosicionado extends ChartAlvo {
|
|
509
|
+
cx: number;
|
|
510
|
+
}
|
|
511
|
+
export interface ChartItemDeLegenda {
|
|
512
|
+
chave: string;
|
|
513
|
+
serie: number;
|
|
514
|
+
nome: string;
|
|
515
|
+
}
|
|
516
|
+
export interface ChartLinhaDaTabela {
|
|
517
|
+
chave: string;
|
|
518
|
+
categoria: string;
|
|
519
|
+
valores: Array<{
|
|
520
|
+
chave: string;
|
|
521
|
+
texto: string;
|
|
522
|
+
}>;
|
|
523
|
+
}
|
|
524
|
+
export interface ChartModeloCartesiano {
|
|
525
|
+
vazio: boolean;
|
|
526
|
+
viewBox: string;
|
|
527
|
+
largura: number;
|
|
528
|
+
altura: number;
|
|
529
|
+
topo: number;
|
|
530
|
+
base: number;
|
|
531
|
+
esquerda: number;
|
|
532
|
+
fimX: number;
|
|
533
|
+
y0: number;
|
|
534
|
+
mostraGrade: boolean;
|
|
535
|
+
mostraLegenda: boolean;
|
|
536
|
+
grade: ChartGradeLinha[];
|
|
537
|
+
rotulosX: ChartRotuloXPosicionado[];
|
|
538
|
+
alvos: ChartAlvoPosicionado[];
|
|
539
|
+
barras: ChartBarra[];
|
|
540
|
+
tracos: ChartTraco[];
|
|
541
|
+
pontos: ChartPontoDaLinha[];
|
|
542
|
+
legenda: ChartItemDeLegenda[];
|
|
543
|
+
resumo: string;
|
|
544
|
+
rotuloDaLista: string;
|
|
545
|
+
legendaDaTabela: string;
|
|
546
|
+
cabecalhos: ChartItemDeLegenda[];
|
|
547
|
+
tabela: ChartLinhaDaTabela[];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Prop booleana que liga por padrão: só `false` desliga (Vue entrega `undefined` sem `boolean`). */
|
|
551
|
+
function ligadoPorPadrao(valor: any): boolean {
|
|
552
|
+
return valor !== false && valor !== "false";
|
|
553
|
+
}
|
|
554
|
+
export function modeloCartesiano(tipo: "barras" | "linhas", entrada: ChartEntradaCartesiana, largura: number, altura: number): ChartModeloCartesiano {
|
|
555
|
+
const formato: ChartFormato = {
|
|
556
|
+
valueFormatter: entrada.valueFormatter,
|
|
557
|
+
unit: entrada.unit
|
|
558
|
+
};
|
|
559
|
+
const dados = normalizarDados(entrada.data, entrada.series, entrada.labels, entrada.seriesName || entrada.title || "Valor");
|
|
560
|
+
const largo = Math.max(120, Math.round(largura || 0));
|
|
561
|
+
const alto = Math.max(80, Math.round(altura || 0));
|
|
562
|
+
const legenda = dados.series.map((s, j) => ({
|
|
563
|
+
chave: String(j),
|
|
564
|
+
serie: j + 1,
|
|
565
|
+
nome: s.name
|
|
566
|
+
}));
|
|
567
|
+
const nomeDoTipo = tipo === "barras" ? "Gráfico de barras" : "Gráfico de linhas";
|
|
568
|
+
const base: ChartModeloCartesiano = {
|
|
569
|
+
vazio: true,
|
|
570
|
+
viewBox: `0 0 ${largo} ${alto}`,
|
|
571
|
+
largura: largo,
|
|
572
|
+
altura: alto,
|
|
573
|
+
topo: 0,
|
|
574
|
+
base: alto,
|
|
575
|
+
esquerda: 0,
|
|
576
|
+
fimX: largo,
|
|
577
|
+
y0: alto,
|
|
578
|
+
mostraGrade: ligadoPorPadrao(entrada.showGrid),
|
|
579
|
+
mostraLegenda: legenda.length > 1 && ligadoPorPadrao(entrada.showLegend),
|
|
580
|
+
grade: [],
|
|
581
|
+
rotulosX: [],
|
|
582
|
+
alvos: [],
|
|
583
|
+
barras: [],
|
|
584
|
+
tracos: [],
|
|
585
|
+
pontos: [],
|
|
586
|
+
legenda,
|
|
587
|
+
resumo: entrada.ariaLabel || `${nomeDoTipo} sem dados.`,
|
|
588
|
+
rotuloDaLista: entrada.title ? `Valores por categoria — ${entrada.title}` : "Valores por categoria",
|
|
589
|
+
legendaDaTabela: entrada.title || nomeDoTipo,
|
|
590
|
+
cabecalhos: legenda,
|
|
591
|
+
tabela: []
|
|
592
|
+
};
|
|
593
|
+
if (!temValores(dados)) return base;
|
|
594
|
+
const plano = montarPlano(dados, largo, alto, formato);
|
|
595
|
+
const fimX = largo - plano.direita;
|
|
596
|
+
const tracos = tipo === "linhas" ? montarLinhas(dados, plano) : [];
|
|
597
|
+
const pontos: ChartPontoDaLinha[] = [];
|
|
598
|
+
for (const t of tracos) for (const p of t.pontos) pontos.push(p);
|
|
599
|
+
return {
|
|
600
|
+
...base,
|
|
601
|
+
vazio: false,
|
|
602
|
+
topo: plano.topo,
|
|
603
|
+
base: plano.base,
|
|
604
|
+
esquerda: plano.esquerda,
|
|
605
|
+
fimX,
|
|
606
|
+
y0: plano.y0,
|
|
607
|
+
grade: plano.ticks.map((t, i) => ({
|
|
608
|
+
chave: String(i),
|
|
609
|
+
x1: plano.esquerda,
|
|
610
|
+
x2: fimX,
|
|
611
|
+
y: t.y,
|
|
612
|
+
xTexto: plano.esquerda - 8,
|
|
613
|
+
texto: t.texto
|
|
614
|
+
})),
|
|
615
|
+
rotulosX: plano.rotulosX.map((r, i) => ({
|
|
616
|
+
chave: String(i),
|
|
617
|
+
x: r.x,
|
|
618
|
+
y: plano.base + 8,
|
|
619
|
+
texto: r.texto
|
|
620
|
+
})),
|
|
621
|
+
alvos: plano.alvos.map(a => ({
|
|
622
|
+
...a,
|
|
623
|
+
cx: plano.centroX(a.indice)
|
|
624
|
+
})),
|
|
625
|
+
barras: tipo === "barras" ? montarBarras(dados, plano) : [],
|
|
626
|
+
tracos,
|
|
627
|
+
pontos,
|
|
628
|
+
// Sem o título: a <figcaption> já dá nome à figura, e repetir aqui faria o
|
|
629
|
+
// leitor de tela anunciar o título duas vezes.
|
|
630
|
+
resumo: entrada.ariaLabel || resumoCartesiano(nomeDoTipo, dados, formato),
|
|
631
|
+
tabela: dados.categorias.map((categoria, i) => ({
|
|
632
|
+
chave: String(i),
|
|
633
|
+
categoria,
|
|
634
|
+
valores: dados.series.map((s, j) => ({
|
|
635
|
+
chave: String(j),
|
|
636
|
+
texto: valorAcessivel(s.values[i], formato)
|
|
637
|
+
}))
|
|
638
|
+
}))
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
export interface ChartEntradaRosca extends ChartFormato {
|
|
642
|
+
data?: ChartPonto[];
|
|
643
|
+
title?: string;
|
|
644
|
+
ariaLabel?: string;
|
|
645
|
+
centerLabel?: string;
|
|
646
|
+
centerValue?: string;
|
|
647
|
+
otherLabel?: string;
|
|
648
|
+
showLegend?: any;
|
|
649
|
+
}
|
|
650
|
+
export interface ChartModeloRosca {
|
|
651
|
+
vazio: boolean;
|
|
652
|
+
tamanho: number;
|
|
653
|
+
viewBox: string;
|
|
654
|
+
fatias: ChartFatia[];
|
|
655
|
+
centroValor: string;
|
|
656
|
+
centroRotulo: string;
|
|
657
|
+
mostraLegenda: boolean;
|
|
658
|
+
resumo: string;
|
|
659
|
+
}
|
|
660
|
+
export function modeloRosca(entrada: ChartEntradaRosca, tamanho: number): ChartModeloRosca {
|
|
661
|
+
const formato: ChartFormato = {
|
|
662
|
+
valueFormatter: entrada.valueFormatter,
|
|
663
|
+
unit: entrada.unit
|
|
664
|
+
};
|
|
665
|
+
const lado = Math.max(96, Math.round(tamanho || 0));
|
|
666
|
+
const rosca = montarRosca(entrada.data, lado, formato, entrada.otherLabel || "Outros");
|
|
667
|
+
const centroValor = entrada.centerValue || formatarValor(rosca.total, formato);
|
|
668
|
+
const centroRotulo = entrada.centerLabel === undefined ? "Total" : entrada.centerLabel;
|
|
669
|
+
return {
|
|
670
|
+
vazio: rosca.total <= 0,
|
|
671
|
+
tamanho: lado,
|
|
672
|
+
viewBox: `0 0 ${lado} ${lado}`,
|
|
673
|
+
fatias: rosca.fatias,
|
|
674
|
+
centroValor,
|
|
675
|
+
centroRotulo,
|
|
676
|
+
mostraLegenda: ligadoPorPadrao(entrada.showLegend),
|
|
677
|
+
// Curto de propósito: as partes, com valor e percentual, estão na legenda
|
|
678
|
+
// (visível ou oculta), que é a leitura principal. Repetir aqui faria o
|
|
679
|
+
// leitor de tela ler tudo duas vezes.
|
|
680
|
+
resumo: entrada.ariaLabel || (rosca.total > 0 ? `Gráfico de rosca com ${rosca.fatias.length} partes. Total: ${centroValor}${centroRotulo && centroRotulo !== "Total" ? " " + centroRotulo : ""}.` : "Gráfico de rosca sem dados.")
|
|
681
|
+
};
|
|
682
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Densidade global de interação (decisão 25/07, simulação de adoção).
|
|
3
|
+
*
|
|
4
|
+
* O modo "touch" (`<html data-density="touch">`) eleva os alvos de toque dos
|
|
5
|
+
* controles interativos para ≥44px (Apple HIG / WCAG 2.5.8), aumentando
|
|
6
|
+
* controle e espaçamento juntos — sem hit-areas invisíveis, que roubam
|
|
7
|
+
* clique de vizinhos em listas densas. O default permanece idêntico.
|
|
8
|
+
*/
|
|
9
|
+
export type Density = "default" | "touch";
|
|
10
|
+
const DENSITY_ATTRIBUTE = "data-density";
|
|
11
|
+
|
|
12
|
+
/** Define a densidade global (remove o atributo quando "default"). */
|
|
13
|
+
export const setDensity = (density: Density): void => {
|
|
14
|
+
if (typeof document === "undefined") return;
|
|
15
|
+
if (density === "default") {
|
|
16
|
+
document.documentElement.removeAttribute(DENSITY_ATTRIBUTE);
|
|
17
|
+
} else {
|
|
18
|
+
document.documentElement.setAttribute(DENSITY_ATTRIBUTE, density);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Densidade global atual. */
|
|
23
|
+
export const getDensity = (): Density => {
|
|
24
|
+
if (typeof document === "undefined") return "default";
|
|
25
|
+
const value = document.documentElement.getAttribute(DENSITY_ATTRIBUTE);
|
|
26
|
+
return value === "touch" ? "touch" : "default";
|
|
27
|
+
}
|