diff --git a/src/marks/dot.js b/src/marks/dot.js index 58e529def2..4bb551375e 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -92,8 +92,7 @@ export class Dot extends Mark { circle ? (selection) => { selection - .attr("cx", X ? (i) => X[i] : cx) - .attr("cy", Y ? (i) => Y[i] : cy) + .attr("transform", template`translate(${X ? (i) => X[i] : cx},${Y ? (i) => Y[i] : cy})`) .attr("r", R ? (i) => R[i] : r); } : (selection) => { diff --git a/src/scales.js b/src/scales.js index 7c70b9876d..70f5e94c4d 100644 --- a/src/scales.js +++ b/src/scales.js @@ -249,7 +249,7 @@ export function normalizeScale(key, scale, hint) { return createScale(key, hint === undefined ? undefined : [{hint}], {...scale}); } -function createScale(key, channels = [], options = {}) { +export function createScale(key, channels = [], options = {}) { const type = inferScaleType(key, channels, options); // Warn for common misuses of implicit ordinal scales. We disable this test if diff --git a/test/plots/index.ts b/test/plots/index.ts index 0a3ee3eb0d..af8fde34d1 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -340,3 +340,4 @@ export * from "./yearly-requests-dot.js"; export * from "./yearly-requests-line.js"; export * from "./yearly-requests.js"; export * from "./young-adults.js"; +export * from "./zoom-dot.js"; diff --git a/test/plots/zoom-dot.js b/test/plots/zoom-dot.js new file mode 100644 index 0000000000..d27ade2856 --- /dev/null +++ b/test/plots/zoom-dot.js @@ -0,0 +1,80 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {createScale, createScaleFunctions} from "../../src/scales.js"; + +export async function zoomDot() { + const data = await d3.csv("data/gistemp.csv", d3.autoType); + return Plot.plot({ + axis: null, + color: {scheme: "BuRd"}, + marks: [Plot.dot(data, zoom({x: "Date", y: "Anomaly", stroke: "Anomaly"}))] + }); +} + +// TODO +// - only one zoom per SVG, not per mark+facet +// - compute a new projection +function zoom(options) { + return { + ...options, + render(index, scales, values, dimensions, context, next) { + let g = next(index, scales, values, dimensions, context); + + // Strategy 1: Recompute the scale domains, then re-scale the channels. + // const {x, y} = scales.scales; + // const {x: X, y: Y} = values.channels; + // d3.select(context.ownerSVGElement).call( + // d3.zoom().on("start zoom end", ({transform}) => { + // const xDomain = Array.from(x.range, (v) => x.invert(transform.invertX(v))); + // const yDomain = Array.from(y.range, (v) => y.invert(transform.invertY(v))); + // const xScale = createScale("x", [], {...x, domain: xDomain}); + // const yScale = createScale("y", [], {...y, domain: yDomain}); + // const zoomScales = createScaleFunctions({x: xScale, y: yScale}); + // const zoomValues = this.scale({x: X, y: Y}, zoomScales, context); + // const mergeScales = {...scales, ...zoomScales, scales: {...scales.scales, ...zoomScales.scales}}; + // const mergeValues = {...values, ...zoomValues, channels: {...values.channels, ...zoomValues.channels}}; + // const r = next(index, mergeScales, mergeValues, dimensions, context); + // g.replaceWith(r); + // g = r; + // }) + // ); + + // Strategy 2: Transform the scaled channel values. + // d3.select(context.ownerSVGElement).call( + // d3.zoom().on("start zoom end", ({transform}) => { + // const x = values.x.map(transform.applyX, transform); + // const y = values.y.map(transform.applyY, transform); + // const r = next(index, scales, {...values, x, y}, dimensions, context); + // g.replaceWith(r); + // g = r; + // }) + // ); + + // Strategy 3: Transform the G element. + // const T = Array.from(g.children, (c) => c.getAttribute("transform")); + // d3.select(context.ownerSVGElement).call( + // d3.zoom().on("start zoom end", ({transform}) => { + // g.setAttribute("transform", String(transform)); + // for (let i = 0; i < g.children.length; ++i) { + // g.children[i].setAttribute("transform", `${T[i]} scale(${1 / transform.k})`); + // } + // }) + // ); + + // Strategy 4: Update the circles directly. + const {x: X, y: Y} = values; + d3.select(context.ownerSVGElement).call( + d3.zoom().on("start zoom end", ({transform}) => { + for (let i = 0; i < index.length; ++i) { + const j = index[i]; + const xj = transform.applyX(X[j]); + const yj = transform.applyY(Y[j]); + g.children[i].setAttribute("transform", `translate(${xj},${yj})`); + } + }) + ); + + return g; + } + }; +}