Initial Release help-frontend

Dr. Frontend

Linked Visualizations

Cross-framework state sharing using nanostores. The map (Svelte) and scatterplot (Vue) share state seamlessly through a common store.

Live Demo

SvelteClick a region to filter the scatterplot

Region Map

Click a region to filter demographics

NortheastSoutheastMidwestSouthwestWest CoastClick a region to filter

VueDemographics update based on map selection

Demographics Scatterplot

Showing: All Regions (0 data points)

Average AgeMedian Income ($)
Urban
Suburban
Rural

Implementation Guide

Step 1: Create a Shared Store

Define your shared state using nanostores atoms and computed values:

// stores/linkedVizStore.ts
import { atom, computed } from 'nanostores';

export const selectedRegionId = atom<string | null>(null);

export const filteredData = computed(selectedRegionId, (id) => {
  if (!id) return ALL_DATA;
  return ALL_DATA.filter(d => d.regionId === id);
});

export function selectRegion(id: string | null) {
  selectedRegionId.set(id);
}

Step 2: Subscribe in Svelte

Use the store directly in Svelte components:

<script lang="ts">
import { onMount } from 'svelte';
import { selectedRegionId, selectRegion } from '../stores/linkedVizStore';

let currentSelection: string | null = null;

onMount(() => {
  const unsubscribe = selectedRegionId.subscribe((value) => {
    currentSelection = value;
  });
  return unsubscribe;
});
</script>

<button on:click={() => selectRegion('northeast')}>
  Northeast
</button>

Step 3: Subscribe in Vue

Use the store with Vue's reactivity system:

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { filteredData } from '../stores/linkedVizStore';

const data = ref([]);

let unsubscribe: (() => void) | null = null;

onMounted(() => {
  unsubscribe = filteredData.subscribe((value) => {
    data.value = value;
  });
});

onUnmounted(() => {
  unsubscribe?.();
});
</script>

<template>
  <div v-for="item in data" :key="item.id">
    {{ item.name }}
  </div>
</template>

Architecture

┌─────────────────────────────────────────────────┐
│                  Astro Page                      │
│  ┌──────────────────┐  ┌──────────────────────┐ │
│  │   Svelte Map     │  │   Vue Scatterplot    │ │
│  │                  │  │                      │ │
│  │  onClick ────────┼──┼─► Updates display    │ │
│  │                  │  │                      │ │
│  └────────┬─────────┘  └──────────┬───────────┘ │
│           │                       │             │
│           ▼                       ▼             │
│  ┌────────────────────────────────────────────┐ │
│  │           Nanostore (TypeScript)           │ │
│  │                                            │ │
│  │  selectedRegionId: atom<string | null>     │ │
│  │  filteredData: computed(...)               │ │
│  │                                            │ │
│  └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

Key Benefits

  • Framework agnostic: Same store works with React, Vue, Svelte, and vanilla JS
  • Tiny bundle: nanostores is ~1KB gzipped
  • TypeScript support: Full type safety across frameworks
  • Computed values: Derived state updates automatically
  • No context needed: Works without React Context or Vue provide/inject

Dependencies

npm install nanostores @nanostores/vue
npm install @astrojs/svelte @astrojs/vue svelte vue

Key Files

  • src/stores/linkedVizStore.ts - Shared state
  • src/components/examples/RegionMap.svelte - Svelte map
  • src/components/examples/DemographicsScatter.vue - Vue chart

Use Cases

Dashboard Filters

Global filters that update multiple charts from different frameworks simultaneously.

Brushing & Linking

Select data in one visualization to highlight corresponding points in others.

Micro-frontends

Share state between independently deployed components built with different frameworks.