Why this matters
As a Data Visualization Engineer, you often move from a prototype to a production build. A strong handoff spec prevents rework, clarifies data contracts, and makes delivery predictable. Real tasks you will face:
- Describing exactly which metrics, dimensions, and filters a chart needs (and how they aggregate).
- Defining interactions: hover, click, drill, selection, cross-filtering, and keyboard behavior.
- Capturing states: loading, empty, partial, error, and permission-restricted views.
- Setting performance budgets, accessibility requirements, and acceptance criteria engineers can test.
Who this is for
- Data Visualization Engineers turning prototypes into production dashboards.
- BI Developers and Analytics Engineers collaborating with front-end teams.
- Designers and PMs who must produce build-ready specs for engineering.
Prerequisites
- Basic understanding of charts (marks, encodings, scales).
- Experience with data types: metrics vs. dimensions, aggregations, and timezones.
- Comfort reading a simple API/SQL schema.
Concept explained simply
A handoff spec is a single source of truth that tells engineers what to build and how it should behave—without dictating their implementation. It answers four big questions:
- Why: What decision or action this visualization supports.
- What: Data contract, visual requirements, and acceptance criteria.
- How it behaves: Interactions, states, performance, accessibility, and responsiveness.
- Boundaries: What’s out of scope and what is still open.
Mental model
- Contract (data): Inputs, transformations, aggregations, and constraints.
- Storyboard (visual + interaction): How the chart looks and reacts across states.
- Guardrails (quality): Performance budgets, accessibility, and testable acceptance criteria.
Copy-paste handoff spec template
Open the template
Title: [Feature/Chart name]
Owner: [Name] Reviewers: [Names] Version: [v1.0]
Problem & Goal: [Decision to support; target user; success signal]
Scope: [In scope]; Out of Scope: [Out of scope]
User Stories:
- As a [role], I want [task] so that [outcome].
Data Contract:
Sources: [Tables/APIs]
Fields (type): [metric1 (float), dimension1 (string), date (UTC)]
Aggregations: [sum(metric1) by date, dimension1]
Filters (default + user): [date: last 90 days, product in {A,B}]
Timezone: [UTC unless noted]
Nulls & Missing: [rules]
Sample Payload (min): [{...}]
Visualization Requirements:
Chart Type & Marks: [e.g., line with points]
Encodings: [x=date (time), y=metric1 (linear), color=dimension1 (categorical)]
Scales: [y starts at 0?; log?; shared scales across small multiples?]
Labels & Formatting: [K/M/B; % with 1 decimal; SI]
Annotations: [threshold line at 95%]
Legends: [placement, ordering]
Interactions:
Hover: [what shows; fields; delay; stickiness]
Click/Drill: [event name, target view, parameters]
Selection/Brush: [single/multi; cross-filter effect]
Keyboard: [tab order, shortcuts]
States:
Loading: [skeleton, spinner]
Empty: [copy and illustration]
Error: [message, retry]
Partial Data: [badge, tooltip note]
Permissions: [redacted state]
Responsiveness:
Breakpoints: [>=1200, 768–1199, <768]
Layout Rules: [legend wraps, axis ticks reduce]
Performance Budgets:
Data volume: [max 50k rows per request]
Initial render: [≤1500 ms on median hardware]
Interaction latency: [≤150 ms hover tooltip]
Accessibility:
Contrast: [AA]
Focus order: [defined]
ARIA: [roles, labels]
Screen reader text: [hidden summaries]
Color: [non-color encodings for key signals]
Tracking/Telemetry:
Events: [view_loaded, legend_toggle, drill_click]
Properties: [user_role, filter_state]
Acceptance Criteria (testable):
- [e.g., Tooltip shows fields A,B,C and appears within 150 ms]
- [...]
Risks & Open Questions:
- [Owner] [Due date] [Plan]
Change Log:
- [Date] [Change] [Owner]
Worked examples
Example 1 — KPI sparkline card (7-day rolling)
- Goal: Show last 30 days of signups with 7-day rolling average; quick trend glance.
- Data: source=analytics.signups; fields=date (UTC, daily), signups (int). Aggregation=sum(signups) by date; computed rolling_avg over 7 days.
- Chart: line (rolling_avg) + faint bars (daily). y starts at 0. Tooltip: date, signups, rolling_avg.
- States: loading skeleton; empty state if 0 rows with message "No signups in selected period"; error with retry.
- Performance: max 365 days; initial render ≤800 ms; tooltip latency ≤100 ms.
- Accessibility: keyboard focusable card; aria-label summarizing last value and trend direction.
- Acceptance: Rolling avg matches SQL reference within 0.1; tooltip fields and formatting exact; color contrast AA.
Example 2 — Category bar chart with drill to table
- Goal: Compare revenue by product category; enable drill to transactions.
- Data: source=bi.revenue; metric=revenue_usd (float), dim=category (string). Aggregation=sum(revenue_usd) by category.
- Chart: horizontal bars; sort desc by value; labels inside bars if space ≥60px else outside.
- Interaction: click bar → emit event drill_click(category) → open table view filtered by category.
- States: empty when no categories; partial data badge if <5 categories present.
- Performance: max categories 30; render ≤1s; drill transition ≤300 ms post-event.
- Acceptance: Sorting accurate; labels never overlap; drill passes correct category id.
Example 3 — Choropleth map with range filter
- Goal: Visualize conversion rate by state with adjustable date range.
- Data: source=mart.conversion; fields=state_code (US), conversions (int), visits (int), date (UTC). Metric: rate=conversions/visits.
- Chart: choropleth; color: quantize into 5 buckets; domain [0%, 20%]; legend with bucket boundaries.
- Interaction: hover → tooltip {state, conversions, visits, rate%}; click → select state (single select) and apply cross-filters to side charts.
- States: loading skeleton map; empty → gray map with note; error → message with retry.
- Accessibility: keyboard navigation over states; focus ring; SR announces state and rate.
- Performance: topojson simplified; max 2 MB payload; render ≤1.2s; hover latency ≤120 ms.
- Acceptance: Color bucket thresholds match legend; selection syncs with side charts within 250 ms.
Engineer-ready handoff checklist
- Purpose, scope, and user stories are clear.
- Data contract: sources, fields, aggregations, filters, timezone, null rules, sample payload.
- Visualization details: marks, encodings, scales, labels, annotations, legends.
- Interactions: event names, parameters, outcomes, keyboard access.
- All states (loading, empty, error, partial, permissions) are described with display rules.
- Responsiveness: breakpoints and layout rules.
- Performance budgets: data volume, render, and interaction latency targets.
- Accessibility: contrast, focus order, ARIA, non-color cues.
- Tracking/telemetry with event names and properties.
- Acceptance criteria are measurable and testable.
- Risks/open questions have owners and due dates.
- Change log present.
Common mistakes and how to self-check
- Missing timezone or aggregation rule → Self-check: Can someone replicate your numbers from raw data? If not, add explicit rules.
- Vague interactions ("click to drill") → Self-check: Write event name, target view, and parameters. Could QA test it without guessing?
- No empty/error state → Self-check: What does the user see when 0 rows or API 500 occurs?
- Unbounded data volume → Self-check: State row caps, sampling, or pagination.
- Color-only encoding for critical info → Self-check: Add patterns, labels, or icons to pass color-blind scenarios.
- Acceptance criteria too fuzzy → Self-check: Add numbers (e.g., "≤150 ms", field list, formats).
Practical projects
- Project A: Specify a revenue dashboard handoff (3 charts + filters). Include data contracts and ACs. Have a peer try to build from it.
- Project B: Convert a Figma prototype into a full spec for a time-series explorer with brushing and zoom.
- Project C: Write a spec for a cross-filtered map + bar chart layout, including responsive rules for mobile.
Practice exercises
Do these before the test. Solutions are provided for self-review.
Exercise 1 — Funnel conversion chart handoff
- Write a 1–2 page handoff spec for a Signup Funnel (Visited → Started → Completed).
- Include data contract, visualization, interactions, states, responsiveness, performance, accessibility, tracking, and acceptance criteria.
- Use a sample payload of 3–5 rows.
Show sample solution
Title: Signup Funnel
Owner: You Version: v1.0
Goal: Help PMs spot drop-off stages over last 30 days.
Scope: Desktop & tablet; mobile stacked view.
Data Contract:
Source: mart.funnel_daily
Fields: date (UTC, date), stage (enum: visited, started, completed), users (int)
Agg: sum(users) by stage for selected date range (default last 30 days)
Filters: date range (last 7/30/90, custom)
Nulls: treat missing stage as visited
Sample: [{date:"2025-05-01", stage:"visited", users:1200}, ...]
Visualization:
Type: funnel bars (vertical), width encodes users; labels show users and % of previous stage
Colors: visited #B0C4DE, started #6495ED, completed #1E90FF
Labels: users with comma; percent with 1 decimal
Interactions:
Hover: show {stage, users, % from previous}
Click: drill_click(stage) → open session table filtered by stage
States:
Loading: skeleton bars
Empty: "No activity in selected range"
Error: "Data unavailable" + retry
Responsiveness:
≥1200: funnel + legend right
768–1199: legend below
<768: stacked horizontal funnel
Performance:
Data volume: ≤90 days
Render ≤800 ms, tooltip ≤120 ms
Accessibility:
Focus order: title → legend → bars
ARIA: role=img with text summary: "Funnel: 1200 visited, 600 started (50%), 300 completed (50%)"
Tracking:
Events: view_loaded, drill_click(stage)
Acceptance Criteria:
- Percent calculations match SQL reference within 0.1%
- Labels do not overlap
- Drill passes correct stage param
Exercise 2 — Map with drill table: data + interaction spec
- Define the data contract for a country map colored by churn rate (churned/customers).
- Specify the click interaction to open a country-level table, including event name, params, and acceptance criteria.
Show sample solution
Data Contract:
Source: mart.customer_status
Fields: country_code (ISO2), churned (int), customers (int), date (UTC)
Metric: churn_rate = churned/customers
Filters: date range (default last quarter)
Timezone: UTC
Sample: [{country_code:"US", churned:120, customers:4000, date:"2025-06-30"}]
Visualization:
Type: choropleth; 5 quantiles; domain 0–10%
Interaction:
Click: event=drill_click, params={country_code}
Outcome: open table view filtered by country_code and date range
Acceptance Criteria:
- Click emits drill_click with correct country_code
- Table opens within 300 ms after event
- Color bucket in legend matches computed churn_rate bucket
Mini challenge
Pick any chart you’ve built before. In 25 minutes, draft the Data Contract, Interactions, and States sections only. Then ask: Could QA verify this without talking to you? If not, tighten wording and add measurable acceptance criteria.
Learning path
- Before: Chart design fundamentals → Data modeling for
- Now: Handoff specs for engineering (this lesson).
- Next: Cross-component coordination (design tokens, shared data contracts) → Performance tuning → Accessibility deep dive.
Next steps
- Finish the exercises above and compare with the sample solutions.
- Use the checklist to review your most recent spec.
- When ready, take the Quick Test below. Everyone can take it for free; only logged-in users will have progress saved.
Quick Test anchor
Start the Quick Test below to check your understanding. You can retake it any time.