Skip to Content
🏗️ Architecture & DevelopmentAdding A Data Component

Last Updated: 4/8/2026


Adding a New Data Component

This guide walks through the process of creating a new data visualization layer for the “Where Did They Go?” map. Data components are Vue 3 components that register with BaseMap.vue, subscribe to state change hooks, fetch their data, and render visualizations to the shared SVG canvas.

Prerequisites

Before creating a new data component, you should understand:

  • Vue 3 Composition API (<script setup> syntax)
  • D3.js for SVG rendering and geographic projections
  • The BaseMap hook system (see “The BaseMap Hook System” article)
  • GeoJSON or JSON data formats

Step 1: Create the Component File

Create a new Vue component in client/src/components/. Follow the naming convention [DataType]Data.vue (e.g., AirportData.vue, LibraryData.vue).

<!-- components/AirportData.vue Renders and updates data relating to airports in Kansas. === Hooks === OnYearChange: Fetches airport data corresponding to the new year OnZoomChange: Adjusts symbol size based on zoom level Filter: Fades in when checked, fades out when unchecked. --> <script setup> // External imports import { defineProps, onMounted, useTemplateRef, watch, inject } from 'vue'; import * as d3 from 'd3'; // Utility imports import { fetchGeojson } from '@/utility/fetchers'; import { fadeIn, fadeOut } from '@/d3/transitions/fadeSelection'; import { registerKey } from '@/utility/RegisterKey'; // Enum imports import { MapZoomLevel } from '@/enums/MapZoomLevel'; import { GroupType } from '@/enums/GroupType'; // Define props and template refs const props = defineProps(['properties']); const gRef = useTemplateRef('g'); // Component-specific constants const label = 'airports'; const pathGen = d3.geoPath(props.properties.projection); // Component state let selection = null; let gTag = null; let paths = { geojson: `${props.properties.path}/geojson`, json: `${props.properties.path}/json`, }; // Registration happens in Step 2... </script> <template> <g class="airports" ref="g"></g> </template> <style scoped> :global(.airport) { fill: orange; stroke: black; stroke-width: 0.2; pointer-events: visible; } </style>

Step 2: Register with BaseMap

Inject the registerKey and call registerComponent() to register your component and receive hooks. The registration should happen during component setup (before onMounted).

// Register this component with BaseMap const hooks = inject(registerKey)(label, { filter: { legibleLabel: 'Airports', defaultStatus: false, visibleStates: new Set([MapZoomLevel.STATE, MapZoomLevel.COUNTY]), groups: [GroupType.INFRASTRUCTURE], onChecked: onChecked, onUnchecked: onUnchecked, }, });

Filter Configuration Options

**legibleLabel** (string, required) — Human-readable label displayed in the filter panel UI.

**defaultStatus** (boolean, required) — Whether the filter starts checked (true) or unchecked (false).

**visibleStates** (Set , required) — Set of zoom levels where this filter should appear in the panel. Use MapZoomLevel.STATE and/or MapZoomLevel.COUNTY.

**groups** (array of strings, required) — Logical grouping for the filter. Use GroupType.INFRASTRUCTURE or GroupType.OTHER. Can be an empty array if no grouping is needed.

**onChecked** (function, required) — Callback invoked when the user checks the filter checkbox. Typically fades in your visualization.

**onUnchecked** (function, required) — Callback invoked when the user unchecks the filter checkbox. Typically fades out your visualization.

Step 3: Subscribe to Hooks

Use the returned hooks object to subscribe to map state changes. Each hook accepts a callback function with the signature (newValue, oldValue, params) => void.

// Subscribe to year changes hooks.onYearChange((newValue, oldValue, params) => { // Fetch new data for the selected year const { result } = fetchGeojson(`${paths.geojson}/airports_${newValue}.geojson`); renderToSVG(result); }); // Subscribe to zoom level changes hooks.onZoomChange((newValue, oldValue, params) => { if (!selection) return; switch (newValue) { case MapZoomLevel.STATE: // Smaller symbols at state level selection.transition().duration(500).attr('r', 1.5); break; case MapZoomLevel.COUNTY: // Larger symbols at county level selection.transition().duration(500).attr('r', 3); break; } }); // Subscribe to county transitions (optional) hooks.onCountyTransition((newValue, oldValue, params) => { // Update visibility based on bounding box if needed updateVisibleByBBox(); });

Available Hooks

**onYearChange** — Triggered when the timeline slider moves. Use this to fetch data for the new year or update visibility based on temporal properties (e.g., “built in 1950”).

**onZoomChange** — Triggered when transitioning between State View and County View. Use this to adjust symbol sizes, stroke widths, or label visibility.

**onCountyTransition** — Triggered every time a county is selected, including when switching between counties without returning to State View. Use this to filter features by bounding box.

Step 4: Fetch Data

Use the fetchGeojson() or fetchJson() utility functions to load data from the server. These functions return a reactive result object with data, loading, and error refs.

onMounted(() => { gTag = d3.select(gRef.value); // Fetch initial data const { result } = fetchGeojson(`${paths.geojson}/airports_1860.geojson`); renderToSVG(result); });

Handling Async Data

The result object is reactive. If data is still loading when you first access it, watch for the loading ref to change:

function renderToSVG(result) { const d = result.data.value; const l = result.loading.value; const e = result.error.value; if (l) { // Data still loading - watch for completion const unwatch = watch( () => result.loading.value, () => { renderToSVG(result); unwatch(); } ); return; } if (e) { // Handle error console.error('Failed to load airport data:', e); return; } // Data loaded successfully - render it renderFeatures(d); }

Step 5: Render to SVG

Use D3.js to bind your data to SVG elements and render them to the <g> element referenced by gRef.

function renderFeatures(data) { selection = gTag .selectAll('.airport') .data(data.features, d => d.properties.id) .join( // Enter: create new elements (enter) => enter .append('circle') .attr('cx', d => { const coords = props.properties.projection(d.geometry.coordinates); return coords[0]; }) .attr('cy', d => { const coords = props.properties.projection(d.geometry.coordinates); return coords[1]; }) .attr('r', 1.5) .attr('opacity', '0%') .classed('airport', true) .call(enter => fadeIn(enter)), // Update: modify existing elements (update) => update, // Exit: remove old elements (exit) => fadeOut(exit).remove() ); }

Using the Projection

Access the D3 geo projection from props.properties.projection. This converts [longitude, latitude] coordinates to [x, y] SVG coordinates:

const coords = props.properties.projection(feature.geometry.coordinates); const x = coords[0]; const y = coords[1];

For path-based features (LineStrings, Polygons), use d3.geoPath():

const pathGen = d3.geoPath(props.properties.projection); selection.attr('d', pathGen);

Step 6: Implement Filter Callbacks

Define the onChecked and onUnchecked functions referenced in your filter configuration:

function onChecked() { if (selection) { fadeIn(selection); } } function onUnchecked() { if (selection) { fadeOut(selection); } }

These functions control what happens when the user toggles your filter in the UI. Most components simply fade in/out, but you can implement more complex behavior if needed.

Step 7: Register in BaseMap Template

Add your component to the BaseMap.vue template so it renders as part of the map:

<!-- In client/src/components/BaseMap.vue --> <template> <div class="container"> <svg ref="svg" width="1200" height="800" viewBox="0 0 1600 800"> <BorderData :properties="properties" @transition="onTransition" /> <RiverData :properties="properties" /> <LakeData :properties="properties" /> <RailroadData :properties="properties" /> <TractData :properties="properties" /> <CityData :properties="properties" /> <SchoolData :properties="properties" @school-hover="hoveredSchool = $event" /> <HealthcareData :properties="properties" /> <InterstateData :properties="properties" /> <!-- Add your new component here --> <AirportData :properties="properties" /> </svg> <!-- ... rest of template ... --> </div> </template>

Rendering order matters: Components listed later in the template render on top of earlier components. Place your component in the appropriate z-order position.

Complete Example: Minimal Component

Here’s a complete minimal data component that demonstrates all the key concepts:

<script setup> import { defineProps, onMounted, useTemplateRef, inject } from 'vue'; import * as d3 from 'd3'; import { fetchGeojson } from '@/utility/fetchers'; import { fadeIn, fadeOut } from '@/d3/transitions/fadeSelection'; import { registerKey } from '@/utility/RegisterKey'; import { MapZoomLevel } from '@/enums/MapZoomLevel'; import { GroupType } from '@/enums/GroupType'; const props = defineProps(['properties']); const gRef = useTemplateRef('g'); const label = 'example'; let selection = null; let gTag = null; // Register with BaseMap const hooks = inject(registerKey)(label, { filter: { legibleLabel: 'Example Layer', defaultStatus: true, visibleStates: new Set([MapZoomLevel.STATE, MapZoomLevel.COUNTY]), groups: [GroupType.OTHER], onChecked: () => fadeIn(selection), onUnchecked: () => fadeOut(selection), }, }); // Subscribe to hooks hooks.onYearChange((newYear) => { console.log('Year changed to:', newYear); }); // Fetch and render data onMounted(() => { gTag = d3.select(gRef.value); const { result } = fetchGeojson(`${props.properties.path}/geojson/example.geojson`); result.data.value && renderData(result.data.value); }); function renderData(data) { const pathGen = d3.geoPath(props.properties.projection); selection = gTag .selectAll('.example-feature') .data(data.features) .join('path') .attr('d', pathGen) .classed('example-feature', true); } </script> <template> <g class="example" ref="g"></g> </template> <style scoped> :global(.example-feature) { fill: none; stroke: purple; stroke-width: 1; } </style>

Testing Your Component

  1. Start the development server (should auto-start in Dev Container)
  2. Open http://localhost:5173 in your browser
  3. Verify your filter appears in the filter panel at the appropriate zoom level
  4. Toggle the filter on/off to verify fade in/out behavior
  5. Move the timeline slider to verify year change behavior
  6. Zoom into a county to verify zoom change behavior

What’s Next

  • Basemap Hook System – Deep-dive into how the registration API and hooks work internally
  • System Architecture – Understand how data components fit into the overall application architecture
  • Fetching Data – Details on the fetchGeojson/fetchJson composables and retry logic