diff --git a/src/mark.js b/src/mark.js index bdc2792c0f..64c17b17b3 100644 --- a/src/mark.js +++ b/src/mark.js @@ -45,6 +45,12 @@ export class Mark { }) }; } + update(g, values) { + const svg = g.ownerSVGElement; + console.log(values); + svg.value = values; + svg.dispatchEvent(new CustomEvent('input')); + } } // TODO Type coercion? diff --git a/src/marks/bar.js b/src/marks/bar.js index 89916bd249..799975b41d 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -47,7 +47,7 @@ export class AbstractBar extends Mark { render(I, scales, channels, dimensions) { const {rx, ry} = this; const {color} = scales; - const {z: Z, title: L, fill: F, stroke: S} = channels; + const {z: Z, title: L, fill: F, stroke: S, picker: J} = channels; const index = filter(I, ...this._positions(channels), F, S); if (Z) index.sort((i, j) => ascending(Z[i], Z[j])); return create("svg:g") @@ -56,6 +56,10 @@ export class AbstractBar extends Mark { .call(g => g.selectAll() .data(index) .join("rect") + .call(J ? rect => rect + .on("click", (event, i) => super.update(event.currentTarget, J[i])) + : () => {} + ) .call(applyDirectStyles, this) .attr("x", this._x(scales, channels, dimensions)) .attr("width", this._width(scales, channels, dimensions)) @@ -89,13 +93,14 @@ export class AbstractBar extends Mark { } export class BarX extends AbstractBar { - constructor(data, {x1, x2, y, ...options} = {}) { + constructor(data, {x1, x2, y, picker = d => d, ...options} = {}) { super( data, [ {name: "x1", value: x1, scale: "x"}, {name: "x2", value: x2, scale: "x"}, - {name: "y", value: y, scale: "y", type: "band", optional: true} + {name: "y", value: y, scale: "y", type: "band", optional: true}, + {name: "picker", value: picker, optional: true} ], options ); @@ -117,13 +122,14 @@ export class BarX extends AbstractBar { } export class BarY extends AbstractBar { - constructor(data, {x, y1, y2, ...options} = {}) { + constructor(data, {x, y1, y2, picker = d => d, ...options} = {}) { super( data, [ {name: "y1", value: y1, scale: "y"}, {name: "y2", value: y2, scale: "y"}, - {name: "x", value: x, scale: "x", type: "band", optional: true} + {name: "x", value: x, scale: "x", type: "band", optional: true}, + {name: "picker", value: picker, optional: true} ], options ); diff --git a/src/marks/rect.js b/src/marks/rect.js index a2f701541a..69dc006bdf 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -16,6 +16,7 @@ export class Rect extends Mark { title, fill, stroke, + picker = d => d, inset = 0, insetTop = inset, insetRight = inset, @@ -38,7 +39,8 @@ export class Rect extends Mark { {name: "z", value: z, optional: true}, {name: "title", value: title, optional: true}, {name: "fill", value: vfill, scale: "color", optional: true}, - {name: "stroke", value: vstroke, scale: "color", optional: true} + {name: "stroke", value: vstroke, scale: "color", optional: true}, + {name: "picker", value: picker, optional: true} ], options ); @@ -53,7 +55,7 @@ export class Rect extends Mark { render( I, {x, y, color}, - {x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, title: L, fill: F, stroke: S} + {x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, title: L, fill: F, stroke: S, picker: J} ) { const {rx, ry} = this; const index = filter(I, X1, Y2, X2, Y2, F, S); @@ -64,6 +66,10 @@ export class Rect extends Mark { .call(g => g.selectAll() .data(index) .join("rect") + .call(J ? rect => rect + .on("click", (event, i) => super.update(event.currentTarget, J[i])) + : () => {} + ) .call(applyDirectStyles, this) .attr("x", i => Math.min(x(X1[i]), x(X2[i])) + this.insetLeft) .attr("y", i => Math.min(y(Y1[i]), y(Y2[i])) + this.insetTop) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 24195df416..214dfa450e 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -6,27 +6,27 @@ import {offset} from "../style.js"; // Group on y, z, fill, or stroke, if any, then bin on x. export function binX({x, y, out = y == null ? "y" : "fill", inset, insetLeft, insetRight, ...options} = {}) { ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); - const [transform, x1, x2, l] = bin1(x, "y", {y, ...options}); - return {x1, x2, ...transform, inset, insetLeft, insetRight, [out]: l}; + const [transform, x1, x2, l, picker] = bin1(x, "y", {y, ...options}); + return {x1, x2, ...transform, picker, inset, insetLeft, insetRight, [out]: l}; } // Group on y, z, fill, or stroke, if any, then bin on x. export function binXMid({x, out = "r", ...options} = {}) { - const [transform, x1, x2, l] = bin1(x, "y", options); - return {x: mid(x1, x2), ...transform, [out]: l}; + const [transform, x1, x2, l, picker] = bin1(x, "y", options); + return {x: mid(x1, x2), ...transform, picker, [out]: l}; } // Group on x, z, fill, or stroke, if any, then bin on y. export function binY({y, x, out = x == null ? "x" : "fill", inset, insetTop, insetBottom, ...options} = {}) { ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); - const [transform, y1, y2, l] = bin1(y, "x", {x, ...options}); - return {y1, y2, ...transform, inset, insetTop, insetBottom, [out]: l}; + const [transform, y1, y2, l, picker] = bin1(y, "x", {x, ...options}); + return {y1, y2, ...transform, picker, inset, insetTop, insetBottom, [out]: l}; } // Group on y, z, fill, or stroke, if any, then bin on x. export function binYMid({y, out = "r", ...options} = {}) { - const [transform, y1, y2, l] = bin1(y, "x", options); - return {y: mid(y1, y2), ...transform, [out]: l}; + const [transform, y1, y2, l, picker] = bin1(y, "x", options); + return {y: mid(y1, y2), ...transform, picker, [out]: l}; } // Group on z, fill, or stroke, if any, then bin on x and y. @@ -39,8 +39,8 @@ export function binR({x, y, ...options} = {}) { export function bin({x, y, out = "fill", inset, insetTop, insetRight, insetBottom, insetLeft, ...options} = {}) { ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); - const [transform, x1, x2, y1, y2, l] = bin2(x, y, options); - return {x1, x2, y1, y2, ...transform, inset, insetTop, insetRight, insetBottom, insetLeft, [out]: l}; + const [transform, x1, x2, y1, y2, l, picker] = bin2(x, y, options); + return {x1, x2, y1, y2, ...transform, picker, inset, insetTop, insetRight, insetBottom, insetLeft, [out]: l}; } function bin1(x, key, {[key]: k, z, fill, stroke, weight, domain, thresholds, normalize, cumulative, ...options} = {}) { @@ -55,6 +55,7 @@ function bin1(x, key, {[key]: k, z, fill, stroke, weight, domain, thresholds, no const [BZ, setBZ] = maybeLazyChannel(z); const [BF = fill, setBF] = maybeLazyChannel(vfill); const [BS = stroke, setBS] = maybeLazyChannel(vstroke); + const [J, setJ] = lazyChannel(); return [ { ...key && {[key]: BK}, @@ -107,12 +108,14 @@ function bin1(x, key, {[key]: k, z, fill, stroke, weight, domain, thresholds, no } binFacets.push(binFacet); } + setJ(binData); return {data: binData, facets: binFacets}; }) }, X1, X2, - L + L, + J ]; } @@ -135,6 +138,7 @@ function bin2(x, y, {weight, domain, thresholds, normalize, z, fill, stroke, ... const [BZ, setBZ] = maybeLazyChannel(z); const [BF = fill, setBF] = maybeLazyChannel(vfill); const [BS = stroke, setBS] = maybeLazyChannel(vstroke); + const [J, setJ] = lazyChannel(); return [ { z: BZ, @@ -184,6 +188,7 @@ function bin2(x, y, {weight, domain, thresholds, normalize, z, fill, stroke, ... } binFacets.push(binFacet); } + setJ(binData); return {data: binData, facets: binFacets}; }) }, @@ -191,7 +196,8 @@ function bin2(x, y, {weight, domain, thresholds, normalize, z, fill, stroke, ... X2, Y1, Y2, - L + L, + J ]; } diff --git a/src/transforms/group.js b/src/transforms/group.js index 7dd4dd9911..acccdb5986 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -4,14 +4,14 @@ import {valueof, maybeColor, maybeTransform, maybeValue, maybeLazyChannel, lazyC // Group on y, z, fill, or stroke, if any, then group on x. export function groupX({x, y, out = y == null ? "y" : "fill", ...options} = {}) { - const [transform, X, l] = group1(x, "y", {y, ...options}); - return {x: X, ...transform, [out]: l}; + const [transform, X, l, picker] = group1(x, "y", {y, ...options}); + return {x: X, ...transform, picker, [out]: l}; } // Group on x, z, fill, or stroke, if any, then group on y. export function groupY({y, x, out = x == null ? "x" : "fill", ...options} = {}) { - const [transform, Y, l] = group1(y, "x", {x, ...options}); - return {y: Y, ...transform, [out]: l}; + const [transform, Y, l, picker] = group1(y, "x", {x, ...options}); + return {y: Y, ...transform, picker, [out]: l}; } // Group on z, fill, or stroke, if any. @@ -20,8 +20,8 @@ export function groupR(options) { } export function group({x, y, out = "fill", ...options} = {}) { - const [transform, X, Y, L] = group2(x, y, options); - return {x: X, y: Y, ...transform, [out]: L}; + const [transform, X, Y, L, picker] = group2(x, y, options); + return {x: X, y: Y, ...transform, picker, [out]: L}; } function group1(x = identity, key, {[key]: k, weight, domain, normalize, z, fill, stroke, ...options} = {}) { @@ -34,6 +34,7 @@ function group1(x = identity, key, {[key]: k, weight, domain, normalize, z, fill const [BZ, setBZ] = maybeLazyChannel(z); const [BF = fill, setBF] = maybeLazyChannel(vfill); const [BS = stroke, setBS] = maybeLazyChannel(vstroke); + const [J, setJ] = lazyChannel(); const defined = maybeDomain(domain); return [ { @@ -79,11 +80,13 @@ function group1(x = identity, key, {[key]: k, weight, domain, normalize, z, fill } groupFacets.push(groupFacet); } + setJ(groupData); return {data: groupData, facets: groupFacets}; }) }, X, - L + L, + J ]; } @@ -100,6 +103,7 @@ function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} const [vstroke] = maybeColor(stroke); const [F = fill, setF] = maybeLazyChannel(vfill); const [S = stroke, setS] = maybeLazyChannel(vstroke); + const [J, setJ] = lazyChannel(); const xdefined = maybeDomain(xdomain); const ydefined = maybeDomain(ydomain); return [ @@ -148,12 +152,14 @@ function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} } groupFacets.push(groupFacet); } + setJ(groupData); return {data: groupData, facets: groupFacets}; }) }, X, Y, - L + L, + J ]; } diff --git a/test/marks/bar-test.js b/test/marks/bar-test.js index 9574e796e6..46992ff23d 100644 --- a/test/marks/bar-test.js +++ b/test/marks/bar-test.js @@ -5,9 +5,9 @@ tape("barX() has the expected defaults", test => { const bar = Plot.barX(); test.strictEqual(bar.data, undefined); test.strictEqual(bar.transform, undefined); - test.deepEqual(bar.channels.map(c => c.name), ["x1", "x2"]); - test.deepEqual(bar.channels.map(c => Plot.valueof([1, 2, 3], c.value)), [[0, 0, 0], [1, 2, 3]]); - test.deepEqual(bar.channels.map(c => c.scale), ["x", "x"]); + test.deepEqual(bar.channels.map(c => c.name), ["x1", "x2", "picker"]); + test.deepEqual(bar.channels.map(c => Plot.valueof([1, 2, 3], c.value)), [[0, 0, 0], [1, 2, 3], [1, 2, 3]]); + test.deepEqual(bar.channels.map(c => c.scale), ["x", "x", undefined]); test.strictEqual(bar.fill, undefined); test.strictEqual(bar.fillOpacity, undefined); test.strictEqual(bar.stroke, undefined); @@ -26,8 +26,8 @@ tape("barX() has the expected defaults", test => { tape("barX(data, {y}) uses a band scale", test => { const bar = Plot.barX(undefined, {y: "x"}); - test.deepEqual(bar.channels.map(c => c.name), ["x1", "x2", "y"]); - test.deepEqual(bar.channels.map(c => c.scale), ["x", "x", "y"]); + test.deepEqual(bar.channels.map(c => c.name), ["x1", "x2", "y", "picker"]); + test.deepEqual(bar.channels.map(c => c.scale), ["x", "x", "y", undefined]); test.strictEqual(bar.channels.find(c => c.name === "y").type, "band"); test.strictEqual(bar.channels.find(c => c.name === "y").value.label, "x"); }); @@ -99,9 +99,9 @@ tape("barY() has the expected defaults", test => { const bar = Plot.barY(); test.strictEqual(bar.data, undefined); test.strictEqual(bar.transform, undefined); - test.deepEqual(bar.channels.map(c => c.name), ["y1", "y2"]); - test.deepEqual(bar.channels.map(c => Plot.valueof([1, 2, 3], c.value)), [[0, 0, 0], [1, 2, 3]]); - test.deepEqual(bar.channels.map(c => c.scale), ["y", "y"]); + test.deepEqual(bar.channels.map(c => c.name), ["y1", "y2", "picker"]); + test.deepEqual(bar.channels.map(c => Plot.valueof([1, 2, 3], c.value)), [[0, 0, 0], [1, 2, 3], [1, 2, 3]]); + test.deepEqual(bar.channels.map(c => c.scale), ["y", "y", undefined]); test.strictEqual(bar.fill, undefined); test.strictEqual(bar.fillOpacity, undefined); test.strictEqual(bar.stroke, undefined); @@ -119,7 +119,7 @@ tape("barY() has the expected defaults", test => { }); tape("barY(data, {x}) uses a band scale", test => { - const bar = Plot.barY(undefined, {x: "y"}); + const bar = Plot.barY(undefined, {x: "y", picker: null}); test.deepEqual(bar.channels.map(c => c.name), ["y1", "y2", "x"]); test.deepEqual(bar.channels.map(c => c.scale), ["y", "y", "x"]); test.strictEqual(bar.channels.find(c => c.name === "x").type, "band"); diff --git a/test/marks/rect-test.js b/test/marks/rect-test.js index 9bbb5be4ce..565265ad80 100644 --- a/test/marks/rect-test.js +++ b/test/marks/rect-test.js @@ -2,7 +2,7 @@ import * as Plot from "@observablehq/plot"; import tape from "tape-await"; tape("rect(data, options) has the expected defaults", test => { - const rect = Plot.rect(undefined, {x1: "0", y1: "1", x2: "2", y2: "3"}); + const rect = Plot.rect(undefined, {x1: "0", y1: "1", x2: "2", y2: "3", picker: null}); test.strictEqual(rect.data, undefined); test.strictEqual(rect.transform, undefined); test.deepEqual(rect.channels.map(c => c.name), ["x1", "y1", "x2", "y2"]);