Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
},
"license": "ISC",
"type": "module",
"main": "src/index.ts",
"module": "src/index.ts",
"main": "dist/esm/index.js",
"module": "dist/esm/index.js",
"jsdelivr": "dist/replot.umd.min.js",
"unpkg": "dist/replot.umd.min.js",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"umd": "./dist/replot.umd.min.js",
"default": "./src/index.ts"
"default": "./dist/esm/index.js"
},
"./react": {
"types": "./dist/types/react/index.d.ts",
"default": "./src/react/index.tsx"
"default": "./dist/esm/react/index.js"
},
"./package.json": "./package.json"
},
Expand All @@ -46,11 +46,7 @@
},
"files": [
"dist/**/*.js",
"dist/types/**/*.d.ts",
"src/**/*.d.ts",
"src/**/*.js",
"src/**/*.ts",
"src/**/*.tsx"
"dist/types/**/*.d.ts"
],
"scripts": {
"test": "yarn test:mocha && yarn test:tsc && yarn test:lint && yarn test:prettier && yarn test:package",
Expand All @@ -63,16 +59,16 @@
"test:prettier": "prettier --check src test",
"test:tsc": "tsc",
"build:types": "tsc -p tsconfig.build.json && node scripts/copy-declarations.mjs",
"bundle": "vite build --config vite.bundle.config.ts && MINIFY=1 vite build --config vite.bundle.config.ts && yarn build:types",
"build:esm": "tsc -p tsconfig.esm.json && node scripts/copy-sources.mjs",
"bundle": "vite build --config vite.bundle.config.ts && MINIFY=1 vite build --config vite.bundle.config.ts && yarn build:esm && yarn build:types",
"prepublishOnly": "rm -rf dist && yarn bundle",
"dev": "vite",
"docs:dev": "tsx docs/scripts/ensure-bundle.ts && tsx docs/scripts/build-api-data.ts && vite --config docs/vite.docs.config.ts",
"docs:build": "yarn bundle && tsx docs/scripts/build-api-data.ts && vite build --config docs/vite.docs.config.ts",
"docs:preview": "vite preview --config docs/vite.docs.config.ts"
},
"sideEffects": [
"./src/index.js",
"./src/index.ts"
"./dist/esm/index.js"
],
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.2",
Expand Down
45 changes: 45 additions & 0 deletions scripts/copy-sources.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copies the plain-JavaScript sources that `tsc -p tsconfig.esm.json` declines
// to emit (see issue #140).
//
// Most of the core is plain .js with a sibling hand-written .d.ts
// (src/channel.js + src/channel.d.ts, …). TypeScript treats the .d.ts as the
// authoritative module for that specifier and skips compiling the .js, so
// without this step dist/esm is full of dangling imports. The files are
// already ESM, so copying is the whole job.
//
// The companion for declarations is scripts/copy-declarations.mjs.
import {mkdir, copyFile, readdir, access} from "node:fs/promises";
import {dirname, join, relative} from "node:path";
import {fileURLToPath} from "node:url";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const source = join(root, "src");
const target = join(root, "dist", "esm");

async function* sources(dir) {
for (const entry of await readdir(dir, {withFileTypes: true})) {
const path = join(dir, entry.name);
if (entry.isDirectory()) yield* sources(path);
else if (entry.name.endsWith(".js")) yield path;
}
}

async function exists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}

let copied = 0;
for await (const path of sources(source)) {
const destination = join(target, relative(source, path));
if (await exists(destination)) continue; // tsc emitted it; leave that alone
await mkdir(dirname(destination), {recursive: true});
await copyFile(path, destination);
copied++;
}

console.log(`copy-sources: copied ${copied} JavaScript source${copied === 1 ? "" : "s"} into dist/esm`);
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from "./marks/vector.js";
export * from "./marks/waffle.js";
export {valueof, column, identity, indexOf} from "./options.js";
export * from "./plot.js";
export * from "./plotDom.js";
export * from "./projection.js";
export * from "./reducer.js";
export * from "./scales.js";
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {Mark} from "./mark.js";
import {plot} from "./plot.js";
import {plot} from "./plotDom.js";

// Note: this side effect avoids a circular dependency.
Mark.prototype.plot = function ({marks = [], ...options} = {}) {
return plot({...options, marks: [...marks, this]});
};

export {plot} from "./plot.js";
export {plot} from "./plotDom.js";
export {Mark, marks} from "./mark.js";
export {Area, area, areaX, areaY} from "./marks/area.js";
export {Arrow, arrow} from "./marks/arrow.js";
Expand Down
87 changes: 2 additions & 85 deletions src/plot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,19 @@ import {createContext} from "./context.js";
import {createDimensions} from "./dimensions.js";
import {createFacets, recreateFacets, facetExclude, facetGroups, facetTranslator, facetFilter} from "./facet.js";
import {pointer, pointerX, pointerY} from "./interactions/pointer.js";
import {buildAutoLegends, renderLegendElement} from "./react/legends/Legend.js";
import {Mark} from "./mark.js";
import {axisFx, axisFy, axisX, axisY, gridFx, gridFy, gridX, gridY} from "./marks/axis.js";
import {frame} from "./marks/frame.js";
import {tip} from "./marks/tip.js";
import {isColor, isIterable, isNone, isScaleOptions} from "./options.js";
import {dataify, lengthof, map, yes, maybeIntervalTransform} from "./options.js";
import {createProjection, getGeometryChannels, hasProjection, xyProjection} from "./projection.js";
import {createScales, createScaleFunctions, autoScaleRange, exposeScales} from "./scales.js";
import {createScales, createScaleFunctions, autoScaleRange} from "./scales.js";
import {innerDimensions, outerDimensions} from "./scales.js";
import {isPosition, registry as scaleRegistry} from "./scales/index.js";
import {maybeClassName} from "./style.js";
import {initializer} from "./transforms/basic.js";
import {consumeWarnings, warn} from "./warnings.js";
import {renderToStaticMarkup} from "react-dom/server";
import {buildStaticPlotSvg} from "./react/renderStatic.js";
import {warn} from "./warnings.js";

// Returns the pre-render state needed by both the imperative DOM build path
// (used by `plot()` below) and the React JSX path (used by `<Plot>` in
Expand Down Expand Up @@ -276,86 +273,6 @@ export function computePlot(options: any = {}): any {
};
}

export function plot(options: any = {}) {
const computed: any = computePlot(options);
const {className, scales, scaleDescriptors, context} = computed;
const {style, title, subtitle, caption} = options;
const document = context.document;
const figureHolder: {current: any} = context.figureHolder;

// Drain warnings emitted during computePlot so the ⚠️ indicator renders and
// the warn() dedupe state is reset (matching the React <Plot> path).
const warnings = consumeWarnings();

// Render the marks to a detached <svg> via React's renderJSX — no
// d3-selection. The same renderMarksWith/renderJSX code powers <Plot>, so
// the imperative and JSX outputs stay in lockstep. We serialize to markup
// and reparse into the target document (which may be a custom jsdom doc).
const markup = renderToStaticMarkup(buildStaticPlotSvg(computed, warnings, options.className));
const holder = document.createElement("div");
holder.innerHTML = markup;
const svg: any = holder.firstElementChild;

// Apply the plot-level style option (string or object), mirroring
// applyInlineStyles on the former imperative path.
if (typeof style === "string") svg.setAttribute("style", style);
else if (style != null) Object.assign(svg.style, style);

figureHolder.current = svg;

// Wrap the plot in a figure, if needed. Auto-legends render via the React
// legend components (no d3-selection); serialize each to a DOM node in the
// target document, matching the former createLegends output.
const legends = buildAutoLegends(scaleDescriptors, context, options).map((el) => {
const h = document.createElement("div");
h.innerHTML = renderToStaticMarkup(el);
return h.firstElementChild;
});
const {figure: figured = title != null || subtitle != null || caption != null || legends.length > 0} = options;
if (figured) {
const fig: any = document.createElement("figure");
fig.className = `${className}-figure`;
fig.style.maxWidth = "initial"; // avoid Observable default style
if (title != null) fig.append(createTitleElement(document, title, "h2"));
if (subtitle != null) fig.append(createTitleElement(document, subtitle, "h3"));
fig.append(...legends, svg);
if (caption != null) fig.append(createFigcaption(document, caption));
if ("value" in svg) (fig.value = svg.value), delete svg.value;
figureHolder.current = fig;
}

figureHolder.current.scale = exposeScales(scales.scales);
// The .legend(key, options) method renders via the React legend components
// (no d3-selection); serialize to a DOM node in the target document.
figureHolder.current.legend = (key: string, legendOptions: any = {}) => {
if (key !== "color" && key !== "opacity" && key !== "symbol") throw new Error(`unknown legend type: ${key}`);
if (!(key in scaleDescriptors)) return;
const el = renderLegendElement(key, legendOptions, scaleDescriptors, context, options);
if (el == null) return;
// Render into the per-call document option if given (e.g. a separate jsdom
// window), else the plot's document.
const targetDoc = legendOptions?.document ?? document;
const h = targetDoc.createElement("div");
h.innerHTML = renderToStaticMarkup(el);
return h.firstElementChild;
};

return figureHolder.current;
}

