Skip to content

Build client-side Ashby Plot Studio with Tailwind, shadcn UI, and Plotly.js#5

Open
13Bytes wants to merge 1 commit intolouisfrom
codex/build-client-side-ashby-plot-app
Open

Build client-side Ashby Plot Studio with Tailwind, shadcn UI, and Plotly.js#5
13Bytes wants to merge 1 commit intolouisfrom
codex/build-client-side-ashby-plot-app

Conversation

@13Bytes
Copy link
Copy Markdown
Owner

@13Bytes 13Bytes commented Apr 13, 2026

Motivation

  • Provide a client-side, configurable Ashby plot builder using Tailwind + shadcn-style primitives and Plotly.js so users can author and preview Ashby plots entirely in the browser.
  • Keep runtime plot code off the server (lazy-load Plotly) to avoid SSR/runtime issues and to allow client-side exports.

Description

  • Replace the starter home route with a JSON-driven Ashby Plot Studio at app/routes/home.tsx that includes a config editor, dataframe/frame selectors, version validation (version === 3), inline errors, and a live Plotly preview.
  • Implement plotting pipeline that supports grouping/layers, per-layer color mapping and hide-via-null, single-value and span/range values (rendered as error bars), relative quantities (x/y divided by another field), optional log axes, legend placement, guideline overlays (vertical/horizontal/slope), colored-area overlays, and annotations with arrows.
  • Add lightweight shadcn-style UI primitives and helpers in app/components/ui/button.tsx, app/components/ui/card.tsx, and app/lib/utils.ts (cn util using clsx + tailwind-merge).
  • Add runtime typings and a small declaration file app/types.d.ts to allow dynamic imports of plotly.js-dist-min and react-plotly.js/factory, and update package.json / lockfile to include Plotly, react-plotly, UI utilities, icon package, and related type packages.

Testing

  • Ran npm run typecheck which completed successfully after adding the necessary type packages and declaration wrappers.
  • Ran npm run build which completed successfully; the production build emits an expected large-chunk warning due to the Plotly runtime being included, but the build finished without errors.

Codex Task

Copilot AI review requested due to automatic review settings April 13, 2026 16:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the starter home page with a fully client-side “Ashby Plot Studio” that lets users edit a JSON config and preview/export Plotly-based Ashby plots directly in the browser, alongside adding lightweight shadcn-style UI primitives and Plotly-related dependencies/types.

Changes:

  • Replaced app/routes/home.tsx with a JSON-driven plot studio including config editing, validation, and a live Plotly preview + export.
  • Added minimal UI primitives (Button, Card) and a Tailwind class merging helper (cn).
  • Added Plotly + react-plotly + UI utility dependencies and TypeScript declaration shims for dynamic imports.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
package.json Adds Plotly/react-plotly and UI utility dependencies.
package-lock.json Locks the newly added dependency graph (notably large due to Plotly ecosystem deps).
app/types.d.ts Adds module declarations to support dynamic imports for Plotly + react-plotly factory.
app/routes/home.tsx Implements the client-side plot studio with config editor, validation, and Plotly rendering/export.
app/lib/utils.ts Adds cn() helper via clsx + tailwind-merge.
app/components/ui/card.tsx Adds Card UI primitives.
app/components/ui/button.tsx Adds Button primitive with CVA variants and Radix Slot support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread package.json
Comment on lines +18 to +23
"lucide-react": "^1.8.0",
"plotly.js-dist-min": "^3.5.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router": "7.14.0"
"react-plotly.js": "^2.6.0",
"react-router": "7.14.0",
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding plotly.js-dist-min alongside react-plotly.js results in both plotly.js (auto-installed as a peer dependency) and plotly.js-dist-min being present in the lockfile, which significantly increases install size/time. Consider satisfying the peer dependency with a single package (e.g., depend on plotly.js and lazy-load it, or use an npm alias so the peer is met without installing both).

