Skip to content

Broken Visual Editor #1195

@curran

Description

@curran

This does not show the visual editor, but should:

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

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.

See also:

https://github.com/vizhub-core/vzcode/issues/955

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions