react-threat-map 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 +10 -3
- package/dist/index.cjs +49 -9
- package/dist/index.js +49 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Animated cyberattack threats on a static world map, for React. Aggregates attack
|
|
|
8
8
|
origin region — with **US states as first-class origins** — so a busy feed reads as a
|
|
9
9
|
map instead of a hairball.
|
|
10
10
|
|
|
11
|
+
**[→ Live demo](https://bogdantaranenko.github.io/react-threat-map/)**
|
|
12
|
+
|
|
11
13
|
```tsx
|
|
12
14
|
import { ThreatMap } from 'react-threat-map';
|
|
13
15
|
|
|
@@ -598,9 +600,14 @@ Alongside it:
|
|
|
598
600
|
|
|
599
601
|
## Examples
|
|
600
602
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
603
|
+
**[bogdantaranenko.github.io/react-threat-map](https://bogdantaranenko.github.io/react-threat-map/)** —
|
|
604
|
+
the demo below, hosted. Deployed from `main` on every push.
|
|
605
|
+
|
|
606
|
+
It covers basic usage, 500+ streaming attacks with a live FPS counter, one country under
|
|
607
|
+
fire from five origins at different volumes, aggregation strategies side by side, custom
|
|
608
|
+
theming with render hooks, and raw-coordinate reverse geocoding with hover.
|
|
609
|
+
|
|
610
|
+
To run it locally against the library source, so edits hot-reload:
|
|
604
611
|
|
|
605
612
|
```bash
|
|
606
613
|
git clone https://github.com/BogdanTaranenko/react-threat-map
|
package/dist/index.cjs
CHANGED
|
@@ -454,6 +454,40 @@ function useReducedMotion() {
|
|
|
454
454
|
}, []);
|
|
455
455
|
return reduced;
|
|
456
456
|
}
|
|
457
|
+
function configEqual(a, b) {
|
|
458
|
+
const aKeys = Object.keys(a);
|
|
459
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
460
|
+
for (const key of aKeys) {
|
|
461
|
+
const av = a[key];
|
|
462
|
+
const bv = b[key];
|
|
463
|
+
if (Object.is(av, bv)) continue;
|
|
464
|
+
if (isPlainObject(av) && isPlainObject(bv)) {
|
|
465
|
+
if (!shallowEqual(av, bv)) return false;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
function shallowEqual(a, b) {
|
|
473
|
+
const aKeys = Object.keys(a);
|
|
474
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
475
|
+
for (const key of aKeys) {
|
|
476
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
477
|
+
}
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
function isPlainObject(value) {
|
|
481
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
482
|
+
}
|
|
483
|
+
function useStableConfig(defaults, overrides) {
|
|
484
|
+
const resolved = mergeDefined(defaults, overrides);
|
|
485
|
+
const ref = react.useRef(resolved);
|
|
486
|
+
if (!configEqual(ref.current, resolved)) {
|
|
487
|
+
ref.current = resolved;
|
|
488
|
+
}
|
|
489
|
+
return ref.current;
|
|
490
|
+
}
|
|
457
491
|
function drawBaseMap(options) {
|
|
458
492
|
const { ctx, width, height, projection, geo, theme, regions } = options;
|
|
459
493
|
ctx.clearRect(0, 0, width, height);
|
|
@@ -1195,17 +1229,23 @@ function ThreatMap(props) {
|
|
|
1195
1229
|
ariaLabel = "Cyberattack threat map"
|
|
1196
1230
|
} = props;
|
|
1197
1231
|
const containerRef = react.useRef(null);
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
return props.theme
|
|
1232
|
+
const themeOverrides = react.useMemo(() => {
|
|
1233
|
+
if (!props.theme?.severityColors) return props.theme;
|
|
1234
|
+
return { ...props.theme, severityColors: { ...defaultTheme.severityColors, ...props.theme.severityColors } };
|
|
1201
1235
|
}, [props.theme]);
|
|
1202
|
-
const
|
|
1203
|
-
const
|
|
1236
|
+
const theme = useStableConfig(defaultTheme, themeOverrides);
|
|
1237
|
+
const line = useStableConfig(defaultLineStyle, props.line);
|
|
1238
|
+
const regions = useStableConfig(defaultRegions, props.regions);
|
|
1204
1239
|
const reducedMotion = useReducedMotion();
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1240
|
+
const baseAnimation = useStableConfig(defaultAnimation, props.animation);
|
|
1241
|
+
const animation = react.useMemo(
|
|
1242
|
+
() => (
|
|
1243
|
+
// Someone who has asked their OS for less motion should not be handed a
|
|
1244
|
+
// screen of racing lines. The arcs still render; they just hold still.
|
|
1245
|
+
baseAnimation.respectReducedMotion && reducedMotion ? { ...baseAnimation, enabled: false } : baseAnimation
|
|
1246
|
+
),
|
|
1247
|
+
[baseAnimation, reducedMotion]
|
|
1248
|
+
);
|
|
1209
1249
|
const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
|
|
1210
1250
|
const pixelRatio = usePixelRatio();
|
|
1211
1251
|
const width = widthProp ?? measured?.width ?? 0;
|
package/dist/index.js
CHANGED
|
@@ -453,6 +453,40 @@ function useReducedMotion() {
|
|
|
453
453
|
}, []);
|
|
454
454
|
return reduced;
|
|
455
455
|
}
|
|
456
|
+
function configEqual(a, b) {
|
|
457
|
+
const aKeys = Object.keys(a);
|
|
458
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
459
|
+
for (const key of aKeys) {
|
|
460
|
+
const av = a[key];
|
|
461
|
+
const bv = b[key];
|
|
462
|
+
if (Object.is(av, bv)) continue;
|
|
463
|
+
if (isPlainObject(av) && isPlainObject(bv)) {
|
|
464
|
+
if (!shallowEqual(av, bv)) return false;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
function shallowEqual(a, b) {
|
|
472
|
+
const aKeys = Object.keys(a);
|
|
473
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
474
|
+
for (const key of aKeys) {
|
|
475
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
476
|
+
}
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
function isPlainObject(value) {
|
|
480
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
481
|
+
}
|
|
482
|
+
function useStableConfig(defaults, overrides) {
|
|
483
|
+
const resolved = mergeDefined(defaults, overrides);
|
|
484
|
+
const ref = useRef(resolved);
|
|
485
|
+
if (!configEqual(ref.current, resolved)) {
|
|
486
|
+
ref.current = resolved;
|
|
487
|
+
}
|
|
488
|
+
return ref.current;
|
|
489
|
+
}
|
|
456
490
|
function drawBaseMap(options) {
|
|
457
491
|
const { ctx, width, height, projection, geo, theme, regions } = options;
|
|
458
492
|
ctx.clearRect(0, 0, width, height);
|
|
@@ -1194,17 +1228,23 @@ function ThreatMap(props) {
|
|
|
1194
1228
|
ariaLabel = "Cyberattack threat map"
|
|
1195
1229
|
} = props;
|
|
1196
1230
|
const containerRef = useRef(null);
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
return props.theme
|
|
1231
|
+
const themeOverrides = useMemo(() => {
|
|
1232
|
+
if (!props.theme?.severityColors) return props.theme;
|
|
1233
|
+
return { ...props.theme, severityColors: { ...defaultTheme.severityColors, ...props.theme.severityColors } };
|
|
1200
1234
|
}, [props.theme]);
|
|
1201
|
-
const
|
|
1202
|
-
const
|
|
1235
|
+
const theme = useStableConfig(defaultTheme, themeOverrides);
|
|
1236
|
+
const line = useStableConfig(defaultLineStyle, props.line);
|
|
1237
|
+
const regions = useStableConfig(defaultRegions, props.regions);
|
|
1203
1238
|
const reducedMotion = useReducedMotion();
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1239
|
+
const baseAnimation = useStableConfig(defaultAnimation, props.animation);
|
|
1240
|
+
const animation = useMemo(
|
|
1241
|
+
() => (
|
|
1242
|
+
// Someone who has asked their OS for less motion should not be handed a
|
|
1243
|
+
// screen of racing lines. The arcs still render; they just hold still.
|
|
1244
|
+
baseAnimation.respectReducedMotion && reducedMotion ? { ...baseAnimation, enabled: false } : baseAnimation
|
|
1245
|
+
),
|
|
1246
|
+
[baseAnimation, reducedMotion]
|
|
1247
|
+
);
|
|
1208
1248
|
const measured = useElementSize(containerRef, widthProp === void 0 || heightProp === void 0);
|
|
1209
1249
|
const pixelRatio = usePixelRatio();
|
|
1210
1250
|
const width = widthProp ?? measured?.width ?? 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-threat-map",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|