Skip to content

Glow, pulse and emphasis

An item asks for an effect; the view’s effects switch each one globally, so one request can be shown several ways.

  • effects: ["glow"] draws a soft light in the item’s colour behind it.
  • effects: ["pulse"] makes that glow breathe, each item on its own phase.
  • emphasis: ["ring"] draws an outline and a soft halo in the palette’s ownership colour, for a state worth seeing at a glance (such as “yours”). The ring always draws, since it carries meaning; effects.halo switches off only the halo.
Running the code below: effects.tsx
effects.tsx
import { useState } from "react";
import { createSceneSource, DataExplorer, type ExplorerEffects, type ExplorerNode } from "@data-xplorer/core";
// Services in a status board. Items ask for effects; the view's `effects` switch them globally.
const services: [string, Partial<ExplorerNode<null>>][] = [
["api", { effects: ["pulse"], color: "#f87171" }], // failing: it breathes
["auth", { effects: ["glow"], color: "#fbbf24" }], // degraded: a steady glow
["search", { emphasis: ["ring"] }], // yours: a ring and halo
["billing", {}],
["mail", {}],
["cdn", { emphasis: ["ring"] }],
];
const source = createSceneSource<null>({
load: () => ({
id: "status",
heading: "Service status",
clusters: [],
nodes: services.map(([name, extra], i) => ({
id: name,
label: name,
weight: 4 + (i % 3) * 3,
href: `#${name}`,
drillsInto: null,
shape: "disc" as const,
...extra,
})),
edges: [],
}),
});
const switches: (keyof ExplorerEffects)[] = ["glow", "pulse", "halo", "growIn", "hoverRing", "selectionRing"];
export default function Effects() {
const [effects, setEffects] = useState<ExplorerEffects>({});
return (
<>
<div className="demo-toolbar">
{switches.map((name) => (
<label key={name}>
<input
type="checkbox"
checked={effects[name] !== false}
onChange={(e) => setEffects({ ...effects, [name]: e.target.checked })}
/>{" "}
{name}
</label>
))}
</div>
<DataExplorer source={source} focus={null} effects={effects} />
</>
);
}
Switch Default Effect
glow on Glows on items that ask for glow or pulse. Off, none glow.
pulse on Whether a pulse breathes. Off, it holds as a still glow.
halo on The halo behind a ring.
growIn on An item grows from a dot to its size the first time it is drawn.
hoverRing on The ring round the hovered item. Its label still shows.
selectionRing on The ring round the selected item. onNodeSelect still fires.

Nothing animates on a timer. A pulse asks for the next frame only while one is on screen; with none in view, the canvas draws nothing at rest. Under prefers-reduced-motion, a pulse holds still and items appear at their size without growing in. A hollow aggregate never glows.

presence says how much of each item is there, from 0 to 1, by id. It scales and fades an item where it stands, and never moves it: the layout comes from weight once per scene, so presence can change on every frame (a time slider, a filter) while the map stays still.

Running the code below: presence.tsx
presence.tsx
import { useCallback, useState } from "react";
import { createSceneSource, DataExplorer } from "@data-xplorer/core";
// Programming languages, sized by how widely they are taught, placed once. Presence fades in each
// language from the year it appeared, so the map grows as the slider moves and nothing is relaid out.
const languages: [name: string, year: number, weight: number][] = [
["Fortran", 1957, 6],
["Lisp", 1958, 5],
["COBOL", 1959, 5],
["BASIC", 1964, 5],
["C", 1972, 10],
["SQL", 1974, 9],
["C++", 1985, 9],
["Perl", 1987, 4],
["Haskell", 1990, 4],
["Python", 1991, 12],
["Java", 1995, 11],
["JavaScript", 1995, 12],
["PHP", 1995, 6],
["Ruby", 1995, 5],
["C#", 2000, 8],
["Go", 2009, 6],
["Rust", 2015, 6],
["TypeScript", 2012, 9],
["Swift", 2014, 6],
["Kotlin", 2011, 5],
];
const released = new Map(languages.map(([name, year]) => [name, year]));
const source = createSceneSource<null>({
load: () => ({
id: "languages",
heading: "Programming languages",
clusters: [],
nodes: languages.map(([name, , weight]) => ({
id: name,
label: name,
weight,
href: `#${name}`,
drillsInto: null,
shape: "disc" as const,
})),
edges: [],
}),
});
export default function Presence() {
const [year, setYear] = useState(1975);
// A new function when the year changes is what tells the explorer to redraw; items ease to it.
const presence = useCallback((id: string) => ((released.get(id) ?? 0) <= year ? 1 : 0), [year]);
return (
<>
<div className="demo-toolbar">
<label>
Year{" "}
<input type="range" min={1955} max={2020} value={year} onChange={(e) => setYear(Number(e.target.value))} /> {year}
</label>
</div>
<DataExplorer source={source} focus={null} presence={presence} />
</>
);
}
  • Size follows the square root, so an item’s area is proportional to its presence, the same rule weight uses.
  • At 0 an item is a ghost: small, faint and unlabelled, but still drawn, selectable and enterable, so the viewer can see what is not there yet.
  • undefined is 1. Pass a new function when the answer changes; items ease to it, and settle at once under prefers-reduced-motion. The accessible outline lists every item whatever its presence; say what presence means in your own marks or labels if it matters.