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 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
- A runnable Vite demo covering basic usage, 500+ streaming attacks with a live FPS counter,
602
- one country under fire from five origins at different volumes, aggregation strategies side
603
- by side, custom theming with render hooks, and raw-coordinate reverse geocoding with hover:
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 theme = react.useMemo(() => {
1199
- const merged = mergeDefined(defaultTheme, props.theme);
1200
- return props.theme?.severityColors ? { ...merged, severityColors: { ...defaultTheme.severityColors, ...props.theme.severityColors } } : merged;
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 line = react.useMemo(() => mergeDefined(defaultLineStyle, props.line), [props.line]);
1203
- const regions = react.useMemo(() => mergeDefined(defaultRegions, props.regions), [props.regions]);
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 animation = react.useMemo(() => {
1206
- const merged = mergeDefined(defaultAnimation, props.animation);
1207
- return merged.respectReducedMotion && reducedMotion ? { ...merged, enabled: false } : merged;
1208
- }, [props.animation, reducedMotion]);
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 theme = useMemo(() => {
1198
- const merged = mergeDefined(defaultTheme, props.theme);
1199
- return props.theme?.severityColors ? { ...merged, severityColors: { ...defaultTheme.severityColors, ...props.theme.severityColors } } : merged;
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 line = useMemo(() => mergeDefined(defaultLineStyle, props.line), [props.line]);
1202
- const regions = useMemo(() => mergeDefined(defaultRegions, props.regions), [props.regions]);
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 animation = useMemo(() => {
1205
- const merged = mergeDefined(defaultAnimation, props.animation);
1206
- return merged.respectReducedMotion && reducedMotion ? { ...merged, enabled: false } : merged;
1207
- }, [props.animation, reducedMotion]);
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.0",
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",