Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 14, 2025

The Visual Editor failed to display widgets for nested config properties (e.g., margin.left) despite README documentation indicating support. The underlying utility functions didn't handle dot notation paths.

Changes

Nested Property Utilities (utils.ts)

  • Added getNestedProperty<T>() and setNestedProperty<T>() with TypeScript generics
  • Path validation: blocks empty paths, consecutive dots (..), leading/trailing dots
  • Security: prevents prototype pollution by rejecting __proto__, constructor, prototype keys
  • Null-safe checking: uses == null instead of falsy checks to preserve 0, false, empty strings

VisualEditor Component

  • Updated all property access to use nested property utilities
  • Added sensible fallbacks: sliders→min, checkboxes→false, text→"", dropdowns→first option, colors→#000000
  • Fixed sidebar view state consistency

Test Configuration

  • Corrected widget property from "Point Radius""pointRadius"
  • Added "margin.left" widget demonstrating nested access

Example

// Before: only top-level properties worked
configData[property] = newValue;

// After: supports nested paths with security
const value = getNestedProperty<number>(configData, 'margin.left');
const updated = setNestedProperty(configData, 'margin.left', 100);
// Throws on: '__proto__.polluted', 'foo..bar', '.leading', 'trailing.'

Security: CodeQL scan passes with 0 alerts.

Original prompt

This section details on the original issue you should resolve

<issue_title>Broken Visual Editor</issue_title>
<issue_description>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 { scatterPl...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes vizhub-core/vzcode#1195

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Copilot AI and others added 7 commits December 14, 2025 02:53
Co-authored-by: curran <68416+curran@users.noreply.github.com>
Co-authored-by: curran <68416+curran@users.noreply.github.com>
Co-authored-by: curran <68416+curran@users.noreply.github.com>
Co-authored-by: curran <68416+curran@users.noreply.github.com>
Co-authored-by: curran <68416+curran@users.noreply.github.com>
Co-authored-by: curran <68416+curran@users.noreply.github.com>
Co-authored-by: curran <68416+curran@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix broken visual editor loading state Add nested property access support to Visual Editor Dec 14, 2025
Copilot AI requested a review from curran December 14, 2025 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants