react-threat-map 0.2.3 → 0.2.4
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 +36 -0
- package/dist/index.cjs +38 -9
- package/dist/index.js +38 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ import { ThreatMap } from 'react-threat-map';
|
|
|
32
32
|
## Contents
|
|
33
33
|
|
|
34
34
|
- [Install](#install)
|
|
35
|
+
- [Browser support](#browser-support)
|
|
35
36
|
- [Quick start](#quick-start)
|
|
36
37
|
- [Aggregation](#aggregation)
|
|
37
38
|
- [Customization](#customization)
|
|
@@ -62,6 +63,41 @@ React 16.14, 17, 18, and 19 are all supported.
|
|
|
62
63
|
|
|
63
64
|
---
|
|
64
65
|
|
|
66
|
+
## Browser support
|
|
67
|
+
|
|
68
|
+
| Browser | Minimum |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| Chrome / Edge | 88 |
|
|
71
|
+
| Firefox | 89 |
|
|
72
|
+
| Safari (macOS) | 15 |
|
|
73
|
+
| Safari (iOS) | 15 |
|
|
74
|
+
|
|
75
|
+
The floor is set by CSS [`aspect-ratio`][ar], which is how a `<ThreatMap>` given no
|
|
76
|
+
height derives one — the canvases are absolutely positioned, so nothing else in the
|
|
77
|
+
box has a height to contribute. Below that, the component falls back to a
|
|
78
|
+
`min-height` computed from the measured width, so it still renders; the map just
|
|
79
|
+
stops responding to a container whose height is set purely in CSS.
|
|
80
|
+
|
|
81
|
+
Everything else the library touches is older than that floor: Canvas 2D with
|
|
82
|
+
`Path2D` and the `lighter` composite operation, `ResizeObserver` (feature-detected,
|
|
83
|
+
with a `getBoundingClientRect` fallback), and `matchMedia` (both the modern
|
|
84
|
+
`addEventListener` and the `addListener` form Safari needed before 14). The
|
|
85
|
+
published bundle is ES2020 and ships no polyfills.
|
|
86
|
+
|
|
87
|
+
Two things degrade quietly rather than breaking, both on Safari below 16:
|
|
88
|
+
|
|
89
|
+
- The `resolution` media feature is missing, so the canvas is not re-sharpened when
|
|
90
|
+
a window moves to a display with a different pixel ratio. The ratio picked at
|
|
91
|
+
mount stays.
|
|
92
|
+
- `prefers-reduced-motion` still works — that query has been supported since Safari
|
|
93
|
+
10.1.
|
|
94
|
+
|
|
95
|
+
There is no IE11 support and none is planned; `Path2D` alone rules it out.
|
|
96
|
+
|
|
97
|
+
[ar]: https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
65
101
|
## Quick start
|
|
66
102
|
|
|
67
103
|
The only required prop is `attacks`. The map sizes itself to its container, loads its own
|
package/dist/index.cjs
CHANGED
|
@@ -430,17 +430,24 @@ function usePixelRatio() {
|
|
|
430
430
|
const [ratio, setRatio] = react.useState(currentRatio);
|
|
431
431
|
react.useEffect(() => {
|
|
432
432
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
433
|
-
let
|
|
433
|
+
let unlisten;
|
|
434
434
|
const update = () => {
|
|
435
|
+
unlisten?.();
|
|
435
436
|
setRatio(currentRatio());
|
|
436
437
|
listen();
|
|
437
438
|
};
|
|
438
439
|
const listen = () => {
|
|
439
|
-
query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
440
|
-
query.addEventListener
|
|
440
|
+
const query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
441
|
+
if (typeof query.addEventListener === "function") {
|
|
442
|
+
query.addEventListener("change", update);
|
|
443
|
+
unlisten = () => query.removeEventListener("change", update);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
query.addListener(update);
|
|
447
|
+
unlisten = () => query.removeListener(update);
|
|
441
448
|
};
|
|
442
449
|
listen();
|
|
443
|
-
return () =>
|
|
450
|
+
return () => unlisten?.();
|
|
444
451
|
}, []);
|
|
445
452
|
return ratio;
|
|
446
453
|
}
|
|
@@ -452,11 +459,23 @@ function useReducedMotion() {
|
|
|
452
459
|
const query = window.matchMedia(QUERY);
|
|
453
460
|
setReduced(query.matches);
|
|
454
461
|
const update = (event) => setReduced(event.matches);
|
|
455
|
-
query.addEventListener
|
|
456
|
-
|
|
462
|
+
if (typeof query.addEventListener === "function") {
|
|
463
|
+
query.addEventListener("change", update);
|
|
464
|
+
return () => query.removeEventListener("change", update);
|
|
465
|
+
}
|
|
466
|
+
query.addListener(update);
|
|
467
|
+
return () => query.removeListener(update);
|
|
457
468
|
}, []);
|
|
458
469
|
return reduced;
|
|
459
470
|
}
|
|
471
|
+
function useSupportsAspectRatio() {
|
|
472
|
+
const [supported, setSupported] = react.useState(true);
|
|
473
|
+
react.useEffect(() => {
|
|
474
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
|
|
475
|
+
setSupported(CSS.supports("aspect-ratio", "1"));
|
|
476
|
+
}, []);
|
|
477
|
+
return supported;
|
|
478
|
+
}
|
|
460
479
|
function configEqual(a, b) {
|
|
461
480
|
const aKeys = Object.keys(a);
|
|
462
481
|
if (aKeys.length !== Object.keys(b).length) return false;
|
|
@@ -579,7 +598,7 @@ function BaseMapCanvas(props) {
|
|
|
579
598
|
ref: canvasRef,
|
|
580
599
|
width: Math.max(1, Math.round(width * pixelRatio)),
|
|
581
600
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
582
|
-
style: { position: "absolute",
|
|
601
|
+
style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
|
|
583
602
|
"aria-hidden": "true"
|
|
584
603
|
}
|
|
585
604
|
);
|
|
@@ -1224,7 +1243,9 @@ function ThreatCanvas(props) {
|
|
|
1224
1243
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
1225
1244
|
style: {
|
|
1226
1245
|
position: "absolute",
|
|
1227
|
-
|
|
1246
|
+
// Longhand: the `inset` shorthand is Safari 14.1+.
|
|
1247
|
+
top: 0,
|
|
1248
|
+
left: 0,
|
|
1228
1249
|
width: "100%",
|
|
1229
1250
|
height: "100%",
|
|
1230
1251
|
// Without handlers the canvas must not eat pointer events aimed at
|
|
@@ -1274,6 +1295,7 @@ function ThreatMap(props) {
|
|
|
1274
1295
|
);
|
|
1275
1296
|
const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
|
|
1276
1297
|
const pixelRatio = usePixelRatio();
|
|
1298
|
+
const supportsAspectRatio = useSupportsAspectRatio();
|
|
1277
1299
|
const width = widthProp ?? measured?.width ?? 0;
|
|
1278
1300
|
const height = heightProp ?? (measured && measured.height > 0 ? measured.height : width > 0 ? defaultHeightFor(projectionProp, width) : 0);
|
|
1279
1301
|
const projection = react.useMemo(
|
|
@@ -1331,7 +1353,14 @@ function ThreatMap(props) {
|
|
|
1331
1353
|
// a flex parent, or the consumer's own `style` below — so it sets a
|
|
1332
1354
|
// floor without taking the decision away from them. The value is a string
|
|
1333
1355
|
// for React 16.14/17's sake; see aspectRatioStyleFor.
|
|
1334
|
-
|
|
1356
|
+
//
|
|
1357
|
+
// Where `aspect-ratio` is not implemented at all (Safari/iOS <15, Chrome
|
|
1358
|
+
// <88, Firefox <89) the declaration is dropped and the wrapper collapses,
|
|
1359
|
+
// so fall back to a `min-height` derived from the width. `min-height`
|
|
1360
|
+
// rather than `height` keeps the "floor, not a decision" property, and
|
|
1361
|
+
// deriving it from the *width* rather than the measured height is what
|
|
1362
|
+
// stops it feeding back into its own measurement.
|
|
1363
|
+
...heightProp !== void 0 ? { height: heightProp } : supportsAspectRatio ? { aspectRatio: aspectRatioStyleFor(projectionProp) } : width > 0 ? { minHeight: defaultHeightFor(projectionProp, width) } : {},
|
|
1335
1364
|
...style
|
|
1336
1365
|
},
|
|
1337
1366
|
role: "img",
|
package/dist/index.js
CHANGED
|
@@ -429,17 +429,24 @@ function usePixelRatio() {
|
|
|
429
429
|
const [ratio, setRatio] = useState(currentRatio);
|
|
430
430
|
useEffect(() => {
|
|
431
431
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
432
|
-
let
|
|
432
|
+
let unlisten;
|
|
433
433
|
const update = () => {
|
|
434
|
+
unlisten?.();
|
|
434
435
|
setRatio(currentRatio());
|
|
435
436
|
listen();
|
|
436
437
|
};
|
|
437
438
|
const listen = () => {
|
|
438
|
-
query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
439
|
-
query.addEventListener
|
|
439
|
+
const query = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
440
|
+
if (typeof query.addEventListener === "function") {
|
|
441
|
+
query.addEventListener("change", update);
|
|
442
|
+
unlisten = () => query.removeEventListener("change", update);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
query.addListener(update);
|
|
446
|
+
unlisten = () => query.removeListener(update);
|
|
440
447
|
};
|
|
441
448
|
listen();
|
|
442
|
-
return () =>
|
|
449
|
+
return () => unlisten?.();
|
|
443
450
|
}, []);
|
|
444
451
|
return ratio;
|
|
445
452
|
}
|
|
@@ -451,11 +458,23 @@ function useReducedMotion() {
|
|
|
451
458
|
const query = window.matchMedia(QUERY);
|
|
452
459
|
setReduced(query.matches);
|
|
453
460
|
const update = (event) => setReduced(event.matches);
|
|
454
|
-
query.addEventListener
|
|
455
|
-
|
|
461
|
+
if (typeof query.addEventListener === "function") {
|
|
462
|
+
query.addEventListener("change", update);
|
|
463
|
+
return () => query.removeEventListener("change", update);
|
|
464
|
+
}
|
|
465
|
+
query.addListener(update);
|
|
466
|
+
return () => query.removeListener(update);
|
|
456
467
|
}, []);
|
|
457
468
|
return reduced;
|
|
458
469
|
}
|
|
470
|
+
function useSupportsAspectRatio() {
|
|
471
|
+
const [supported, setSupported] = useState(true);
|
|
472
|
+
useEffect(() => {
|
|
473
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
|
|
474
|
+
setSupported(CSS.supports("aspect-ratio", "1"));
|
|
475
|
+
}, []);
|
|
476
|
+
return supported;
|
|
477
|
+
}
|
|
459
478
|
function configEqual(a, b) {
|
|
460
479
|
const aKeys = Object.keys(a);
|
|
461
480
|
if (aKeys.length !== Object.keys(b).length) return false;
|
|
@@ -578,7 +597,7 @@ function BaseMapCanvas(props) {
|
|
|
578
597
|
ref: canvasRef,
|
|
579
598
|
width: Math.max(1, Math.round(width * pixelRatio)),
|
|
580
599
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
581
|
-
style: { position: "absolute",
|
|
600
|
+
style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
|
|
582
601
|
"aria-hidden": "true"
|
|
583
602
|
}
|
|
584
603
|
);
|
|
@@ -1223,7 +1242,9 @@ function ThreatCanvas(props) {
|
|
|
1223
1242
|
height: Math.max(1, Math.round(height * pixelRatio)),
|
|
1224
1243
|
style: {
|
|
1225
1244
|
position: "absolute",
|
|
1226
|
-
|
|
1245
|
+
// Longhand: the `inset` shorthand is Safari 14.1+.
|
|
1246
|
+
top: 0,
|
|
1247
|
+
left: 0,
|
|
1227
1248
|
width: "100%",
|
|
1228
1249
|
height: "100%",
|
|
1229
1250
|
// Without handlers the canvas must not eat pointer events aimed at
|
|
@@ -1273,6 +1294,7 @@ function ThreatMap(props) {
|
|
|
1273
1294
|
);
|
|
1274
1295
|
const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
|
|
1275
1296
|
const pixelRatio = usePixelRatio();
|
|
1297
|
+
const supportsAspectRatio = useSupportsAspectRatio();
|
|
1276
1298
|
const width = widthProp ?? measured?.width ?? 0;
|
|
1277
1299
|
const height = heightProp ?? (measured && measured.height > 0 ? measured.height : width > 0 ? defaultHeightFor(projectionProp, width) : 0);
|
|
1278
1300
|
const projection = useMemo(
|
|
@@ -1330,7 +1352,14 @@ function ThreatMap(props) {
|
|
|
1330
1352
|
// a flex parent, or the consumer's own `style` below — so it sets a
|
|
1331
1353
|
// floor without taking the decision away from them. The value is a string
|
|
1332
1354
|
// for React 16.14/17's sake; see aspectRatioStyleFor.
|
|
1333
|
-
|
|
1355
|
+
//
|
|
1356
|
+
// Where `aspect-ratio` is not implemented at all (Safari/iOS <15, Chrome
|
|
1357
|
+
// <88, Firefox <89) the declaration is dropped and the wrapper collapses,
|
|
1358
|
+
// so fall back to a `min-height` derived from the width. `min-height`
|
|
1359
|
+
// rather than `height` keeps the "floor, not a decision" property, and
|
|
1360
|
+
// deriving it from the *width* rather than the measured height is what
|
|
1361
|
+
// stops it feeding back into its own measurement.
|
|
1362
|
+
...heightProp !== void 0 ? { height: heightProp } : supportsAspectRatio ? { aspectRatio: aspectRatioStyleFor(projectionProp) } : width > 0 ? { minHeight: defaultHeightFor(projectionProp, width) } : {},
|
|
1334
1363
|
...style
|
|
1335
1364
|
},
|
|
1336
1365
|
role: "img",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-threat-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "A React component that renders animated cyberattack threats on a static world map, with intelligent per-region aggregation and first-class US state support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|