From 158d071b75bb70af15357cddde6696f73c4fd472 Mon Sep 17 00:00:00 2001 From: jheer Date: Thu, 21 Mar 2024 08:39:33 -0700 Subject: [PATCH] feat: Add initial projection support for interactors. --- packages/plot/src/interactors/Interval1D.js | 9 +++-- packages/plot/src/interactors/Interval2D.js | 7 ++-- packages/plot/src/interactors/Nearest.js | 3 +- packages/plot/src/interactors/util/scale.js | 45 +++++++++++++++++++++ 4 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 packages/plot/src/interactors/util/scale.js diff --git a/packages/plot/src/interactors/Interval1D.js b/packages/plot/src/interactors/Interval1D.js index 4b76800e..e8bc5a08 100644 --- a/packages/plot/src/interactors/Interval1D.js +++ b/packages/plot/src/interactors/Interval1D.js @@ -6,6 +6,7 @@ import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { patchScreenCTM } from './util/patchScreenCTM.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; +import { getScale } from './util/scale.js'; export class Interval1D { constructor(mark, { @@ -62,11 +63,11 @@ export class Interval1D { } init(svg, root) { - const { brush, channel, style } = this; - this.scale = svg.scale(channel); + const { brush, channel, style, mark: { plot } } = this; + this.scale = getScale(svg, channel, plot); - const rx = svg.scale('x').range; - const ry = svg.scale('y').range; + const rx = getScale(svg, 'x', plot).range; + const ry = getScale(svg, 'y', plot).range; brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]); const facets = select(svg).selectAll('g[aria-label="facet"]'); diff --git a/packages/plot/src/interactors/Interval2D.js b/packages/plot/src/interactors/Interval2D.js index afef1bca..5df978cf 100644 --- a/packages/plot/src/interactors/Interval2D.js +++ b/packages/plot/src/interactors/Interval2D.js @@ -6,6 +6,7 @@ import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { patchScreenCTM } from './util/patchScreenCTM.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; +import { getScale } from './util/scale.js'; const asc = (a, b) => a - b; @@ -69,9 +70,9 @@ export class Interval2D { } init(svg) { - const { brush, style } = this; - const xscale = this.xscale = svg.scale('x'); - const yscale = this.yscale = svg.scale('y'); + const { brush, style, mark: { plot } } = this; + const xscale = this.xscale = getScale(svg, 'x', plot); + const yscale = this.yscale = getScale(svg, 'y', plot); const rx = xscale.range; const ry = yscale.range; brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]); diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index 519a4b07..00697af7 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -2,6 +2,7 @@ import { isSelection } from '@uwdata/mosaic-core'; import { eq, literal } from '@uwdata/mosaic-sql'; import { select, pointer } from 'd3'; import { getField } from './util/get-field.js'; +import { getScale } from './util/scale.js'; export class Nearest { constructor(mark, { @@ -36,7 +37,7 @@ export class Nearest { const facets = select(svg).selectAll('g[aria-label="facet"]'); const root = facets.size() ? facets : select(svg); - const scale = svg.scale(channel); + const scale = getScale(svg, channel, mark.plot); const param = !isSelection(selection); root.on('pointerdown pointermove', function(evt) { diff --git a/packages/plot/src/interactors/util/scale.js b/packages/plot/src/interactors/util/scale.js new file mode 100644 index 00000000..453002b0 --- /dev/null +++ b/packages/plot/src/interactors/util/scale.js @@ -0,0 +1,45 @@ +export function getScale(fig, channel, plot) { + const scale = fig.scale(channel); + if (scale) return scale; + + const proj = fig.projection(); + if (proj && (channel === 'x' || channel === 'y')) { + const type = plot.getAttribute('projectionType'); + return projectionScale(proj, type, channel); + } +} + +function projectionScale(proj, type, channel) { + const { offset, translate: [tx, ty], scale, width, height } = proj; + const planar = type === 'identity' || type === 'reflect-y'; + + let range; + let apply; + let invert; + + if (channel === 'x') { + range = [offset[0], offset[0] + width]; + apply = planar ? x => x * scale + tx + : v => proj.stream([v, 0])[0]; + invert = planar ? x => (x - tx) / scale + : proj.invert ? x => proj.invert([(x - tx) / scale, 0])[0] + : null; + } else { + range = [offset[1], offset[1] + height]; + apply = type === 'identity' ? y => y * scale + ty + : type === 'reflect-y' ? y => -y * scale + ty + : v => proj.stream([0, v])[1]; + invert = type === 'identity' ? y => (y - ty) / scale + : type === 'reflect-y' ? y => (ty - y) / scale + : proj.invert ? y => proj.invert([0, (y - ty) / scale])[1] + : null; + } + + return { + type: planar || type === 'equirectangular' ? 'linear' : `${type}-x`, + domain: range.map(v => invert(v)), + range, + apply, + invert + }; +}