Linked Brushing

Brushing one view highlights the same items in every other view. Each ScatterPlot series carries point_ids (here element symbols), and every plot receives the shared selection as highlighted_ids: keyed points outside it are dimmed. The selection itself comes from on_select, which fires after every Alt+drag (Option+drag on macOS) and reports the brushed points with their point_id. The periodic table joins in through its existing active_elements and color_overrides props, and clicking a tile adds or removes that element.

Interactive example

svelte<script lang="ts">
  import { element_data, ELEM_PROPERTY_LABELS, PeriodicTable, ScatterPlot } from 'matterviz'
  import type { ChemicalElement, ElementSymbol, InternalPoint } from 'matterviz'

  const symbols = element_data.map(({ symbol }) => symbol)
  const plots = [
    [`electronegativity`, `atomic_radius`, `linear`],
    [`density`, `melting_point`, `log`],
  ] as const
  const axis_label = (key: keyof ChemicalElement) => {
    const [label, unit] = ELEM_PROPERTY_LABELS[key] ?? [key]
    return unit ? `${label} (${unit})` : label
  }

  // null = nothing brushed, so no view dims anything
  let selected = $state<ElementSymbol[] | null>(null)
  // Each plot's Alt+drag rings; cleared once the selection comes from elsewhere
  let rings = $state<{ series_idx: number; point_idx: number }[][]>([[], []])

  const on_brush =
    (plot_idx: number) =>
    ({ points }: { points: InternalPoint[] }) => {
      const ids = new Set(points.map(({ point_id }) => point_id))
      selected = points.length > 0 ? symbols.filter((symbol) => ids.has(symbol)) : null
      rings[1 - plot_idx] = []
    }
  const set_selection = (next: ElementSymbol[]) => {
    selected = next.length > 0 ? next : null
    rings = [[], []]
  }
  const toggle_element = ({ symbol }: ChemicalElement) => {
    const current = selected ?? []
    set_selection(
      current.includes(symbol)
        ? current.filter((other) => other !== symbol)
        : [...current, symbol],
    )
  }
  // Grey out the tiles outside the selection so the table reads like the dimmed plots
  const dimmed_tiles = $derived(
    Object.fromEntries(
      symbols
        .filter((symbol) => selected && !selected.includes(symbol))
        .map((symbol) => [symbol, `#8884`]),
    ),
  )
</script>

<p style="display: flex; gap: 1em; align-items: center; min-height: 2em">
  {#if selected}
    <strong>{selected.length} selected:</strong>
    <span style="flex: 1">{selected.join(`, `)}</span>
    <button onclick={() => set_selection([])}>Clear</button>
  {:else}
    Alt+drag a rectangle in either plot, or click periodic table tiles.
  {/if}
</p>

<div
  style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1em"
>
  {#each plots as [x_key, y_key, scale_type], plot_idx (x_key)}
    {@const [x_label, y_label] = [axis_label(x_key), axis_label(y_key)]}
    <!-- Missing values become NaN, which the plot drops, so ids stay aligned with x and y -->
    <ScatterPlot
      series={[
        {
          x: element_data.map((elem) => elem[x_key] ?? NaN),
          y: element_data.map((elem) => elem[y_key] ?? NaN),
          point_ids: symbols,
          metadata: element_data,
          markers: `points`,
        },
      ]}
      highlighted_ids={selected}
      bind:selected_points={rings[plot_idx]}
      on_select={on_brush(plot_idx)}
      x_axis={{ label: x_label, scale_type }}
      y_axis={{ label: y_label }}
      style="height: 320px"
    >
      {#snippet tooltip({ metadata, x_formatted, y_formatted })}
        <strong>{metadata?.symbol} {metadata?.name}</strong><br />
        {x_label}: {x_formatted}<br />{y_label}: {y_formatted}
      {/snippet}
    </ScatterPlot>
  {/each}
</div>

<PeriodicTable
  active_elements={selected ?? []}
  color_overrides={dimmed_tiles}
  on_activate={toggle_element}
  style="margin-top: 1em"
/>