@project-gridmap/library 0.1.0 → 0.1.2
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 -5
- package/package.json +2 -2
- package/src/gridmap.js +112 -13
package/README.md
CHANGED
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
Project Gridmap is a content-agnostic hierarchical grid map renderer. It knows
|
|
4
4
|
about layers, groups, items, and cells. It does not know what those things mean.
|
|
5
5
|
|
|
6
|
-
The current root `index.html` is still the standalone Bible demo for GitHub
|
|
7
|
-
Pages. The importable package in `src/` is deliberately generic so it can power
|
|
8
|
-
that demo, The Bible Game, and other ordered datasets.
|
|
9
|
-
|
|
10
6
|
## Install
|
|
11
7
|
|
|
12
8
|
```bash
|
|
13
|
-
npm install @project-gridmap/
|
|
9
|
+
npm install @project-gridmap/library
|
|
14
10
|
```
|
|
15
11
|
|
|
16
12
|
## Usage
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@project-gridmap/library",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A content-agnostic hierarchical grid map renderer
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "A content-agnostic hierarchical grid map renderer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
package/src/gridmap.js
CHANGED
|
@@ -201,6 +201,8 @@ export class Gridmap {
|
|
|
201
201
|
this.medianValue = median(values);
|
|
202
202
|
const sortedValues = values.filter((value) => Number.isFinite(value)).sort((a, b) => a - b);
|
|
203
203
|
this.referenceValue = sortedValues[Math.floor(Math.max(0, sortedValues.length - 1) * 0.99)] ?? this.medianValue;
|
|
204
|
+
const sizes = this.model.cells.map((cell) => this.numberWorldSize(cell)).sort((a, b) => a - b);
|
|
205
|
+
this.medianNumberSize = sizes[sizes.length >> 1] || this.model.cellSide;
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
viewport() {
|
|
@@ -564,7 +566,15 @@ export class Gridmap {
|
|
|
564
566
|
|
|
565
567
|
const selected = this.state.selected;
|
|
566
568
|
if (selected) {
|
|
569
|
+
const item = this.model.items[selected.itemIndex];
|
|
567
570
|
const colour = this.colourOf(selected, theme.text);
|
|
571
|
+
ctx.strokeStyle = colour;
|
|
572
|
+
ctx.globalAlpha = 1;
|
|
573
|
+
ctx.lineWidth = 1.5;
|
|
574
|
+
ctx.beginPath();
|
|
575
|
+
rect(item);
|
|
576
|
+
ctx.stroke();
|
|
577
|
+
|
|
568
578
|
ctx.strokeStyle = colour;
|
|
569
579
|
ctx.fillStyle = colour;
|
|
570
580
|
ctx.globalAlpha = 0.1;
|
|
@@ -598,34 +608,95 @@ export class Gridmap {
|
|
|
598
608
|
this.renderLabels(X, Y);
|
|
599
609
|
}
|
|
600
610
|
|
|
611
|
+
markScale(cell, floor = 0.4) {
|
|
612
|
+
return this.options.relativeMarkSize && cell.value
|
|
613
|
+
? Math.max(floor, Math.sqrt(cell.value / this.medianValue))
|
|
614
|
+
: 1;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
numberWorldSize(cell) {
|
|
618
|
+
const digits = String(cell.label).length;
|
|
619
|
+
const share = this.options.relativeMarkSize && cell.value
|
|
620
|
+
? Math.min(1, Math.max(0.35, Math.sqrt(cell.value / this.referenceValue)))
|
|
621
|
+
: 0.7;
|
|
622
|
+
return Math.min(
|
|
623
|
+
Math.min(cell.width, cell.height) * this.options.markMaxSize * share,
|
|
624
|
+
cell.width * 0.7 / (digits * 0.6),
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
useNumbers() {
|
|
629
|
+
return this.options.markType === 'number' && this.medianNumberSize * this.camera.k >= this.options.numberMinPx;
|
|
630
|
+
}
|
|
631
|
+
|
|
601
632
|
renderMarks(ctx, X, Y, onScreen) {
|
|
602
633
|
const theme = this.options.theme;
|
|
603
|
-
const useNumbers = this.
|
|
634
|
+
const useNumbers = this.useNumbers();
|
|
635
|
+
const worldK = this.frame(this.model.world).k;
|
|
636
|
+
const dot = Math.min(7, 3 * (this.camera.k / worldK) ** 0.28);
|
|
637
|
+
const damping = useNumbers
|
|
638
|
+
? Math.min(1, 24 / (this.medianNumberSize * this.camera.k))
|
|
639
|
+
: 1;
|
|
640
|
+
|
|
604
641
|
ctx.textAlign = 'center';
|
|
605
642
|
ctx.textBaseline = 'middle';
|
|
606
643
|
for (const item of this.model.items) {
|
|
607
644
|
ctx.fillStyle = this.colourOf(item, theme.text);
|
|
608
645
|
for (const cell of item.cells) {
|
|
609
646
|
if (!onScreen(cell)) continue;
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
: 0.7;
|
|
613
|
-
const size = Math.min(cell.width, cell.height) * this.options.markMaxSize * scale * this.camera.k;
|
|
614
|
-
ctx.globalAlpha = cell === this.state.hover || cell === this.state.selected ? 1 : this.options.markOpacity;
|
|
647
|
+
const strong = cell === this.state.hover || cell === this.state.selected;
|
|
648
|
+
ctx.globalAlpha = strong ? 1 : this.options.markOpacity;
|
|
615
649
|
if (useNumbers) {
|
|
616
|
-
|
|
650
|
+
const size = this.numberWorldSize(cell) * this.camera.k * damping;
|
|
651
|
+
if (size < 2) continue;
|
|
652
|
+
ctx.font = `${strong ? 500 : 400} ${Math.max(2, Math.min(24, size))}px ${theme.font}`;
|
|
617
653
|
ctx.fillText(cell.label, X(cell.centerX), Y(cell.centerY));
|
|
618
654
|
} else {
|
|
619
|
-
const radius =
|
|
655
|
+
const radius = dot * this.markScale(cell) / 2;
|
|
620
656
|
ctx.beginPath();
|
|
621
657
|
ctx.arc(X(cell.centerX), Y(cell.centerY), radius, 0, Math.PI * 2);
|
|
622
658
|
ctx.fill();
|
|
623
659
|
}
|
|
624
660
|
}
|
|
625
661
|
}
|
|
662
|
+
|
|
663
|
+
this.renderMarkEmphasis(ctx, X, Y, useNumbers, dot, damping);
|
|
626
664
|
ctx.globalAlpha = 1;
|
|
627
665
|
}
|
|
628
666
|
|
|
667
|
+
renderMarkEmphasis(ctx, X, Y, useNumbers, dot, damping) {
|
|
668
|
+
const hover = this.state.hover;
|
|
669
|
+
const selected = this.state.selected;
|
|
670
|
+
|
|
671
|
+
if (!useNumbers && hover) {
|
|
672
|
+
const colour = this.colourOf(hover, this.options.theme.text);
|
|
673
|
+
const radius = dot * this.markScale(hover) / 2;
|
|
674
|
+
ctx.fillStyle = colour;
|
|
675
|
+
ctx.strokeStyle = colour;
|
|
676
|
+
ctx.globalAlpha = 1;
|
|
677
|
+
ctx.beginPath();
|
|
678
|
+
ctx.arc(X(hover.centerX), Y(hover.centerY), radius + 1.2, 0, Math.PI * 2);
|
|
679
|
+
ctx.fill();
|
|
680
|
+
ctx.globalAlpha = 0.35;
|
|
681
|
+
ctx.beginPath();
|
|
682
|
+
ctx.arc(X(hover.centerX), Y(hover.centerY), radius + 7, 0, Math.PI * 2);
|
|
683
|
+
ctx.stroke();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (selected) {
|
|
687
|
+
const colour = this.colourOf(selected, this.options.theme.text);
|
|
688
|
+
const numberSize = this.numberWorldSize(selected) * this.camera.k * damping;
|
|
689
|
+
const numberRadius = Math.max(numberSize * 0.55, String(selected.label).length * 0.6 * numberSize / 2);
|
|
690
|
+
const starRadius = dot * this.markScale(selected) / 2;
|
|
691
|
+
ctx.strokeStyle = colour;
|
|
692
|
+
ctx.globalAlpha = 1;
|
|
693
|
+
ctx.lineWidth = 1;
|
|
694
|
+
ctx.beginPath();
|
|
695
|
+
ctx.arc(X(selected.centerX), Y(selected.centerY), useNumbers ? numberRadius + 5 : starRadius + 6, 0, Math.PI * 2);
|
|
696
|
+
ctx.stroke();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
629
700
|
renderVeil(ctx, rect) {
|
|
630
701
|
const region = this.focusRegion();
|
|
631
702
|
if (!region) return;
|
|
@@ -642,27 +713,55 @@ export class Gridmap {
|
|
|
642
713
|
renderLabels(X, Y) {
|
|
643
714
|
const labelItem = this.options.labels.item;
|
|
644
715
|
const selected = this.state.selected;
|
|
716
|
+
const hover = this.state.hover;
|
|
645
717
|
this.overlay.replaceChildren();
|
|
718
|
+
|
|
719
|
+
for (const layer of this.model.layers) {
|
|
720
|
+
const h = layer.height * this.camera.k;
|
|
721
|
+
const text = layer.label.toUpperCase();
|
|
722
|
+
if (h < text.length * 7.5) continue;
|
|
723
|
+
const el = createElement('div', { text }, this.overlay);
|
|
724
|
+
Object.assign(el.style, {
|
|
725
|
+
position: 'absolute',
|
|
726
|
+
left: `${X(layer.x) - 22}px`,
|
|
727
|
+
top: `${Y(layer.y + layer.height / 2)}px`,
|
|
728
|
+
transform: 'translate(-50%, -50%) rotate(-90deg)',
|
|
729
|
+
transformOrigin: 'center',
|
|
730
|
+
color: selected?.layerIndex === layer.index ? this.options.theme.text : this.options.theme.mutedText,
|
|
731
|
+
fontSize: '9.5px',
|
|
732
|
+
letterSpacing: '0.32em',
|
|
733
|
+
whiteSpace: 'nowrap',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
646
737
|
for (const item of this.model.items) {
|
|
647
738
|
const w = item.width * this.camera.k;
|
|
648
739
|
const h = item.height * this.camera.k;
|
|
649
|
-
if (
|
|
650
|
-
const
|
|
740
|
+
if (h < 22) continue;
|
|
741
|
+
const full = typeof labelItem === 'function'
|
|
651
742
|
? labelItem(item, this)
|
|
652
743
|
: this.options.itemLabels === 'full' ? item.label : item.shortLabel;
|
|
653
|
-
const
|
|
744
|
+
const text = `${item.index + 1}.${full}`.toUpperCase();
|
|
745
|
+
const number = String(item.index + 1);
|
|
746
|
+
const label = text.length * 7.3 + 20 <= w ? text : number.length * 7.3 + 14 <= w ? number : null;
|
|
747
|
+
if (!label) continue;
|
|
748
|
+
|
|
749
|
+
const el = createElement('div', { text: label }, this.overlay);
|
|
654
750
|
Object.assign(el.style, {
|
|
655
751
|
position: 'absolute',
|
|
656
752
|
left: `${X(item.x) + 5}px`,
|
|
657
753
|
top: `${Y(item.y) - 6}px`,
|
|
658
|
-
maxWidth: `${Math.max(20, w -
|
|
754
|
+
maxWidth: `${Math.max(20, w - 8)}px`,
|
|
659
755
|
overflow: 'hidden',
|
|
660
756
|
whiteSpace: 'nowrap',
|
|
661
757
|
textOverflow: 'clip',
|
|
662
758
|
color: this.colourOf(item, this.options.theme.mutedText),
|
|
663
759
|
background: this.options.theme.background,
|
|
664
760
|
padding: '0 4px',
|
|
665
|
-
|
|
761
|
+
fontSize: '9px',
|
|
762
|
+
letterSpacing: '0.14em',
|
|
763
|
+
lineHeight: '1.33',
|
|
764
|
+
opacity: selected?.itemIndex === item.index || hover?.itemIndex === item.index ? '1' : '0.72',
|
|
666
765
|
});
|
|
667
766
|
}
|
|
668
767
|
}
|