orn-ui 0.2.7 → 0.2.9
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/AGENTS.md +25 -3
- package/README.md +9 -8
- package/lib/commonjs/atoms/Gradient.js +111 -0
- package/lib/commonjs/atoms/Gradient.js.map +1 -0
- package/lib/commonjs/atoms/ShinyText.js +188 -0
- package/lib/commonjs/atoms/ShinyText.js.map +1 -0
- package/lib/commonjs/atoms/index.js +14 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/organisms/Chart.js +947 -0
- package/lib/commonjs/organisms/Chart.js.map +1 -0
- package/lib/commonjs/organisms/OptionWheel.js +408 -0
- package/lib/commonjs/organisms/OptionWheel.js.map +1 -0
- package/lib/commonjs/organisms/Slides.js +406 -0
- package/lib/commonjs/organisms/Slides.js.map +1 -0
- package/lib/commonjs/organisms/Timeline.js +420 -0
- package/lib/commonjs/organisms/Timeline.js.map +1 -0
- package/lib/commonjs/organisms/chart/frame.js +373 -0
- package/lib/commonjs/organisms/chart/frame.js.map +1 -0
- package/lib/commonjs/organisms/chart/marks.js +316 -0
- package/lib/commonjs/organisms/chart/marks.js.map +1 -0
- package/lib/commonjs/organisms/chart/math.js +468 -0
- package/lib/commonjs/organisms/chart/math.js.map +1 -0
- package/lib/commonjs/organisms/chart/palette.js +120 -0
- package/lib/commonjs/organisms/chart/palette.js.map +1 -0
- package/lib/commonjs/organisms/index.js +28 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/commonjs/organisms/useSlides.js +186 -0
- package/lib/commonjs/organisms/useSlides.js.map +1 -0
- package/lib/module/atoms/Gradient.js +106 -0
- package/lib/module/atoms/Gradient.js.map +1 -0
- package/lib/module/atoms/ShinyText.js +183 -0
- package/lib/module/atoms/ShinyText.js.map +1 -0
- package/lib/module/atoms/index.js +2 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/organisms/Chart.js +942 -0
- package/lib/module/organisms/Chart.js.map +1 -0
- package/lib/module/organisms/OptionWheel.js +403 -0
- package/lib/module/organisms/OptionWheel.js.map +1 -0
- package/lib/module/organisms/Slides.js +401 -0
- package/lib/module/organisms/Slides.js.map +1 -0
- package/lib/module/organisms/Timeline.js +415 -0
- package/lib/module/organisms/Timeline.js.map +1 -0
- package/lib/module/organisms/chart/frame.js +367 -0
- package/lib/module/organisms/chart/frame.js.map +1 -0
- package/lib/module/organisms/chart/marks.js +310 -0
- package/lib/module/organisms/chart/marks.js.map +1 -0
- package/lib/module/organisms/chart/math.js +445 -0
- package/lib/module/organisms/chart/math.js.map +1 -0
- package/lib/module/organisms/chart/palette.js +114 -0
- package/lib/module/organisms/chart/palette.js.map +1 -0
- package/lib/module/organisms/index.js +4 -0
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/module/organisms/useSlides.js +182 -0
- package/lib/module/organisms/useSlides.js.map +1 -0
- package/lib/typescript/atoms/Gradient.d.ts +37 -0
- package/lib/typescript/atoms/Gradient.d.ts.map +1 -0
- package/lib/typescript/atoms/ShinyText.d.ts +48 -0
- package/lib/typescript/atoms/ShinyText.d.ts.map +1 -0
- package/lib/typescript/atoms/index.d.ts +4 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/organisms/Chart.d.ts +208 -0
- package/lib/typescript/organisms/Chart.d.ts.map +1 -0
- package/lib/typescript/organisms/OptionWheel.d.ts +78 -0
- package/lib/typescript/organisms/OptionWheel.d.ts.map +1 -0
- package/lib/typescript/organisms/Slides.d.ts +69 -0
- package/lib/typescript/organisms/Slides.d.ts.map +1 -0
- package/lib/typescript/organisms/Timeline.d.ts +61 -0
- package/lib/typescript/organisms/Timeline.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/frame.d.ts +85 -0
- package/lib/typescript/organisms/chart/frame.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/marks.d.ts +88 -0
- package/lib/typescript/organisms/chart/marks.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/math.d.ts +161 -0
- package/lib/typescript/organisms/chart/math.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/palette.d.ts +6 -0
- package/lib/typescript/organisms/chart/palette.d.ts.map +1 -0
- package/lib/typescript/organisms/index.d.ts +8 -0
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/lib/typescript/organisms/useSlides.d.ts +52 -0
- package/lib/typescript/organisms/useSlides.d.ts.map +1 -0
- package/llms.txt +6 -5
- package/package.json +33 -2
- package/registry/chart.json +35 -0
- package/registry/gradient.json +18 -0
- package/registry/index.json +79 -0
- package/registry/manifest.json +42 -0
- package/registry/option-wheel.json +19 -0
- package/registry/shiny-text.json +19 -0
- package/registry/slides.json +24 -0
- package/registry/timeline.json +20 -0
- package/src/atoms/Gradient.tsx +99 -0
- package/src/atoms/ShinyText.tsx +226 -0
- package/src/atoms/index.ts +4 -0
- package/src/organisms/Chart.tsx +1223 -0
- package/src/organisms/OptionWheel.tsx +476 -0
- package/src/organisms/Slides.tsx +413 -0
- package/src/organisms/Timeline.tsx +467 -0
- package/src/organisms/chart/frame.tsx +349 -0
- package/src/organisms/chart/marks.tsx +295 -0
- package/src/organisms/chart/math.ts +475 -0
- package/src/organisms/chart/palette.ts +112 -0
- package/src/organisms/index.ts +22 -0
- package/src/organisms/useSlides.ts +220 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geometría y escalas de Chart: todo lo que se puede probar sin montar un
|
|
3
|
+
* árbol vive acá, sin React y sin dependencias. marks.tsx sólo traduce estos
|
|
4
|
+
* números a estilos, y Chart.tsx sólo decide qué marca dibujar.
|
|
5
|
+
*
|
|
6
|
+
* Convenciones:
|
|
7
|
+
* - Coordenadas de React Native: origen arriba-izquierda, `y` crece hacia abajo.
|
|
8
|
+
* - Ángulos en grados, 0° a las 12 en punto, creciendo en sentido horario —
|
|
9
|
+
* que es como se lee una torta y como gira `transform: rotate` en RN.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface Point {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Fila de datos: una categoría del eje x y un número por serie. */
|
|
18
|
+
export type ChartRow = Record<string, string | number | null | undefined>;
|
|
19
|
+
|
|
20
|
+
/** Lee una celda como número, o null si no lo es (hueco en la serie). */
|
|
21
|
+
export function toNumber(value: unknown): number | null {
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
26
|
+
return Math.min(max, Math.max(min, value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Mata el ruido de punto flotante que deja sumar `step` en un bucle. */
|
|
30
|
+
function tidy(value: number): number {
|
|
31
|
+
return Number(value.toFixed(10));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── escalas ────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* El 1, 2, 5 o 10 (por su potencia de diez) más cercano por encima de `rough`.
|
|
38
|
+
* Es lo que hace que los ticks caigan en números que alguien diría en voz
|
|
39
|
+
* alta — 0, 250, 500 — y no en 0, 237, 474.
|
|
40
|
+
*/
|
|
41
|
+
export function niceStep(rough: number): number {
|
|
42
|
+
if (!(rough > 0)) return 1;
|
|
43
|
+
const magnitude = Math.pow(10, Math.floor(Math.log10(rough)));
|
|
44
|
+
const fraction = rough / magnitude;
|
|
45
|
+
const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10;
|
|
46
|
+
return nice * magnitude;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Ticks {
|
|
50
|
+
/** Extremo inferior redondeado hacia afuera. */
|
|
51
|
+
min: number;
|
|
52
|
+
/** Extremo superior redondeado hacia afuera. */
|
|
53
|
+
max: number;
|
|
54
|
+
step: number;
|
|
55
|
+
values: number[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Dominio redondeado a valores legibles más la lista de ticks. `count` es una
|
|
60
|
+
* intención, no una promesa: redondear hacia afuera puede dar uno más o uno
|
|
61
|
+
* menos, y eso es preferible a un eje con números arbitrarios.
|
|
62
|
+
*/
|
|
63
|
+
export function niceScale(min: number, max: number, count = 5): Ticks {
|
|
64
|
+
let lo = Math.min(min, max);
|
|
65
|
+
let hi = Math.max(min, max);
|
|
66
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
|
|
67
|
+
lo = 0;
|
|
68
|
+
hi = 1;
|
|
69
|
+
}
|
|
70
|
+
if (lo === hi) {
|
|
71
|
+
// Un solo valor no define un rango. Se le inventa uno simétrico para que
|
|
72
|
+
// la marca no quede ni pegada al borde ni ocupando el alto entero.
|
|
73
|
+
const pad = Math.abs(lo) || 1;
|
|
74
|
+
lo -= pad;
|
|
75
|
+
hi += pad;
|
|
76
|
+
}
|
|
77
|
+
const step = niceStep((hi - lo) / Math.max(1, count));
|
|
78
|
+
const start = Math.floor(lo / step) * step;
|
|
79
|
+
const end = Math.ceil(hi / step) * step;
|
|
80
|
+
const values: number[] = [];
|
|
81
|
+
// `end + step / 2` en vez de `end`: sumar step acumula error y el último
|
|
82
|
+
// tick se perdería justo en los dominios que más se usan (0–100, 0–1).
|
|
83
|
+
for (let value = start; value <= end + step / 2; value += step) values.push(tidy(value));
|
|
84
|
+
return { min: start, max: end, step, values };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ticks de un dominio que el consumidor fijó a mano. No redondea nada: si
|
|
89
|
+
* alguien pide 0–100, el eje dice 0–100. Es lo contrario de `niceScale`, y es
|
|
90
|
+
* lo que necesita un chart en vivo — un eje que se reacomoda en cada tick hace
|
|
91
|
+
* saltar la línea aunque el dato no se haya movido.
|
|
92
|
+
*/
|
|
93
|
+
export function exactTicks(min: number, max: number, count = 5): Ticks {
|
|
94
|
+
const lo = Math.min(min, max);
|
|
95
|
+
const hi = Math.max(min, max);
|
|
96
|
+
const steps = Math.max(1, Math.floor(count));
|
|
97
|
+
if (lo === hi) return { min: lo, max: hi, step: 0, values: [lo] };
|
|
98
|
+
const step = (hi - lo) / steps;
|
|
99
|
+
return {
|
|
100
|
+
min: lo,
|
|
101
|
+
max: hi,
|
|
102
|
+
step,
|
|
103
|
+
values: Array.from({ length: steps + 1 }, (_, index) => tidy(lo + index * step)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Une dos dominios quedándose con el más ancho. Es el modo 'sticky': el eje
|
|
109
|
+
* crece cuando llega un valor que no entraba y no se vuelve a achicar, así
|
|
110
|
+
* que lo que se ve moverse es el dato y no la escala.
|
|
111
|
+
*/
|
|
112
|
+
export function growExtent(previous: [number, number] | null, next: [number, number]): [number, number] {
|
|
113
|
+
if (!previous) return next;
|
|
114
|
+
return [Math.min(previous[0], next[0]), Math.max(previous[1], next[1])];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Interpolador lineal dominio → rango. Dominio degenerado cae al medio del rango. */
|
|
118
|
+
export function linearScale(domain: [number, number], range: [number, number]): (value: number) => number {
|
|
119
|
+
const [d0, d1] = domain;
|
|
120
|
+
const [r0, r1] = range;
|
|
121
|
+
const span = d1 - d0;
|
|
122
|
+
if (span === 0) return () => (r0 + r1) / 2;
|
|
123
|
+
return (value: number) => r0 + ((value - d0) / span) * (r1 - r0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ExtentOptions {
|
|
127
|
+
/** Las series se suman en vez de compararse (barras/áreas apiladas). */
|
|
128
|
+
stacked?: boolean;
|
|
129
|
+
/** Fuerza el 0 adentro del dominio: sin él, una barra corta miente. */
|
|
130
|
+
includeZero?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Mínimo y máximo de las series pedidas. Sin datos devuelve [0, 1]. */
|
|
134
|
+
export function extent(rows: ChartRow[], keys: string[], options: ExtentOptions = {}): [number, number] {
|
|
135
|
+
const { stacked = false, includeZero = false } = options;
|
|
136
|
+
let min = Infinity;
|
|
137
|
+
let max = -Infinity;
|
|
138
|
+
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
if (stacked) {
|
|
141
|
+
// Positivos y negativos se apilan hacia lados opuestos del cero, así que
|
|
142
|
+
// cada lado tiene su propio total y ninguno cancela al otro.
|
|
143
|
+
let up = 0;
|
|
144
|
+
let down = 0;
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
const value = toNumber(row[key]);
|
|
147
|
+
if (value === null) continue;
|
|
148
|
+
if (value >= 0) up += value;
|
|
149
|
+
else down += value;
|
|
150
|
+
}
|
|
151
|
+
min = Math.min(min, down);
|
|
152
|
+
max = Math.max(max, up);
|
|
153
|
+
} else {
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
const value = toNumber(row[key]);
|
|
156
|
+
if (value === null) continue;
|
|
157
|
+
min = Math.min(min, value);
|
|
158
|
+
max = Math.max(max, value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (min === Infinity) return [0, 1];
|
|
164
|
+
if (includeZero) {
|
|
165
|
+
min = Math.min(min, 0);
|
|
166
|
+
max = Math.max(max, 0);
|
|
167
|
+
}
|
|
168
|
+
return [min, max];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── apilado ────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
export interface StackBand {
|
|
174
|
+
key: string;
|
|
175
|
+
/** Borde de la banda más cerca del cero. */
|
|
176
|
+
from: number;
|
|
177
|
+
/** Borde más lejos del cero. */
|
|
178
|
+
to: number;
|
|
179
|
+
value: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Bandas apiladas de una fila. Los positivos crecen desde 0 hacia arriba y
|
|
184
|
+
* los negativos desde 0 hacia abajo: una serie negativa no le come el alto a
|
|
185
|
+
* las positivas, que es lo que pasa si se apila todo en un solo acumulador.
|
|
186
|
+
*/
|
|
187
|
+
export function stackRow(row: ChartRow, keys: string[]): StackBand[] {
|
|
188
|
+
const bands: StackBand[] = [];
|
|
189
|
+
let up = 0;
|
|
190
|
+
let down = 0;
|
|
191
|
+
for (const key of keys) {
|
|
192
|
+
const value = toNumber(row[key]);
|
|
193
|
+
if (value === null) continue;
|
|
194
|
+
if (value >= 0) {
|
|
195
|
+
bands.push({ key, from: up, to: up + value, value });
|
|
196
|
+
up += value;
|
|
197
|
+
} else {
|
|
198
|
+
bands.push({ key, from: down, to: down + value, value });
|
|
199
|
+
down += value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return bands;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── polar ──────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
const RAD = Math.PI / 180;
|
|
208
|
+
|
|
209
|
+
/** Punto a `radius` del centro en el ángulo dado (0° arriba, horario). */
|
|
210
|
+
export function polar(center: Point, radius: number, angle: number): Point {
|
|
211
|
+
return {
|
|
212
|
+
x: center.x + radius * Math.sin(angle * RAD),
|
|
213
|
+
y: center.y - radius * Math.cos(angle * RAD),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Ángulo de un punto respecto del centro, normalizado a [0, 360). */
|
|
218
|
+
export function angleOf(center: Point, point: Point): number {
|
|
219
|
+
const degrees = Math.atan2(point.x - center.x, center.y - point.y) / RAD;
|
|
220
|
+
return (degrees + 360) % 360;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface Arc {
|
|
224
|
+
/** Índice de la fila que representa. */
|
|
225
|
+
index: number;
|
|
226
|
+
start: number;
|
|
227
|
+
sweep: number;
|
|
228
|
+
value: number;
|
|
229
|
+
/** Proporción sobre el total, 0–1. */
|
|
230
|
+
ratio: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Arcos de una torta. Los negativos no existen en una torta (no hay "menos
|
|
235
|
+
* porción"): se descartan en vez de invertirse. Total 0 ⇒ nada que dibujar.
|
|
236
|
+
*/
|
|
237
|
+
export function arcsFor(values: number[]): Arc[] {
|
|
238
|
+
const safe = values.map((value) => (Number.isFinite(value) && value > 0 ? value : 0));
|
|
239
|
+
const total = safe.reduce((sum, value) => sum + value, 0);
|
|
240
|
+
if (total <= 0) return [];
|
|
241
|
+
|
|
242
|
+
const arcs: Arc[] = [];
|
|
243
|
+
let cursor = 0;
|
|
244
|
+
safe.forEach((value, index) => {
|
|
245
|
+
if (value <= 0) return;
|
|
246
|
+
const sweep = (value / total) * 360;
|
|
247
|
+
arcs.push({ index, start: cursor, sweep, value, ratio: value / total });
|
|
248
|
+
cursor += sweep;
|
|
249
|
+
});
|
|
250
|
+
return arcs;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** El arco que contiene ese ángulo, o -1. */
|
|
254
|
+
export function arcAt(arcs: Arc[], angle: number): number {
|
|
255
|
+
const normalized = (angle + 360) % 360;
|
|
256
|
+
for (const arc of arcs) {
|
|
257
|
+
if (normalized >= arc.start && normalized < arc.start + arc.sweep) return arc.index;
|
|
258
|
+
}
|
|
259
|
+
return -1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Ángulos de los ejes de un radar: el primero arriba, el resto en reparto parejo. */
|
|
263
|
+
export function radarAngles(count: number): number[] {
|
|
264
|
+
if (count <= 0) return [];
|
|
265
|
+
return Array.from({ length: count }, (_, index) => (index * 360) / count);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── segmentos y curvas ─────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
export interface SegmentBox {
|
|
271
|
+
left: number;
|
|
272
|
+
top: number;
|
|
273
|
+
width: number;
|
|
274
|
+
height: number;
|
|
275
|
+
/** Grados para `transform: [{ rotate }]`. */
|
|
276
|
+
rotate: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Caja de un segmento a→b: sin SVG, una línea es un rectángulo fino rotado
|
|
281
|
+
* sobre su centro. Ancho = distancia, alto = grosor, rotación = la pendiente.
|
|
282
|
+
*/
|
|
283
|
+
export function segmentBox(a: Point, b: Point, thickness: number): SegmentBox {
|
|
284
|
+
const dx = b.x - a.x;
|
|
285
|
+
const dy = b.y - a.y;
|
|
286
|
+
const length = Math.hypot(dx, dy);
|
|
287
|
+
return {
|
|
288
|
+
left: (a.x + b.x) / 2 - length / 2,
|
|
289
|
+
top: (a.y + b.y) / 2 - thickness / 2,
|
|
290
|
+
width: length,
|
|
291
|
+
height: thickness,
|
|
292
|
+
rotate: Math.atan2(dy, dx) / RAD,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Suaviza una polilínea con Catmull-Rom muestreada en `steps` tramos por vano.
|
|
298
|
+
* La curva pasa por todos los puntos originales (a diferencia de un Bézier de
|
|
299
|
+
* control libre), así que el dato sigue siendo el dato; sólo se agrega camino
|
|
300
|
+
* entre medio, y el resultado se dibuja con los mismos segmentos rectos.
|
|
301
|
+
*/
|
|
302
|
+
export function smoothPoints(points: Point[], steps = 8): Point[] {
|
|
303
|
+
if (points.length < 3 || steps < 2) return points;
|
|
304
|
+
const out: Point[] = [points[0]!];
|
|
305
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
306
|
+
// Los extremos se duplican para que el primer y el último vano tengan
|
|
307
|
+
// las cuatro referencias que pide la spline.
|
|
308
|
+
const p0 = points[i - 1] ?? points[i]!;
|
|
309
|
+
const p1 = points[i]!;
|
|
310
|
+
const p2 = points[i + 1]!;
|
|
311
|
+
const p3 = points[i + 2] ?? p2;
|
|
312
|
+
for (let s = 1; s <= steps; s += 1) {
|
|
313
|
+
const t = s / steps;
|
|
314
|
+
const t2 = t * t;
|
|
315
|
+
const t3 = t2 * t;
|
|
316
|
+
out.push({
|
|
317
|
+
x: 0.5 * (2 * p1.x + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
|
|
318
|
+
y: 0.5 * (2 * p1.y + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* `y` de la polilínea en esa `x`, o null si cae fuera. Es lo que convierte una
|
|
327
|
+
* línea en área: cada tira vertical pregunta su alto acá.
|
|
328
|
+
*/
|
|
329
|
+
export function sampleY(points: Point[], x: number): number | null {
|
|
330
|
+
if (points.length === 0) return null;
|
|
331
|
+
if (points.length === 1) return points[0]!.y;
|
|
332
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
333
|
+
const a = points[i]!;
|
|
334
|
+
const b = points[i + 1]!;
|
|
335
|
+
const [lo, hi] = a.x <= b.x ? [a, b] : [b, a];
|
|
336
|
+
if (x < lo.x || x > hi.x) continue;
|
|
337
|
+
if (hi.x === lo.x) return lo.y;
|
|
338
|
+
return lo.y + ((x - lo.x) / (hi.x - lo.x)) * (hi.y - lo.y);
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── formato ────────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
const UNITS = ['', 'k', 'M', 'B'];
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Formato por defecto de ejes y tooltip: compacto, porque un eje de un chart
|
|
349
|
+
* de 320px no tiene lugar para "1250000". El consumidor lo reemplaza entero
|
|
350
|
+
* con `formatValue`.
|
|
351
|
+
*/
|
|
352
|
+
export function formatNumber(value: number): string {
|
|
353
|
+
if (!Number.isFinite(value)) return '';
|
|
354
|
+
const sign = value < 0 ? '-' : '';
|
|
355
|
+
let magnitude = Math.abs(value);
|
|
356
|
+
let unit = 0;
|
|
357
|
+
while (magnitude >= 1000 && unit < UNITS.length - 1) {
|
|
358
|
+
magnitude /= 1000;
|
|
359
|
+
unit += 1;
|
|
360
|
+
}
|
|
361
|
+
const decimals = magnitude < 10 && magnitude % 1 !== 0 ? 1 : 0;
|
|
362
|
+
return `${sign}${magnitude.toFixed(decimals)}${UNITS[unit]}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export interface FanStrip {
|
|
366
|
+
start: number;
|
|
367
|
+
sweep: number;
|
|
368
|
+
radius: number;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Descompone un polígono alrededor de un centro en porciones angulares: es lo
|
|
373
|
+
* que permite rellenar un radar con la misma primitiva que dibuja una torta.
|
|
374
|
+
*
|
|
375
|
+
* El corte va por el lado del polígono (parámetro `t`), no por el ángulo: el
|
|
376
|
+
* radio de una cuerda no es lineal en el ángulo, y cortando por ángulo los
|
|
377
|
+
* vértices quedarían redondeados justo donde el dato manda que haya una punta.
|
|
378
|
+
*/
|
|
379
|
+
export function fanStrips(center: Point, points: Point[], steps = 6): FanStrip[] {
|
|
380
|
+
if (points.length < 3 || steps < 1) return [];
|
|
381
|
+
const strips: FanStrip[] = [];
|
|
382
|
+
|
|
383
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
384
|
+
const a = points[i]!;
|
|
385
|
+
const b = points[(i + 1) % points.length]!;
|
|
386
|
+
|
|
387
|
+
let previous = a;
|
|
388
|
+
let previousAngle = angleOf(center, a);
|
|
389
|
+
for (let s = 1; s <= steps; s += 1) {
|
|
390
|
+
const t = s / steps;
|
|
391
|
+
const point = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
392
|
+
const angle = angleOf(center, point);
|
|
393
|
+
// El salto de 360 a 0 cae en algún lado del recorrido: sin esto, esa
|
|
394
|
+
// porción se dibujaría girando al revés y taparía todo el resto.
|
|
395
|
+
let sweep = angle - previousAngle;
|
|
396
|
+
if (sweep < 0) sweep += 360;
|
|
397
|
+
if (sweep > 0) {
|
|
398
|
+
const radius = (Math.hypot(previous.x - center.x, previous.y - center.y) + Math.hypot(point.x - center.x, point.y - center.y)) / 2;
|
|
399
|
+
// Las porciones se solapan un pelo a propósito: sin el solape, el
|
|
400
|
+
// antialias deja una costura clara entre tira y tira.
|
|
401
|
+
strips.push({ start: previousAngle, sweep: sweep + 0.4, radius });
|
|
402
|
+
}
|
|
403
|
+
previous = point;
|
|
404
|
+
previousAngle = angle;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return strips;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── diezmado ───────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* LTTB (Largest-Triangle-Three-Buckets): elige `threshold` puntos de `values`
|
|
414
|
+
* quedándose con la forma de la curva. Devuelve índices, no puntos, para que
|
|
415
|
+
* quien llama pueda conservar la fila entera —todas sus series— y no sólo la
|
|
416
|
+
* que se usó para decidir.
|
|
417
|
+
*
|
|
418
|
+
* Por qué no un muestreo cada N: cada N se come los picos, que son justo lo
|
|
419
|
+
* que alguien mira en una serie temporal. LTTB parte el recorrido en baldes y
|
|
420
|
+
* de cada uno se queda con el punto que forma el triángulo más grande con el
|
|
421
|
+
* ya elegido y con el promedio del balde siguiente — o sea, con el que más
|
|
422
|
+
* cambia la silueta. Primero y último se conservan siempre.
|
|
423
|
+
*
|
|
424
|
+
* `x` es el índice: las categorías de un chart están repartidas parejo sobre
|
|
425
|
+
* el eje, así que la distancia horizontal entre dos puntos es su distancia de
|
|
426
|
+
* índice.
|
|
427
|
+
*/
|
|
428
|
+
export function lttbIndices(values: (number | null)[], threshold: number): number[] {
|
|
429
|
+
const total = values.length;
|
|
430
|
+
if (threshold >= total || threshold < 3) return values.map((_, index) => index);
|
|
431
|
+
|
|
432
|
+
const at = (index: number) => values[index] ?? 0;
|
|
433
|
+
const selected = [0];
|
|
434
|
+
// Un balde por punto intermedio: el primero y el último no se sortean. Con
|
|
435
|
+
// `threshold < total` el balde mide más de un punto, así que ninguno queda
|
|
436
|
+
// vacío y salen exactamente `threshold` índices, sin repetidos.
|
|
437
|
+
const every = (total - 2) / (threshold - 2);
|
|
438
|
+
let previous = 0;
|
|
439
|
+
|
|
440
|
+
for (let bucket = 0; bucket < threshold - 2; bucket += 1) {
|
|
441
|
+
// Promedio del balde *siguiente*: el tercer vértice del triángulo. Va
|
|
442
|
+
// corrido un balde respecto de los candidatos, y ese corrimiento es el
|
|
443
|
+
// algoritmo — comparar contra el propio balde no mira hacia adelante.
|
|
444
|
+
const avgStart = Math.floor((bucket + 1) * every) + 1;
|
|
445
|
+
const avgEnd = Math.min(Math.floor((bucket + 2) * every) + 1, total);
|
|
446
|
+
let avgX = 0;
|
|
447
|
+
let avgY = 0;
|
|
448
|
+
for (let i = avgStart; i < avgEnd; i += 1) {
|
|
449
|
+
avgX += i;
|
|
450
|
+
avgY += at(i);
|
|
451
|
+
}
|
|
452
|
+
const span = Math.max(1, avgEnd - avgStart);
|
|
453
|
+
avgX /= span;
|
|
454
|
+
avgY /= span;
|
|
455
|
+
|
|
456
|
+
const start = Math.floor(bucket * every) + 1;
|
|
457
|
+
const end = Math.floor((bucket + 1) * every) + 1;
|
|
458
|
+
let best = start;
|
|
459
|
+
let bestArea = -1;
|
|
460
|
+
for (let i = start; i < end; i += 1) {
|
|
461
|
+
const area = Math.abs(
|
|
462
|
+
(previous - avgX) * (at(i) - at(previous)) - (previous - i) * (avgY - at(previous))
|
|
463
|
+
);
|
|
464
|
+
if (area > bestArea) {
|
|
465
|
+
bestArea = area;
|
|
466
|
+
best = i;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
selected.push(best);
|
|
470
|
+
previous = best;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
selected.push(total - 1);
|
|
474
|
+
return selected;
|
|
475
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paleta categórica de Chart, derivada del theme.
|
|
3
|
+
*
|
|
4
|
+
* No hay una lista de colores acá a propósito: si la hubiera, un `brand`
|
|
5
|
+
* personalizado daría un chart que no se parece al resto de la app, y el modo
|
|
6
|
+
* oscuro tendría que mantenerse a mano. Las primeras series reusan los acentos
|
|
7
|
+
* del theme (que ya vienen con el contraste resuelto para su scheme) y, cuando
|
|
8
|
+
* se acaban, se sigue rotando el tono del primario por el ángulo áureo: la
|
|
9
|
+
* secuencia 137.5° es la que más tarda en repetir un tono parecido, que es
|
|
10
|
+
* exactamente lo que se le pide a una paleta categórica.
|
|
11
|
+
*
|
|
12
|
+
* Cada color se verifica contra `surface` a 3:1 — el mínimo de WCAG 1.4.11
|
|
13
|
+
* para objetos gráficos, que es lo que es una barra — y se aclara u oscurece
|
|
14
|
+
* hasta cumplirlo.
|
|
15
|
+
*/
|
|
16
|
+
import { contrast, toHex, toHsl } from '../../theme/colors';
|
|
17
|
+
import type { Theme } from '../../theme/types';
|
|
18
|
+
|
|
19
|
+
/** Contraste mínimo de un objeto gráfico contra su fondo (WCAG 1.4.11). */
|
|
20
|
+
export const GRAPHIC_CONTRAST = 3;
|
|
21
|
+
|
|
22
|
+
const GOLDEN_ANGLE = 137.508;
|
|
23
|
+
|
|
24
|
+
/** Acentos del theme en orden de aparición: tonos bien separados entre sí. */
|
|
25
|
+
function seeds(theme: Theme): string[] {
|
|
26
|
+
const { primary, secondary, warning, success, error } = theme.colors;
|
|
27
|
+
return [primary, secondary, warning, success, error];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Empuja la luminosidad hasta despegar del fondo. El paso es chico y el tope
|
|
32
|
+
* bajo porque el objetivo es corregir un color, no inventar otro: si en 12
|
|
33
|
+
* intentos no llegó, se devuelve lo mejor que se consiguió.
|
|
34
|
+
*/
|
|
35
|
+
function readable(hex: string, surface: string): string {
|
|
36
|
+
if (contrast(hex, surface) >= GRAPHIC_CONTRAST) return hex;
|
|
37
|
+
|
|
38
|
+
const hsl = toHsl(hex);
|
|
39
|
+
// Sobre un fondo claro hay que oscurecer, sobre uno oscuro aclarar.
|
|
40
|
+
const direction = toHsl(surface).l > 0.5 ? -1 : 1;
|
|
41
|
+
let best = hex;
|
|
42
|
+
let bestContrast = contrast(hex, surface);
|
|
43
|
+
|
|
44
|
+
for (let step = 1; step <= 12; step += 1) {
|
|
45
|
+
const candidate = toHex({ ...hsl, l: Math.min(0.95, Math.max(0.08, hsl.l + direction * step * 0.05)) });
|
|
46
|
+
const score = contrast(candidate, surface);
|
|
47
|
+
if (score > bestContrast) {
|
|
48
|
+
best = candidate;
|
|
49
|
+
bestContrast = score;
|
|
50
|
+
}
|
|
51
|
+
if (score >= GRAPHIC_CONTRAST) return candidate;
|
|
52
|
+
}
|
|
53
|
+
return best;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildUnsafe(theme: Theme, count: number): string[] {
|
|
57
|
+
const base = seeds(theme);
|
|
58
|
+
const surface = theme.colors.surface;
|
|
59
|
+
const out: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < count; index += 1) {
|
|
62
|
+
if (index < base.length) {
|
|
63
|
+
out.push(readable(base[index]!, surface));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Pasados los acentos, se gira el tono del primario. `l` alterna un
|
|
67
|
+
// escalón por vuelta para que dos tonos vecinos no se confundan si la
|
|
68
|
+
// rotación los acerca.
|
|
69
|
+
const seed = toHsl(base[0]!);
|
|
70
|
+
const turn = index - base.length + 1;
|
|
71
|
+
out.push(
|
|
72
|
+
readable(
|
|
73
|
+
toHex({
|
|
74
|
+
h: (seed.h + turn * GOLDEN_ANGLE) % 360,
|
|
75
|
+
s: Math.max(0.35, seed.s),
|
|
76
|
+
l: Math.min(0.78, Math.max(0.28, seed.l + (turn % 2 === 0 ? 0.08 : -0.04))),
|
|
77
|
+
}),
|
|
78
|
+
surface
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Los helpers de color hablan hex. Un theme con un acento en `rgba()` es
|
|
87
|
+
* legal para React Native y hace que `toHsl` tire: en ese caso se cae a los
|
|
88
|
+
* acentos tal cual vinieron, ciclando. Un chart con colores repetidos es peor
|
|
89
|
+
* que uno con colores derivados, pero muchísimo mejor que uno que no renderiza.
|
|
90
|
+
*/
|
|
91
|
+
function build(theme: Theme, count: number): string[] {
|
|
92
|
+
try {
|
|
93
|
+
return buildUnsafe(theme, count);
|
|
94
|
+
} catch {
|
|
95
|
+
const base = seeds(theme);
|
|
96
|
+
return Array.from({ length: count }, (_, index) => base[index % base.length]!);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Una paleta por theme, y por theme el arreglo más largo pedido hasta ahora:
|
|
101
|
+
// la cache se comparte entre todos los charts montados, que es el caso normal
|
|
102
|
+
// (un dashboard con seis charts sobre el mismo theme).
|
|
103
|
+
const cache = new WeakMap<Theme, string[]>();
|
|
104
|
+
|
|
105
|
+
/** `count` colores estables para las series: el mismo índice da siempre el mismo color. */
|
|
106
|
+
export function seriesPalette(theme: Theme, count: number): string[] {
|
|
107
|
+
const cached = cache.get(theme);
|
|
108
|
+
if (cached && cached.length >= count) return cached.slice(0, count);
|
|
109
|
+
const built = build(theme, Math.max(count, 8));
|
|
110
|
+
cache.set(theme, built);
|
|
111
|
+
return built.slice(0, count);
|
|
112
|
+
}
|
package/src/organisms/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
export { Timeline } from './Timeline';
|
|
2
|
+
export type { TimelineProps, TimelineItem, TimelineStatus, TimelineAdvance } from './Timeline';
|
|
1
3
|
export { Modal } from './Modal';
|
|
2
4
|
export type { ModalProps, ModalVariant } from './Modal';
|
|
3
5
|
export { BottomSheet } from './BottomSheet';
|
|
4
6
|
export type { BottomSheetProps, FooterPlacement } from './BottomSheet';
|
|
7
|
+
export { OptionWheel } from './OptionWheel';
|
|
8
|
+
export type { OptionWheelProps, OptionWheelOption, OptionWheelVariant } from './OptionWheel';
|
|
5
9
|
export { Select } from './Select';
|
|
6
10
|
export type { SelectProps, SelectOption } from './Select';
|
|
7
11
|
export { Alert } from './Alert';
|
|
@@ -30,3 +34,21 @@ export { NavigationBar } from './NavigationBar';
|
|
|
30
34
|
export type { NavigationBarProps, NavigationBarItem } from './NavigationBar';
|
|
31
35
|
export { ReorderableList } from './ReorderableList';
|
|
32
36
|
export type { ReorderableListProps } from './ReorderableList';
|
|
37
|
+
export { Slides } from './Slides';
|
|
38
|
+
export type {
|
|
39
|
+
SlidesProps,
|
|
40
|
+
SlidesOrientation,
|
|
41
|
+
SlidesIndicators,
|
|
42
|
+
SlidesIndicatorPlacement,
|
|
43
|
+
SlideBackground,
|
|
44
|
+
} from './Slides';
|
|
45
|
+
export { Chart } from './Chart';
|
|
46
|
+
export type {
|
|
47
|
+
ChartProps,
|
|
48
|
+
ChartType,
|
|
49
|
+
ChartSeries,
|
|
50
|
+
ChartRow,
|
|
51
|
+
ChartFilter,
|
|
52
|
+
ChartFilterOption,
|
|
53
|
+
ChartSelection,
|
|
54
|
+
} from './Chart';
|