Skip to content

Commit cd27b5b

Browse files
committed
clip: true clips the mark to the frame
closes #165 related: #181
1 parent 776d3ed commit cd27b5b

20 files changed

+7365
-19
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ All marks support the following optional channels:
646646
* **title** - a tooltip (a string of text, possibly with newlines)
647647
* **href** - a URL to link to
648648
* **ariaLabel** - a short label representing the value in the accessibility tree
649+
* **clip** - if true, the mark is clipped to the frame’s dimensions
649650

650651
The **fill**, **fillOpacity**, **stroke**, **strokeWidth**, **strokeOpacity**, and **opacity** options can be specified as either channels or constants. When the fill or stroke is specified as a function or array, it is interpreted as a channel; when the fill or stroke is specified as a string, it is interpreted as a constant if a valid CSS color and otherwise it is interpreted as a column name for a channel. Similarly when the fill opacity, stroke opacity, object opacity, stroke width, or radius is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel.
651652

src/marks/area.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {Curve} from "../curve.js";
33
import {defined} from "../defined.js";
44
import {Mark} from "../plot.js";
55
import {indexOf, maybeZ} from "../options.js";
6-
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
6+
import {applyClip, applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
77
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
88
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
99

@@ -30,12 +30,13 @@ export class Area extends Mark {
3030
);
3131
this.curve = Curve(curve, tension);
3232
}
33-
render(I, {x, y}, channels) {
33+
render(I, {x, y}, channels, dimensions) {
3434
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z} = channels;
3535
const {dx, dy} = this;
3636
return create("svg:g")
3737
.call(applyIndirectStyles, this)
3838
.call(applyTransform, x, y, dx, dy)
39+
.call(applyClip, this, dimensions)
3940
.call(g => g.selectAll()
4041
.data(Z ? group(I, i => Z[i]).values() : [I])
4142
.join("path")

src/marks/arrow.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {create} from "d3";
22
import {radians} from "../math.js";
33
import {Mark} from "../plot.js";
4-
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
4+
import {applyClip, applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
55
import {maybeSameValue} from "./link.js";
66

77
const defaults = {
@@ -44,7 +44,7 @@ export class Arrow extends Mark {
4444
this.insetStart = +insetStart;
4545
this.insetEnd = +insetEnd;
4646
}
47-
render(index, {x, y}, channels) {
47+
render(index, {x, y}, channels, dimensions) {
4848
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, SW} = channels;
4949
const {dx, dy, strokeWidth, bend, headAngle, headLength, insetStart, insetEnd} = this;
5050
const sw = SW ? i => SW[i] : () => strokeWidth;
@@ -67,6 +67,7 @@ export class Arrow extends Mark {
6767
return create("svg:g")
6868
.call(applyIndirectStyles, this)
6969
.call(applyTransform, x, y, offset + dx, offset + dy)
70+
.call(applyClip, this, dimensions)
7071
.call(g => g.selectAll()
7172
.data(index)
7273
.join("path")

src/marks/bar.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {create} from "d3";
22
import {Mark} from "../plot.js";
33
import {number} from "../options.js";
44
import {isCollapsed} from "../scales.js";
5-
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
5+
import {applyClip, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
66
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
77
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
88
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
@@ -22,6 +22,7 @@ export class AbstractBar extends Mark {
2222
const {dx, dy, rx, ry} = this;
2323
return create("svg:g")
2424
.call(applyIndirectStyles, this)
25+
.call(applyClip, this, dimensions)
2526
.call(this._transform, scales, dx, dy)
2627
.call(g => g.selectAll()
2728
.data(index)

src/marks/dot.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {create, path, symbolCircle} from "d3";
22
import {positive} from "../defined.js";
33
import {identity, maybeFrameAnchor, maybeNumberChannel, maybeSymbolChannel, maybeTuple} from "../options.js";
44
import {Mark} from "../plot.js";
5-
import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
5+
import {applyClip, applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
66

77
const defaults = {
88
ariaLabel: "dot",
@@ -56,6 +56,7 @@ export class Dot extends Mark {
5656
return create("svg:g")
5757
.call(applyIndirectStyles, this)
5858
.call(applyTransform, x, y, offset + dx, offset + dy)
59+
.call(applyClip, this, dimensions)
5960
.call(g => g.selectAll()
6061
.data(index)
6162
.join(circle ? "circle" : "path")

src/marks/frame.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {create} from "d3";
22
import {Mark} from "../plot.js";
33
import {number} from "../options.js";
4-
import {applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
4+
import {applyClip, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
55

66
const defaults = {
77
ariaLabel: "frame",
@@ -31,6 +31,7 @@ export class Frame extends Mark {
3131
.call(applyIndirectStyles, this)
3232
.call(applyDirectStyles, this)
3333
.call(applyTransform, null, null, offset + dx, offset + dy)
34+
.call(applyClip, this, dimensions)
3435
.attr("x", marginLeft + insetLeft)
3536
.attr("y", marginTop + insetTop)
3637
.attr("width", width - marginLeft - marginRight - insetLeft - insetRight)

src/marks/image.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {create} from "d3";
22
import {positive} from "../defined.js";
33
import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, string} from "../options.js";
44
import {Mark} from "../plot.js";
5-
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr, offset, impliedString, applyFrameAnchor} from "../style.js";
5+
import {applyClip, applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr, offset, impliedString, applyFrameAnchor} from "../style.js";
66

77
const defaults = {
88
ariaLabel: "image",
@@ -66,6 +66,7 @@ export class Image extends Mark {
6666
return create("svg:g")
6767
.call(applyIndirectStyles, this)
6868
.call(applyTransform, x, y, offset + dx, offset + dy)
69+
.call(applyClip, this, dimensions)
6970
.call(g => g.selectAll()
7071
.data(index)
7172
.join("image")

src/marks/line.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {Curve} from "../curve.js";
33
import {defined} from "../defined.js";
44
import {Mark} from "../plot.js";
55
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
6-
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js";
6+
import {applyClip, applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js";
77
import {applyGroupedMarkers, markers} from "./marker.js";
88

99
const defaults = {
@@ -30,12 +30,13 @@ export class Line extends Mark {
3030
this.curve = Curve(curve, tension);
3131
markers(this, options);
3232
}
33-
render(I, {x, y}, channels) {
33+
render(I, {x, y}, channels, dimensions) {
3434
const {x: X, y: Y, z: Z} = channels;
3535
const {dx, dy} = this;
3636
return create("svg:g")
3737
.call(applyIndirectStyles, this)
3838
.call(applyTransform, x, y, offset + dx, offset + dy)
39+
.call(applyClip, this, dimensions)
3940
.call(g => g.selectAll()
4041
.data(Z ? group(I, i => Z[i]).values() : [I])
4142
.join("path")

src/marks/link.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {create, path} from "d3";
22
import {Curve} from "../curve.js";
33
import {Mark} from "../plot.js";
4-
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
4+
import {applyClip, applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
55
import {markers, applyMarkers} from "./marker.js";
66

77
const defaults = {
@@ -28,12 +28,13 @@ export class Link extends Mark {
2828
this.curve = Curve(curve, tension);
2929
markers(this, options);
3030
}
31-
render(index, {x, y}, channels) {
31+
render(index, {x, y}, channels, dimensions) {
3232
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
3333
const {dx, dy, curve} = this;
3434
return create("svg:g")
3535
.call(applyIndirectStyles, this)
3636
.call(applyTransform, x, y, offset + dx, offset + dy)
37+
.call(applyClip, this, dimensions)
3738
.call(g => g.selectAll()
3839
.data(index)
3940
.join("path")

src/marks/rect.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {create} from "d3";
22
import {number} from "../options.js";
33
import {Mark} from "../plot.js";
44
import {isCollapsed} from "../scales.js";
5-
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
5+
import {applyClip, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
66
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
77
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
88
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
@@ -51,6 +51,7 @@ export class Rect extends Mark {
5151
return create("svg:g")
5252
.call(applyIndirectStyles, this)
5353
.call(applyTransform, x, y, dx, dy)
54+
.call(applyClip, this, dimensions)
5455
.call(g => g.selectAll()
5556
.data(index)
5657
.join("rect")

src/marks/rule.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {create} from "d3";
22
import {identity, number} from "../options.js";
33
import {Mark} from "../plot.js";
44
import {isCollapsed} from "../scales.js";
5-
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js";
5+
import {applyClip, applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js";
66
import {maybeIntervalX, maybeIntervalY} from "../transforms/interval.js";
77

88
const defaults = {
@@ -41,6 +41,7 @@ export class RuleX extends Mark {
4141
return create("svg:g")
4242
.call(applyIndirectStyles, this)
4343
.call(applyTransform, X && x, null, offset, 0)
44+
.call(applyClip, this, dimensions)
4445
.call(g => g.selectAll("line")
4546
.data(index)
4647
.join("line")

src/marks/text.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {nonempty} from "../defined.js";
33
import {formatNumber} from "../format.js";
44
import {indexOf, identity, string, maybeNumberChannel, maybeTuple, numberChannel, isNumeric, isTemporal, keyword, maybeFrameAnchor, isTextual} from "../options.js";
55
import {Mark} from "../plot.js";
6-
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset, impliedString, applyFrameAnchor} from "../style.js";
6+
import {applyClip, applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform, offset, impliedString, applyFrameAnchor} from "../style.js";
77

88
const defaults = {
99
ariaLabel: "text",
@@ -65,6 +65,7 @@ export class Text extends Mark {
6565
return create("svg:g")
6666
.call(applyIndirectTextStyles, this, T)
6767
.call(applyTransform, x, y, offset + dx, offset + dy)
68+
.call(applyClip, this, dimensions)
6869
.call(g => g.selectAll()
6970
.data(index)
7071
.join("text")

src/marks/tick.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {create} from "d3";
22
import {Mark} from "../plot.js";
33
import {identity, number} from "../options.js";
4-
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js";
4+
import {applyClip, applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles, offset} from "../style.js";
55

66
const defaults = {
77
ariaLabel: "tick",
@@ -18,6 +18,7 @@ class AbstractTick extends Mark {
1818
return create("svg:g")
1919
.call(applyIndirectStyles, this)
2020
.call(this._transform, scales, dx, dy)
21+
.call(applyClip, this, dimensions)
2122
.call(g => g.selectAll("line")
2223
.data(index)
2324
.join("line")

src/marks/vector.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {create} from "d3";
22
import {radians} from "../math.js";
33
import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, keyword} from "../options.js";
44
import {Mark} from "../plot.js";
5-
import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
5+
import {applyClip, applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
66

77
const defaults = {
88
ariaLabel: "vector",
@@ -46,6 +46,7 @@ export class Vector extends Mark {
4646
.attr("fill", "none")
4747
.call(applyIndirectStyles, this)
4848
.call(applyTransform, x, y, offset + dx, offset + dy)
49+
.call(applyClip, this, dimensions)
4950
.call(g => g.selectAll()
5051
.data(index)
5152
.join("path")

src/plot.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Dimensions} from "./dimensions.js";
66
import {Legends, exposeLegends} from "./legends.js";
77
import {arrayify, isOptions, keyword, range, first, second, where} from "./options.js";
88
import {Scales, ScaleFunctions, autoScaleRange, applyScales, exposeScales} from "./scales.js";
9-
import {applyInlineStyles, maybeClassName, styles} from "./style.js";
9+
import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js";
1010
import {basic} from "./transforms/basic.js";
1111

1212
export function plot(options = {}) {
@@ -134,7 +134,7 @@ function filter(index, channels, values) {
134134

135135
export class Mark {
136136
constructor(data, channels = [], options = {}, defaults) {
137-
const {facet = "auto", sort, dx, dy} = options;
137+
const {facet = "auto", sort, dx, dy, clip} = options;
138138
const names = new Set();
139139
this.data = data;
140140
this.sort = isOptions(sort) ? sort : null;
@@ -158,6 +158,7 @@ export class Mark {
158158
});
159159
this.dx = +dx || 0;
160160
this.dy = +dy || 0;
161+
this.clip = maybeClip(clip);
161162
}
162163
initialize(facets, facetChannels) {
163164
let data = arrayify(this.data);

src/style.js

+25
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNum
55

66
export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5;
77

8+
let nextClipPathId = 0;
9+
810
export function styles(
911
mark,
1012
{
@@ -172,6 +174,29 @@ export function applyGroupedChannelStyles(selection, {target}, {ariaLabel: AL, t
172174
applyTitleGroup(selection, T);
173175
}
174176

177+
// clip: true clips to the frame
178+
// TODO: accept other types of clips (paths, urls, x, y, other marks?…)
179+
// https://github.com/observablehq/plot/issues/181
180+
export function maybeClip(clip) {
181+
if (clip === true) return "frame";
182+
if (clip == null || clip === false) return false;
183+
throw new Error(`clip method not implemented: ${clip}`);
184+
}
185+
186+
export function applyClip(selection, mark, {width, height, marginLeft, marginRight, marginTop, marginBottom}) {
187+
if (mark.clip === "frame") {
188+
const id = `plot-clippath-${++nextClipPathId}`;
189+
const w = width - marginRight - marginLeft;
190+
const h = height - marginTop - marginBottom;
191+
selection.append("clipPath")
192+
.attr("id", id)
193+
.append("rect")
194+
.attr("width", w)
195+
.attr("height", h);
196+
applyAttr(selection, "clip-path", `url(#${id})`);
197+
}
198+
}
199+
175200
export function applyIndirectStyles(selection, mark) {
176201
applyAttr(selection, "aria-label", mark.ariaLabel);
177202
applyAttr(selection, "aria-description", mark.ariaDescription);

0 commit comments

Comments
 (0)