function createTitleElement(document, contents, tag) {
if (contents.ownerDocument) return contents;
const e = document.createElement(tag);
e.append(contents);
return e;
}

function createFigcaption(document, caption) {
const e = document.createElement("figcaption");
e.append(caption);
return e;
}

function flatMarks(marks) {
return marks
.flat(Infinity)
Expand Down
96 changes: 96 additions & 0 deletions src/plotDom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {buildAutoLegends, renderLegendElement} from "./react/legends/Legend.js";
import {exposeScales} from "./scales.js";
import {consumeWarnings} from "./warnings.js";
import {computePlot} from "./plot.js";
import {renderToStaticMarkup} from "react-dom/server";
import {buildStaticPlotSvg} from "./react/renderStatic.js";

// The imperative entry point: plot() builds real DOM, so it is the only part
// of the core that needs a renderer capable of serializing React elements.
//
// It lives apart from computePlot (src/plot.ts) deliberately. <Plot> imports
// computePlot, and react-dom declares no `sideEffects: false`, so a bundler
// keeps the react-dom/server import of any module it pulls in — even when the
// binding is unused. Sharing a module with computePlot therefore put the whole
// server renderer in the client bundle of every app that renders a plot
// (issue #140).

export function plot(options: any = {}) {
const computed: any = computePlot(options);
const {className, scales, scaleDescriptors, context} = computed;
const {style, title, subtitle, caption} = options;
const document = context.document;
const figureHolder: {current: any} = context.figureHolder;

// Drain warnings emitted during computePlot so the ⚠️ indicator renders and
// the warn() dedupe state is reset (matching the React <Plot> path).
const warnings = consumeWarnings();

// Render the marks to a detached <svg> via React's renderJSX — no
// d3-selection. The same renderMarksWith/renderJSX code powers <Plot>, so
// the imperative and JSX outputs stay in lockstep. We serialize to markup
// and reparse into the target document (which may be a custom jsdom doc).
const markup = renderToStaticMarkup(buildStaticPlotSvg(computed, warnings, options.className));
const holder = document.createElement("div");
holder.innerHTML = markup;
const svg: any = holder.firstElementChild;

// Apply the plot-level style option (string or object), mirroring
// applyInlineStyles on the former imperative path.
if (typeof style === "string") svg.setAttribute("style", style);
else if (style != null) Object.assign(svg.style, style);

figureHolder.current = svg;

// Wrap the plot in a figure, if needed. Auto-legends render via the React
// legend components (no d3-selection); serialize each to a DOM node in the
// target document, matching the former createLegends output.
const legends = buildAutoLegends(scaleDescriptors, context, options).map((el) => {
const h = document.createElement("div");
h.innerHTML = renderToStaticMarkup(el);
return h.firstElementChild;
});
const {figure: figured = title != null || subtitle != null || caption != null || legends.length > 0} = options;
if (figured) {
const fig: any = document.createElement("figure");
fig.className = `${className}-figure`;
fig.style.maxWidth = "initial"; // avoid Observable default style
if (title != null) fig.append(createTitleElement(document, title, "h2"));
if (subtitle != null) fig.append(createTitleElement(document, subtitle, "h3"));
fig.append(...legends, svg);
if (caption != null) fig.append(createFigcaption(document, caption));
if ("value" in svg) (fig.value = svg.value), delete svg.value;
figureHolder.current = fig;
}

figureHolder.current.scale = exposeScales(scales.scales);
// The .legend(key, options) method renders via the React legend components
// (no d3-selection); serialize to a DOM node in the target document.
figureHolder.current.legend = (key: string, legendOptions: any = {}) => {
if (key !== "color" && key !== "opacity" && key !== "symbol") throw new Error(`unknown legend type: ${key}`);
if (!(key in scaleDescriptors)) return;
const el = renderLegendElement(key, legendOptions, scaleDescriptors, context, options);
if (el == null) return;
// Render into the per-call document option if given (e.g. a separate jsdom
// window), else the plot's document.
const targetDoc = legendOptions?.document ?? document;
const h = targetDoc.createElement("div");
h.innerHTML = renderToStaticMarkup(el);
return h.firstElementChild;
};

return figureHolder.current;
}

function createTitleElement(document, contents, tag) {
if (contents.ownerDocument) return contents;
const e = document.createElement(tag);
e.append(contents);
return e;
}

function createFigcaption(document, caption) {
const e = document.createElement("figcaption");
e.append(caption);
return e;
}
Loading
Loading