react-threat-map 0.1.1 → 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/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.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",