From 11dadc505ef89a67922ad5bf95b5547a3a2bb39b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 23 Nov 2024 19:14:06 -0800 Subject: [PATCH] plot.rescale --- src/plot.js | 23 +++++++++++++++++------ test/plots/index.ts | 1 + test/plots/rescale.ts | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 test/plots/rescale.ts diff --git a/src/plot.js b/src/plot.js index d92aca0587..491f477fde 100644 --- a/src/plot.js +++ b/src/plot.js @@ -139,7 +139,7 @@ export function plot(options = {}) { stateByMark.set(mark, {data, facets, channels}); } - // Initalize the scales and dimensions. + // Initialize the scales and dimensions. const scaleDescriptors = createScales(addScaleChannels(channelsByScale, stateByMark, options), options); const dimensions = createDimensions(scaleDescriptors, marks, options); @@ -159,6 +159,11 @@ export function plot(options = {}) { context.className = className; context.projection = createProjection(options, subdimensions); + // A path generator for marks that want to draw GeoJSON. + context.path = function () { + return geoPath(this.projection ?? xyProjection(scales)); + }; + // Allows e.g. the axis mark to determine faceting lazily. context.filterFacets = (data, channels) => { return facetFilter(facets, {channels, groups: facetGroups(data, channels)}); @@ -236,11 +241,6 @@ export function plot(options = {}) { facetTranslate = facetTranslator(fx, fy, dimensions); } - // A path generator for marks that want to draw GeoJSON. - context.path = function () { - return geoPath(this.projection ?? xyProjection(scales)); - }; - // Compute value objects, applying scales and projection as needed. for (const [mark, state] of stateByMark) { state.values = mark.scale(state.channels, scales, context); @@ -357,6 +357,17 @@ export function plot(options = {}) { .text(`${w.toLocaleString("en-US")} warning${w === 1 ? "" : "s"}. Please check the console.`); } + figure.rescale = (rescales) => { + const reoptions = {...options, figure: false}; + for (const key in rescales) { + if (!(key in scales.scales)) throw new Error(`missing scale: ${key}`); + reoptions[key] = {...scales.scales[key], ...rescales[key]}; + } + const resvg = plot(reoptions); + while (svg.lastChild) svg.removeChild(svg.lastChild); + while (resvg.firstChild) svg.appendChild(resvg.firstChild); + }; + return figure; } diff --git a/test/plots/index.ts b/test/plots/index.ts index f54398dca7..8fe0cfa944 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -259,6 +259,7 @@ export * from "./raster-vapor.js"; export * from "./raster-walmart.js"; export * from "./rect-band.js"; export * from "./reducer-scale-override.js"; +export * from "./rescale.js"; export * from "./rounded-rect.js"; export * from "./seattle-precipitation-density.js"; export * from "./seattle-precipitation-rule.js"; diff --git a/test/plots/rescale.ts b/test/plots/rescale.ts new file mode 100644 index 0000000000..127a541567 --- /dev/null +++ b/test/plots/rescale.ts @@ -0,0 +1,18 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function rescaleZoom() { + const data = await d3.csv("data/gistemp.csv", d3.autoType); + const plot = Plot.dot(data, {x: "Date", y: "Anomaly", stroke: "Anomaly"}).plot(); + requestAnimationFrame(() => { + let frame: number; + (function tick(now) { + if (!plot.isConnected) return cancelAnimationFrame(frame); + const t = (Math.sin(now / 2000) + 1) / 2; + const [x1, x2] = plot.scale("x").domain; + plot.rescale({x: {domain: [+x1 + ((x2 - x1) / 2) * t, +x1 + ((x2 - x1) / 2) * (t + 1)]}}); + frame = requestAnimationFrame(tick); + })(performance.now()); + }); + return plot; +}