-
Notifications
You must be signed in to change notification settings - Fork 19
Open
Description
Add a section of the README describing in detail the visual editor, and the config.json structure it expects, and how to code for it within the viz.
Include snippets from this example:
asyncRequest.js
export const asyncRequest = (
setDataRequest,
loadAndParseData,
) => {
setDataRequest({ status: 'Loading' });
loadAndParseData()
.then((data) => {
setDataRequest({ status: 'Succeeded', data });
})
.catch((error) => {
setDataRequest({ status: 'Failed', error });
});
};renderLoadingState.js
export const renderLoadingState = (
svg,
{ x, y, text, shouldShow, fontSize, fontFamily },
) => {
svg
.selectAll('text.loading-text')
.data(shouldShow ? [null] : [])
.join('text')
.attr('class', 'loading-text')
.attr('x', x)
.attr('y', y)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', fontSize)
.attr('font-family', fontFamily)
.text(text);
};renderMarks.js
export const renderMarks = (
svg,
{
data,
xScale,
yScale,
xValue,
yValue,
pointRadius,
colorScale,
pointOpacity,
},
) =>
svg
.selectAll('circle.data-point')
.data(data)
.join('circle')
.attr('class', 'data-point')
.attr('cx', (d) => xScale(xValue(d)))
.attr('cy', (d) => yScale(yValue(d)))
.attr('r', pointRadius)
.attr('fill', (d) => colorScale[d.species])
.attr('opacity', pointOpacity);styles.css
#viz-container {
position: fixed;
inset: 0;
}iris.csv
sepal_length,sepal_width,petal_length,petal_width,species
5.1,3.5,1.4,0.2,setosa
4.9,3.0,1.4,0.2,setosa
4.7,3.2,1.3,0.2,setosa
4.6,3.1,1.5,0.2,setosa
5.0,3.6,1.4,0.2,setosa
5.4,3.9,1.7,0.4,setosa
4.6,3.4,1.4,0.3,setosa
5.0,3.4,1.5,0.2,setosa
4.4,2.9,1.4,0.2,setosa
4.9,3.1,1.5,0.1,setosa
5.4,3.7,1.5,0.2,setosa
4.8,3.4,1.6,0.2,setosa
4.8,3.0,1.4,0.1,setosa
4.3,3.0,1.1,0.1,setosa
5.8,4.0,1.2,0.2,setosa
5.7,4.4,1.5,0.4,setosa
5.4,3.9,1.3,0.4,setosa
5.1,3.5,1.4,0.3,setosa
5.7,3.8,1.7,0.3,setosa
5.1,3.8,1.5,0.3,setosa
5.4,3.4,1.7,0.2,setosa
5.1,3.7,1.5,0.4,setosa
4.6,3.6,1.0,0.2,setosa
5.1,3.3,1.7,0.5,setosa
4.8,3.4,1.9,0.2,setosa
5.0,3.0,1.6,0.2,setosa
5.0,3.4,1.6,0.4,setosa
5.2,3.5,1.5,0.2,setosa
5.2,3.4,1.4,0.2,setosa
4.7,3.2,1.6,0.2,setosa
4.8,3.1,1.6,0.2,setosa
5.4,3.4,1.5,0.4,setosa
5.2,4.1,1.5,0.1,setosa
5.5,4.2,1.4,0.2,setosa
4.9,3.1,1.5,0.1,setosa
5.0,3.2,1.2,0.2,setosa
5.5,3.5,1.3,0.2,setosa
4.9,3.1,1.5,0.1,setosa
4.4,3.0,1.3,0.2,setosa
5.1,3.4,1.5,0.2,setosa
5.0,3.5,1.3,0.3,setosa
4.5,2.3,1.3,0.3,setosa
4.4,3.2,1.3,0.2,setosa
5.0,3.5,1.6,0.6,setosa
5.1,3.8,1.9,0.4,setosa
4.8,3.0,1.4,0.3,setosa
5.1,3.8,1.6,0.2,setosa
4.6,3.2,1.4,0.2,setosa
5.3,3.7,1.5,0.2,setosa
5.0,3.3,1.4,0.2,setosa
7.0,3.2,4.7,1.4,versicolor
6.4,3.2,4.5,1.5,versicolor
6.9,3.1,4.9,1.5,versicolor
5.5,2.3,4.0,1.3,versicolor
6.5,2.8,4.6,1.5,versicolor
5.7,2.8,4.5,1.3,versicolor
6.3,3.3,4.7,1.6,versicolor
4.9,2.4,3.3,1.0,versicolor
6.6,2.9,4.6,1.3,versicolor
5.2,2.7,3.9,1.4,versicolor
5.0,2.0,3.5,1.0,versicolor
5.9,3.0,4.2,1.5,versicolor
6.0,2.2,4.0,1.0,versicolor
6.1,2.9,4.7,1.4,versicolor
5.6,2.9,3.6,1.3,versicolor
6.7,3.1,4.4,1.4,versicolor
5.6,3.0,4.5,1.5,versicolor
5.8,2.7,4.1,1.0,versicolor
6.2,2.2,4.5,1.5,versicolor
5.6,2.5,3.9,1.1,versicolor
5.9,3.2,4.8,1.8,versicolor
6.1,2.8,4.0,1.3,versicolor
6.3,2.5,4.9,1.5,versicolor
6.1,2.8,4.7,1.2,versicolor
6.4,2.9,4.3,1.3,versicolor
6.6,3.0,4.4,1.4,versicolor
6.8,2.8,4.8,1.4,versicolor
6.7,3.0,5.0,1.7,versicolor
6.0,2.9,4.5,1.5,versicolor
5.7,2.6,3.5,1.0,versicolor
5.5,2.4,3.8,1.1,versicolor
5.5,2.4,3.7,1.0,versicolor
5.8,2.7,3.9,1.2,versicolor
6.0,2.7,5.1,1.6,versicolor
5.4,3.0,4.5,1.5,versicolor
6.0,3.4,4.5,1.6,versicolor
6.7,3.1,4.7,1.5,versicolor
6.3,2.3,4.4,1.3,versicolor
5.6,3.0,4.1,1.3,versicolor
5.5,2.5,4.0,1.3,versicolor
5.5,2.6,4.4,1.2,versicolor
6.1,3.0,4.6,1.4,versicolor
5.8,2.6,4.0,1.2,versicolor
5.0,2.3,3.3,1.0,versicolor
5.6,2.7,4.2,1.3,versicolor
5.7,3.0,4.2,1.2,versicolor
5.7,2.9,4.2,1.3,versicolor
6.2,2.9,4.3,1.3,versicolor
5.1,2.5,3.0,1.1,versicolor
5.7,2.8,4.1,1.3,versicolor
6.3,3.3,6.0,2.5,virginica
5.8,2.7,5.1,1.9,virginica
7.1,3.0,5.9,2.1,virginica
6.3,2.9,5.6,1.8,virginica
6.5,3.0,5.8,2.2,virginica
7.6,3.0,6.6,2.1,virginica
4.9,2.5,4.5,1.7,virginica
7.3,2.9,6.3,1.8,virginica
6.7,2.5,5.8,1.8,virginica
7.2,3.6,6.1,2.5,virginica
6.5,3.2,5.1,2.0,virginica
6.4,2.7,5.3,1.9,virginica
6.8,3.0,5.5,2.1,virginica
5.7,2.5,5.0,2.0,virginica
5.8,2.8,5.1,2.4,virginica
6.4,3.2,5.3,2.3,virginica
6.5,3.0,5.5,1.8,virginica
7.7,3.8,6.7,2.2,virginica
7.7,2.6,6.9,2.3,virginica
6.0,2.2,5.0,1.5,virginica
6.9,3.2,5.7,2.3,virginica
5.6,2.8,4.9,2.0,virginica
7.7,2.8,6.7,2.0,virginica
6.3,2.7,4.9,1.8,virginica
6.7,3.3,5.7,2.1,virginica
7.2,3.2,6.0,1.8,virginica
6.2,2.8,4.8,1.8,virginica
6.1,3.0,4.9,1.8,virginica
6.4,2.8,5.6,2.1,virginica
7.2,3.0,5.8,1.6,virginica
7.4,2.8,6.1,1.9,virginica
7.9,3.8,6.4,2.0,virginica
6.4,2.8,5.6,2.2,virginica
6.3,2.8,5.1,1.5,virginica
6.1,2.6,5.6,1.4,virginica
7.7,3.0,6.1,2.3,virginica
6.3,3.4,5.6,2.4,virginica
6.4,3.1,5.5,1.8,virginica
6.0,3.0,4.8,1.8,virginica
6.9,3.1,5.4,2.1,virginica
6.7,3.1,5.6,2.4,virginica
6.9,3.1,5.1,2.3,virginica
5.8,2.7,5.1,1.9,virginica
6.8,3.2,5.9,2.3,virginica
6.7,3.3,5.7,2.5,virginica
6.7,3.0,5.2,2.3,virginica
6.3,2.5,5.0,1.9,virginica
6.5,3.0,5.2,2.0,virginica
6.2,3.4,5.4,2.3,virginica
5.9,3.0,5.1,1.8,virginica
scatterPlot.js
import { scaleLinear, extent } from 'd3';
import { renderMarks } from './renderMarks.js';
export const scatterPlot = (svg, options) => {
const {
data,
dimensions: { width, height },
margin: { left, right, top, bottom },
xValue,
yValue,
colorScale,
} = options;
const xScale = scaleLinear()
.domain(extent(data, xValue))
.range([left, width - right]);
const yScale = scaleLinear()
.domain(extent(data, yValue))
.range([height - bottom, top]);
renderMarks(svg, { ...options, xScale, yScale, colorScale });
};index.js
import { unidirectionalDataFlow } from 'd3-rosetta';
import { viz } from './viz';
const container = document.getElementById('viz-container');
unidirectionalDataFlow(container, viz);loadAndParseData.js
import { csv } from 'd3';
export const loadAndParseData = async (dataUrl) =>
await csv(dataUrl, (d, i) => {
d.sepal_length = +d.sepal_length;
d.sepal_width = +d.sepal_width;
d.petal_length = +d.petal_length;
d.petal_width = +d.petal_width;
d.id = i;
return d;
});config.json
{
"xValue": "sepal_length",
"yValue": "sepal_width",
"margin": {
"top": 20,
"right": 67,
"bottom": 60,
"left": 60
},
"fontSize": "14px",
"fontFamily": "sans-serif",
"pointRadius": 17.2675034867503,
"pointFill": "black",
"pointOpacity": 0.7,
"loadingFontSize": "24px",
"loadingFontFamily": "sans-serif",
"loadingMessage": "Loading...",
"dataUrl": "iris.csv",
"colorScale": {
"setosa": "#1f77b4",
"versicolor": "#ff7f0e",
"virginica": "#2ca02c"
},
"visualEditorWidgets": [
{
"type": "number",
"label": "Point Radius",
"property": "pointRadius",
"min": 1,
"max": 30
},
{
"type": "number",
"label": "Left Margin",
"property": "margin.left",
"min": 0,
"max": 200
}
]
}
setupSVG.js
import { select } from 'd3';
import { one } from 'd3-rosetta';
export const setupSVG = (container, { width, height }) =>
one(select(container), 'svg')
.attr('width', width)
.attr('height', height);index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Clickable Circles with D3 Rosetta</title>
<link rel="stylesheet" href="styles.css" />
<script type="importmap">
{
"imports": {
"d3": "https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm",
"d3-rosetta":
"https://cdn.jsdelivr.net/npm/d3-rosetta@3.0.0/+esm"
}
}
</script>
</head>
<body>
<div id="viz-container"></div>
<script type="module" src="index.js"></script>
</body>
</html>viz.js
import { createStateField } from 'd3-rosetta';
import { setupSVG } from './setupSVG.js';
import { renderLoadingState } from './renderLoadingState.js';
import { asyncRequest } from './asyncRequest.js';
import { loadAndParseData } from './loadAndParseData.js';
import { scatterPlot } from './scatterPlot.js';
import { measureDimensions } from './measureDimensions.js';
import { json } from 'd3';
export const viz = (container, state, setState) => {
const stateField = createStateField(state, setState);
const [dataRequest, setDataRequest] =
stateField('dataRequest');
const [config, setConfig] = stateField('config');
// Set up postMessage event listener if not already set
if (!state.eventListenerAttached) {
window.addEventListener('message', (event) => {
// Verify the message contains config data
if (event.data && typeof event.data === 'object') {
// Update the config with the received data
setState((state) => ({
...state,
config: {
...state.config,
...event.data,
},
}));
}
});
// Mark that we've attached the event listener to avoid duplicates
setState((prevState) => ({
...prevState,
eventListenerAttached: true,
}));
}
// Load config first if not already loaded
if (!config) {
json('config.json')
.then((loadedConfig) => {
setConfig(loadedConfig);
})
.catch((error) => {
console.error('Failed to load config:', error);
});
return;
}
// After config is loaded, load the data
if (!dataRequest) {
return asyncRequest(setDataRequest, () =>
loadAndParseData(config.dataUrl),
);
}
const { data, error } = dataRequest;
const dimensions = measureDimensions(container);
const svg = setupSVG(container, dimensions);
renderLoadingState(svg, {
shouldShow: !data,
text: error
? `Error: ${error.message}`
: config.loadingMessage,
x: dimensions.width / 2,
y: dimensions.height / 2,
fontSize: config.loadingFontSize,
fontFamily: config.loadingFontFamily,
});
if (data) {
// Transform string properties in config to accessor functions
const configWithAccessors = {
...config,
xValue: (d) => d[config.xValue],
yValue: (d) => d[config.yValue],
};
scatterPlot(svg, {
...configWithAccessors,
data,
dimensions,
});
}
};d3-rosetta-docs.md
# d3-rosetta
Docs included here for LLM context, so it knows how to use
the API.
- **A utility library** for simplifying
[D3](https://d3js.org/) rendering logic with
unidirectional data flow.
### The Problem: Re-using D3 Rendering Logic Across Frameworks
While frameworks like React, Svelte, Vue, and Angular offer
state management and DOM manipulation solutions, D3 excels
in data transformation and visualization, particularly with
axes, transitions, and behaviors (e.g. zoom, drag, and
brush). These D3 features require direct access to the DOM,
making it challenging to replicate them effectively within
frameworks.
### The Solution: Unidirectional Data Flow
Unidirectional data flow is a pattern that can be cleanly
invoked from multiple frameworks. In this paradigm, a single
function is responsible for updating the DOM or rendering
visuals based on a single, central state. As the state
updates, the function re-renders the visualization in an
idempotent manner, meaning it can run multiple times without
causing side effects. Here's what the entry point function
looks like for a D3-based visualization that uses
unidirectional data flow:
measureDimensions.js
export const measureDimensions = (container) => ({
width: container.clientWidth,
height: container.clientHeight,
});README.md
This is a PoC for some ideas around creating a "Visual
Editor", with widgets like sliders and color pickers for
tweaking config parameters.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels