panelui-native 0.54.0 → 0.56.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.
- package/README.md +1 -0
- package/lib/module/components/combobox/index.js +61 -7
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +765 -0
- package/lib/module/components/hex-chart/index.js.map +1 -0
- package/lib/module/components/sortable/index.js +74 -12
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/steps/index.js +95 -12
- package/lib/module/components/steps/index.js.map +1 -1
- package/lib/module/components/tag-input/index.js +435 -0
- package/lib/module/components/tag-input/index.js.map +1 -0
- package/lib/module/components/tour/index.js +661 -0
- package/lib/module/components/tour/index.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +209 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts +277 -0
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/steps/index.d.ts +19 -0
- package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tag-input/index.d.ts +240 -0
- package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tour/index.d.ts +168 -0
- package/lib/typescript/src/components/tour/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +98 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/hex-chart/index.tsx +1009 -0
- package/src/components/steps/index.tsx +103 -8
- package/src/components/tour/index.tsx +933 -0
- package/src/index.ts +22 -0
- package/src/utils/chart.ts +236 -0
package/src/index.ts
CHANGED
|
@@ -258,6 +258,20 @@ export {
|
|
|
258
258
|
type HeatmapCell,
|
|
259
259
|
type HeatmapLayout,
|
|
260
260
|
} from './components/heatmap-chart';
|
|
261
|
+
export {
|
|
262
|
+
HexChart,
|
|
263
|
+
useHexChart,
|
|
264
|
+
type HexChartProps,
|
|
265
|
+
type HexChartHandle,
|
|
266
|
+
type HexChartHeaderProps,
|
|
267
|
+
type HexChartCellsProps,
|
|
268
|
+
type HexChartTooltipProps,
|
|
269
|
+
type HexChartLegendProps,
|
|
270
|
+
type HexChartSkeletonProps,
|
|
271
|
+
type HexChartStatus,
|
|
272
|
+
type HexDatum,
|
|
273
|
+
type HexShape,
|
|
274
|
+
} from './components/hex-chart';
|
|
261
275
|
export {
|
|
262
276
|
Map,
|
|
263
277
|
useMap,
|
|
@@ -919,6 +933,14 @@ export {
|
|
|
919
933
|
type TooltipOpenOn,
|
|
920
934
|
type TooltipVariant,
|
|
921
935
|
} from './components/tooltip';
|
|
936
|
+
export {
|
|
937
|
+
Tour,
|
|
938
|
+
type TourProps,
|
|
939
|
+
type TourStepProps,
|
|
940
|
+
type TourShape,
|
|
941
|
+
type TourPlacement,
|
|
942
|
+
type TourLabels,
|
|
943
|
+
} from './components/tour';
|
|
922
944
|
export {
|
|
923
945
|
Typography,
|
|
924
946
|
type TypographyProps,
|
package/src/utils/chart.ts
CHANGED
|
@@ -504,6 +504,242 @@ export function radarPath(
|
|
|
504
504
|
return `${path}Z`;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
/**
|
|
508
|
+
* A pointy-top hexagon's metrics, from the radius of the circle through its
|
|
509
|
+
* corners.
|
|
510
|
+
*
|
|
511
|
+
* Pointy-top rather than flat-top because it is the rows that have to tile: a
|
|
512
|
+
* row of pointy-top cells has a straight top and bottom edge, so the next row
|
|
513
|
+
* nests into it by half a cell and the field reads as a honeycomb. Flat-top
|
|
514
|
+
* cells tile by column instead, which gives the same shape turned a quarter
|
|
515
|
+
* turn and a field that is taller than it is wide for the same cell count.
|
|
516
|
+
*/
|
|
517
|
+
export interface HexMetrics {
|
|
518
|
+
/** Centre to corner. */
|
|
519
|
+
radius: number;
|
|
520
|
+
/** Across the flats — the full width of one cell. */
|
|
521
|
+
width: number;
|
|
522
|
+
/** Point to point — the full height of one cell. */
|
|
523
|
+
height: number;
|
|
524
|
+
/** Centre to centre along a row. */
|
|
525
|
+
stepX: number;
|
|
526
|
+
/**
|
|
527
|
+
* Centre to centre between rows. Three quarters of the height, not all of
|
|
528
|
+
* it: consecutive rows interlock, and each one only costs the height of the
|
|
529
|
+
* cell less the point it slots into.
|
|
530
|
+
*/
|
|
531
|
+
stepY: number;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function hexMetrics(radius: number): HexMetrics {
|
|
535
|
+
'worklet';
|
|
536
|
+
const width = Math.sqrt(3) * radius;
|
|
537
|
+
return { radius, width, height: radius * 2, stepX: width, stepY: radius * 1.5 };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** The cell radius that fits `columns` of them across `width`. */
|
|
541
|
+
export function hexRadiusFor(width: number, columns: number): number {
|
|
542
|
+
'worklet';
|
|
543
|
+
if (width <= 0 || columns <= 0) return 0;
|
|
544
|
+
// Half a cell wider than the columns alone: the odd rows are offset by that
|
|
545
|
+
// much, and a radius derived without it runs them off the right edge.
|
|
546
|
+
return width / (columns + 0.5) / Math.sqrt(3);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** How many rows of cells fit in `height`. */
|
|
550
|
+
export function hexRowsFor(height: number, metrics: HexMetrics): number {
|
|
551
|
+
'worklet';
|
|
552
|
+
if (height <= 0 || metrics.stepY <= 0) return 0;
|
|
553
|
+
return Math.max(1, Math.floor((height - metrics.height / 4) / metrics.stepY));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** The centre of the cell at `column`, `row`, with odd rows nested half a cell right. */
|
|
557
|
+
export function hexCenter(
|
|
558
|
+
column: number,
|
|
559
|
+
row: number,
|
|
560
|
+
metrics: HexMetrics,
|
|
561
|
+
left: number,
|
|
562
|
+
top: number
|
|
563
|
+
): ChartPoint {
|
|
564
|
+
'worklet';
|
|
565
|
+
const offset = row % 2 === 1 ? metrics.stepX / 2 : 0;
|
|
566
|
+
return {
|
|
567
|
+
x: left + metrics.stepX / 2 + offset + column * metrics.stepX,
|
|
568
|
+
y: top + metrics.radius + row * metrics.stepY,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* One hexagon as a closed path.
|
|
574
|
+
*
|
|
575
|
+
* Corners are turns from twelve o'clock like everything else polar in this
|
|
576
|
+
* file, which puts the first one straight up — the definition of pointy-top.
|
|
577
|
+
*
|
|
578
|
+
* The coordinates are rounded to two decimals because these paths are
|
|
579
|
+
* concatenated by the hundred: a field is one path string per series rather
|
|
580
|
+
* than one node per cell, and full float precision would make each of those
|
|
581
|
+
* strings several times longer for a difference no display can resolve.
|
|
582
|
+
*/
|
|
583
|
+
export function hexPath(cx: number, cy: number, radius: number): string {
|
|
584
|
+
'worklet';
|
|
585
|
+
if (radius <= 0) return '';
|
|
586
|
+
let d = '';
|
|
587
|
+
for (let corner = 0; corner < 6; corner += 1) {
|
|
588
|
+
const point = polarPoint(cx, cy, radius, corner / 6);
|
|
589
|
+
d += `${corner === 0 ? 'M' : 'L'}${point.x.toFixed(2)},${point.y.toFixed(2)}`;
|
|
590
|
+
}
|
|
591
|
+
return `${d}Z`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* A number in 0…1 from a pair of coordinates, the same one every time.
|
|
596
|
+
*
|
|
597
|
+
* `Math.random` would give the honeycomb a different edge on every render,
|
|
598
|
+
* which turns a re-render into an animation nobody asked for and means the
|
|
599
|
+
* same data never screenshots twice.
|
|
600
|
+
*/
|
|
601
|
+
export function hashUnit(a: number, b: number): number {
|
|
602
|
+
'worklet';
|
|
603
|
+
let h = Math.imul(a + 1, 374761393) ^ Math.imul(b + 1, 668265263);
|
|
604
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
605
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* How many cells each value gets out of `budget`, by largest remainder.
|
|
610
|
+
*
|
|
611
|
+
* Rounding each share on its own does not add up — three equal parts of a
|
|
612
|
+
* hundred round to 33 each and leave one over — and a spare cell in a
|
|
613
|
+
* honeycomb is not a rounding error the reader can shrug off, it is a cell of
|
|
614
|
+
* some colour that nothing in the data accounts for. Flooring every share and
|
|
615
|
+
* then handing the leftovers to the largest fractions spends the budget
|
|
616
|
+
* exactly, and spends it on the series with the strongest claim to each one.
|
|
617
|
+
*/
|
|
618
|
+
export function shareCounts(values: number[], budget: number): number[] {
|
|
619
|
+
const total = values.reduce((sum, value) => sum + Math.max(0, value), 0);
|
|
620
|
+
if (total <= 0 || budget <= 0) return values.map(() => 0);
|
|
621
|
+
|
|
622
|
+
const exact = values.map((value) => (Math.max(0, value) / total) * budget);
|
|
623
|
+
const counts = exact.map((value) => Math.floor(value));
|
|
624
|
+
// Every fraction is under one, so there is always less than one cell per
|
|
625
|
+
// series left over and a single pass down the order places all of them. The
|
|
626
|
+
// clamp is against float drift in the sum rather than against the maths.
|
|
627
|
+
const spare = Math.min(
|
|
628
|
+
budget - counts.reduce((sum, count) => sum + count, 0),
|
|
629
|
+
values.length
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const byFraction = exact
|
|
633
|
+
.map((value, index) => ({ index, fraction: value - Math.floor(value) }))
|
|
634
|
+
.sort((a, b) => b.fraction - a.fraction || a.index - b.index);
|
|
635
|
+
|
|
636
|
+
for (let i = 0; i < spare; i += 1) {
|
|
637
|
+
const index = byFraction[i]!.index;
|
|
638
|
+
counts[index] = counts[index]! + 1;
|
|
639
|
+
}
|
|
640
|
+
return counts;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** How the filled cells are arranged in the field. */
|
|
644
|
+
export type HexShape = 'blob' | 'grid';
|
|
645
|
+
|
|
646
|
+
export interface HexCell {
|
|
647
|
+
column: number;
|
|
648
|
+
row: number;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** How far off its ring a cell may be nudged, in field widths. About one cell. */
|
|
652
|
+
const BLOB_WOBBLE = 0.09;
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Every cell of the field, in the order the series fill it.
|
|
656
|
+
*
|
|
657
|
+
* `grid` is reading order, which is the honest arrangement: counting cells off
|
|
658
|
+
* a row is something a reader can actually do. `blob` grows out from the middle
|
|
659
|
+
* instead, which counts for nothing but shows the shape of the split at a
|
|
660
|
+
* glance — the smallest series in the centre with each larger one wrapped
|
|
661
|
+
* around it.
|
|
662
|
+
*
|
|
663
|
+
* Two things make the blob look grown rather than stamped. Distance is measured
|
|
664
|
+
* in the field's own proportions, so a wide field grows a wide blob instead of
|
|
665
|
+
* a circle with empty shoulders either side of it; and each cell's distance is
|
|
666
|
+
* nudged by a hash of its own coordinates, so the boundary between two series
|
|
667
|
+
* comes out ragged instead of as a clean arc. The nudge is a hash and not a
|
|
668
|
+
* random number, so the same data draws the same honeycomb every time.
|
|
669
|
+
*
|
|
670
|
+
* Computed once per layout rather than per frame, so it is a plain function.
|
|
671
|
+
*/
|
|
672
|
+
export function hexFillOrder(columns: number, rows: number, shape: HexShape): HexCell[] {
|
|
673
|
+
const cells: HexCell[] = [];
|
|
674
|
+
for (let row = 0; row < rows; row += 1) {
|
|
675
|
+
for (let column = 0; column < columns; column += 1) {
|
|
676
|
+
cells.push({ column, row });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (shape === 'grid' || columns < 2 || rows < 2) return cells;
|
|
680
|
+
|
|
681
|
+
const midColumn = (columns - 1) / 2;
|
|
682
|
+
const midRow = (rows - 1) / 2;
|
|
683
|
+
|
|
684
|
+
return cells
|
|
685
|
+
.map((cell) => {
|
|
686
|
+
// Odd rows sit half a cell right, and a distance measured without that
|
|
687
|
+
// leans the whole blob to one side.
|
|
688
|
+
const column = cell.column + (cell.row % 2 === 1 ? 0.5 : 0);
|
|
689
|
+
const dx = (column - midColumn) / columns;
|
|
690
|
+
const dy = (cell.row - midRow) / rows;
|
|
691
|
+
const wobble = (hashUnit(cell.column, cell.row) - 0.5) * BLOB_WOBBLE;
|
|
692
|
+
return { cell, key: Math.sqrt(dx * dx + dy * dy) + wobble };
|
|
693
|
+
})
|
|
694
|
+
.sort((a, b) => a.key - b.key)
|
|
695
|
+
.map((entry) => entry.cell);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* The cell under a point, or null when the point is between cells or outside
|
|
700
|
+
* the field.
|
|
701
|
+
*
|
|
702
|
+
* Only three rows can be near any y, and one column near any x within a row, so
|
|
703
|
+
* this checks six candidates rather than solving the cube-rounding — the same
|
|
704
|
+
* answer, and short enough to read.
|
|
705
|
+
*/
|
|
706
|
+
export function hexAt(
|
|
707
|
+
x: number,
|
|
708
|
+
y: number,
|
|
709
|
+
metrics: HexMetrics,
|
|
710
|
+
left: number,
|
|
711
|
+
top: number,
|
|
712
|
+
columns: number,
|
|
713
|
+
rows: number
|
|
714
|
+
): HexCell | null {
|
|
715
|
+
'worklet';
|
|
716
|
+
if (metrics.radius <= 0) return null;
|
|
717
|
+
|
|
718
|
+
const approximateRow = Math.round((y - top - metrics.radius) / metrics.stepY);
|
|
719
|
+
let best: HexCell | null = null;
|
|
720
|
+
let bestDistance = Infinity;
|
|
721
|
+
|
|
722
|
+
for (let row = approximateRow - 1; row <= approximateRow + 1; row += 1) {
|
|
723
|
+
if (row < 0 || row >= rows) continue;
|
|
724
|
+
const offset = row % 2 === 1 ? metrics.stepX / 2 : 0;
|
|
725
|
+
const column = Math.round((x - left - metrics.stepX / 2 - offset) / metrics.stepX);
|
|
726
|
+
if (column < 0 || column >= columns) continue;
|
|
727
|
+
|
|
728
|
+
const centre = hexCenter(column, row, metrics, left, top);
|
|
729
|
+
const dx = x - centre.x;
|
|
730
|
+
const dy = y - centre.y;
|
|
731
|
+
const distance = dx * dx + dy * dy;
|
|
732
|
+
if (distance < bestDistance) {
|
|
733
|
+
bestDistance = distance;
|
|
734
|
+
best = { column, row };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Further than a radius from the nearest centre is outside the honeycomb, not
|
|
739
|
+
// a near miss worth rounding into it.
|
|
740
|
+
return best !== null && bestDistance <= metrics.radius * metrics.radius ? best : null;
|
|
741
|
+
}
|
|
742
|
+
|
|
507
743
|
/** The numbers of one column, with anything unusable left as a gap. */
|
|
508
744
|
export function columnValues(
|
|
509
745
|
data: Record<string, unknown>[],
|