Guide

Viewport-Aware Attributes Architecture

Overview

The chart builder supports viewport-specific customization, allowing different presentation settings for desktop, tablet, and mobile viewports. However, not all attributes should be viewport-aware. This document outlines the architectural decision to separate content attributes (what data to show) from presentation attributes (how to show it).

Core Principle

The same data should be shown across all viewports; only the presentation should adapt.

This ensures:

  • Data integrity: The chart represents the same story across all devices
  • Editorial consistency: One set of findings, not three different datasets
  • User trust: Shared links show the same data regardless of device
  • Simpler mental model: "The data is constant; the presentation adapts"

Attribute Categories

Content Attributes (NOT Viewport-Aware)

Content attributes define WHAT data to show and should be consistent across all viewports. These are accessed directly from attributes and updated using setAttributes().

AttributeDescriptionAccess Pattern
ioData source, color palette, chart family, available categoriesattributes.io
dataRenderCategories to render, sorting order, scales, date formatsattributes.dataRender
divergingBarPositive/negative category assignments, neutral bar configattributes.divergingBar
colorsColor palette (stored in io.colorValue, io.customColors)attributes.io.colorValue

Example Usage:

// ✅ CORRECT: Direct access for content attributes
const io = attributes.io || {};
const dataRender = attributes.dataRender || {};

// Update content attributes directly
setAttributes({
	dataRender: {
		...dataRender,
		categories: newCategories,
	},
});

Presentation Attributes (Viewport-Aware)

Presentation attributes define HOW to show the data and can vary by viewport. These use the useViewportAttributes hook with getCurrentValue() and updateAttributeForDevice().

AttributeDescriptionAccess Pattern
layoutWidth, height, padding, orientationgetCurrentValue('layout', 'width')
labelsFont size, positioning, visibility, custom positionsgetCurrentValue('labels', 'fontSize')
legendOrientation, position, alignment, offsetgetCurrentValue('legend', 'alignment')
annotationsText, positioning, stylinggetCurrentValue('annotations', 'items')
tooltipPositioning, sizing, formattinggetCurrentValue('tooltip', 'active')
independentAxisTick counts, label sizes, positioninggetCurrentValue('independentAxis', 'tickCount')
dependentAxisTick counts, label sizes, positioninggetCurrentValue('dependentAxis', 'tickCount')
metadataTitle, subtitle (for space constraints)getCurrentValue('metadata', 'title')
barBar padding, group paddinggetCurrentValue('bar', 'barPadding')
lineInterpolation, stroke width, area fillgetCurrentValue('line', 'strokeWidth')
dotPlotConnecting line stylesgetCurrentValue('dotPlot', 'connectPoints')
mapProjection, boundaries, zoomgetCurrentValue('map', 'showStateBoundaries')
diffColumnStyling, positioninggetCurrentValue('diffColumn', 'style')
plotBandsBand configuration, stylinggetCurrentValue('plotBands', 'bands')

Example Usage:

// ✅ CORRECT: Use hook for presentation attributes
const { getCurrentValue, updateAttributeForDevice } = useViewportAttributes(
	attributes,
	setAttributes
);

// Read viewport-aware value
const fontSize = getCurrentValue('labels', 'fontSize');

// Update viewport-aware value
updateAttributeForDevice('labels', {
	fontSize: 14,
});

Implementation Guide

Setting Up a Control Component

  1. Import the hook:
    import { useViewportAttributes } from './use-viewport-attributes';
  2. Initialize for presentation attributes:
    const { getCurrentValue, updateAttributeForDevice } = useViewportAttributes(
    	attributes,
    	setAttributes
    );
  3. Access content attributes directly:
    // Content attributes - NOT viewport-aware
    const io = attributes.io || {};
    const dataRender = attributes.dataRender || {};
  4. Access presentation attributes via hook:
    // Presentation attributes - viewport-aware
    const layout = getCurrentValue('layout') || {};
    const fontSize = getCurrentValue('labels', 'fontSize');

Updating Attributes

Content Attributes:

// ✅ CORRECT: Direct setAttributes for content
setAttributes({
	dataRender: {
		...dataRender,
		categories: newCategories,
	},
});

Presentation Attributes:

// ✅ CORRECT: Use updateAttributeForDevice for presentation
updateAttributeForDevice('labels', {
	fontSize: 14,
});

Common Patterns

Reading a Single Value

// Content attribute
const colorValue = attributes.io?.colorValue;

// Presentation attribute
const fontSize = getCurrentValue('labels', 'fontSize');

Reading an Entire Group

// Content attribute
const dataRender = attributes.dataRender || {};

// Presentation attribute
const labels = getCurrentValue('labels') || {};

Updating with Spread

// Content attribute
setAttributes({
	dataRender: {
		...dataRender,
		categories: newCategories,
	},
});

// Presentation attribute
const currentLabels = getCurrentValue('labels') || {};
updateAttributeForDevice('labels', {
	...currentLabels,
	fontSize: 14,
});

Why This Separation?

Problems Solved

  1. Data Consistency: Users expect the same data regardless of device
  2. Editorial Integrity: Charts represent research findings that shouldn't vary by viewport
  3. Simplified Mental Model: Clear distinction between "what" and "how"
  4. Reduced Bugs: No need to sync data attributes across viewports
  5. Better UX: Users can customize presentation without worrying about data integrity

What This Means for Users

  • Content attributes (data, categories, colors) are set once and apply everywhere
  • Presentation attributes (sizes, positions, visibility) can be customized per viewport
  • When adding new data columns, they automatically appear in all viewports
  • Users can optimize layout and typography for each screen size independently

Migration Notes

If you're updating existing code:

  1. Check the attribute category – Is it content or presentation?
  2. Content attributes: Remove getCurrentValue() and updateAttributeForDevice(), use direct attributes access
  3. Presentation attributes: Ensure they use getCurrentValue() and updateAttributeForDevice()
  4. Never mix patterns: Don't use viewport-aware helpers for content attributes

Examples of Correct Usage

✅ Correct: Content Attribute

function DataControls({ attributes, setAttributes }) {
	// Content attribute - direct access
	const dataRender = attributes.dataRender || {};

	return (
		<SelectControl
			value={dataRender.xScale}
			onChange={(value) =>
				setAttributes({
					dataRender: {
						...dataRender,
						xScale: value,
					},
				})
			}
		/>
	);
}

✅ Correct: Presentation Attribute

function LabelControls({ attributes, setAttributes }) {
	const { getCurrentValue, updateAttributeForDevice } = useViewportAttributes(
		attributes,
		setAttributes
	);

	return (
		<NumberControl
			value={getCurrentValue('labels', 'fontSize')}
			onChange={(value) =>
				updateAttributeForDevice('labels', {
					fontSize: value,
				})
			}
		/>
	);
}

❌ Incorrect: Using Viewport-Aware for Content

// ❌ WRONG: dataRender is content, not presentation
const dataRender = getCurrentValue('dataRender') || {};
updateAttributeForDevice('dataRender', { categories: [...] });

❌ Incorrect: Direct Access for Presentation

// ❌ WRONG: labels is presentation, should be viewport-aware
const fontSize = attributes.labels?.fontSize;
setAttributes({ labels: { ...attributes.labels, fontSize: 14 } });
  • src/chart/edit/use-viewport-attributes.js – Hook implementation
  • src/chart/utils/get-config.js – Config merging logic
  • src/chart/class-chart.php – Server-side attribute merging

Questions?

If you're unsure whether an attribute should be viewport-aware, ask:

  1. Does this change WHAT data is shown? → Content attribute (NOT viewport-aware)
  2. Does this change HOW the data is presented? → Presentation attribute (viewport-aware)

When in doubt, err on the side of content (NOT viewport-aware) to maintain data consistency.

Was this helpful?