Copilot uses AI. Check for mistakes.
Comment thread app/routes/home.tsx
Comment on lines +147 to +151
const [{ default: createPlotlyComponent }, plotly] = await Promise.all([
import("react-plotly.js/factory"),
import("plotly.js-dist-min"),
]);
if (alive) setPlot(() => createPlotlyComponent(plotly as any));
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Plotly factory is being initialized with the module namespace object returned by import("plotly.js-dist-min"), not the Plotly instance itself. createPlotlyComponent expects the Plotly object (the module's default export), so this will break at runtime unless you pass plotly.default (or destructure { default: Plotly } from the import).

Suggested change
const [{ default: createPlotlyComponent }, plotly] = await Promise.all([
import("react-plotly.js/factory"),
import("plotly.js-dist-min"),
]);
if (alive) setPlot(() => createPlotlyComponent(plotly as any));
const [{ default: createPlotlyComponent }, { default: Plotly }] = await Promise.all([
import("react-plotly.js/factory"),
import("plotly.js-dist-min"),
]);
if (alive) setPlot(() => createPlotlyComponent(Plotly));

Copilot uses AI. Check for mistakes.
Comment thread app/routes/home.tsx
Comment on lines +174 to +180
if (parsed.config.version !== 3) {
errors.push(`Config version must be 3. Got ${parsed.config.version}.`);
return { data: [], layout: {}, errors };
}

const dataframe = parsed.config.dataframes[selectedDf];
if (!dataframe) {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsed.config is cast to AppConfig without runtime validation. If the JSON has version: 3 but dataframes is missing/not an array, parsed.config.dataframes[selectedDf] will throw and crash the route. Consider validating that dataframes/frames/materials are arrays (and that required keys exist) before indexing, and return an error instead of throwing.

Copilot uses AI. Check for mistakes.
Comment thread app/routes/home.tsx
return { data, layout, errors };
}, [parsed.config, selectedDf, selectedFrame]);

const frameOptions = parsed.config?.dataframes[selectedDf]?.frames ?? [];
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frameOptions uses optional chaining only on parsed.config, but not on dataframes itself. If dataframes is undefined/null (or not an array) this expression will still throw when evaluating dataframes[selectedDf]. Use optional chaining with ?.[index] (e.g., parsed.config?.dataframes?.[selectedDf]?.frames) to avoid runtime crashes while editing JSON.

Suggested change
const frameOptions = parsed.config?.dataframes[selectedDf]?.frames ?? [];
const frameOptions = parsed.config?.dataframes?.[selectedDf]?.frames ?? [];

Copilot uses AI. Check for mistakes.
Comment thread app/routes/home.tsx
],
};

type Mode = "default" | "max" | "min" | "span";
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mode includes "max" and "min", but resolveQuantity currently only treats "span" specially and otherwise falls back to a single-value lookup. Either implement "max"/"min" handling or drop these modes from the type so configs can't specify unsupported behavior.

Suggested change
type Mode = "default" | "max" | "min" | "span";
type Mode = "default" | "span";

Copilot uses AI. Check for mistakes.
Comment thread app/routes/home.tsx
Comment on lines +298 to +301
if (line.type === "slope" && line.x0 != null && line.y0 != null && line.slope != null) {
const x1 = line.x0 * 5;
const y1 = line.y0 * Math.pow(x1 / line.x0, line.slope);
shapes.push({ type: "line", x0: line.x0, y0: line.y0, x1, y1, xref: "x", yref: "y", line: { color: line.color ?? "#64748b", width: line.width ?? 1.5, dash: "dot" } });
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "slope" guideline computes y1 using a power-law formula (y ∝ x^slope), which is correct for log-log plots but incorrect for linear axes. If log_x/log_y can be false, consider computing slope guidelines differently based on axis type (or explicitly restrict slope guidelines to log scales and validate accordingly).

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +33
function Button({
className,
variant,
size,
asChild = false,
...props
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Button doesn't set a default type, so when used inside a <form> without an explicit type it will default to submit and can trigger unintended form submissions. Consider defaulting to type="button" when asChild is false and no type is provided.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants