Skip to content

Commit 362d28d

Browse files
mbostockFil
andauthored
vector frameAnchor (#697)
* vector frameAnchor * test Co-authored-by: Philippe Rivière <[email protected]>
1 parent 0343ca9 commit 362d28d

File tree

5 files changed

+107
-12
lines changed

5 files changed

+107
-12
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,9 +1188,12 @@ In addition to the [standard mark options](#marks), the following optional chann
11881188
* **length** - the length in pixels; bound to the *length* scale; defaults to 12
11891189
* **rotate** - the rotation angle in degrees clockwise; defaults to 0
11901190

1191+
If either of the **x** or **y** channels are not specified, the corresponding position is controlled by the **frameAnchor** option.
1192+
11911193
The following options are also supported:
11921194

11931195
* **anchor** - one of *start*, *middle*, or *end*; defaults to *middle*
1196+
* **frameAnchor** - the frame anchor; top-left, top, top-right, right, bottom-right, bottom, bottom-left, left, or middle (default)
11941197

11951198
If the **anchor** is *start*, the arrow will start at the given *xy* position and point in the direction given by the rotation angle. If the **anchor** is *end*, the arrow will maintain the same orientation, but be positioned such that it ends in the given *xy* position. If the **anchor** is *middle*, the arrow will be likewise be positioned such that its midpoint intersects the given *xy* position.
11961199

src/marks/vector.js

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {create} from "d3";
22
import {radians} from "../math.js";
3-
import {maybeNumberChannel, maybeTuple, keyword} from "../options.js";
3+
import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, keyword} from "../options.js";
44
import {Mark} from "../plot.js";
5-
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
5+
import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
66

77
const defaults = {
88
fill: null,
@@ -13,7 +13,7 @@ const defaults = {
1313

1414
export class Vector extends Mark {
1515
constructor(data, options = {}) {
16-
const {x, y, length, rotate, anchor = "middle"} = options;
16+
const {x, y, length, rotate, anchor = "middle", frameAnchor} = options;
1717
const [vl, cl] = maybeNumberChannel(length, 12);
1818
const [vr, cr] = maybeNumberChannel(rotate, 0);
1919
super(
@@ -30,19 +30,16 @@ export class Vector extends Mark {
3030
this.length = cl;
3131
this.rotate = cr;
3232
this.anchor = keyword(anchor, "anchor", ["start", "middle", "end"]);
33+
this.frameAnchor = maybeFrameAnchor(frameAnchor);
3334
}
34-
render(
35-
index,
36-
{x, y},
37-
channels,
38-
{width, height, marginTop, marginRight, marginBottom, marginLeft}
39-
) {
35+
render(index, {x, y}, channels, dimensions) {
4036
const {x: X, y: Y, length: L, rotate: R} = channels;
4137
const {dx, dy, length, rotate, anchor} = this;
38+
const [cx, cy] = applyFrameAnchor(this, dimensions);
4239
const fl = L ? i => L[i] : () => length;
4340
const fr = R ? i => R[i] : () => rotate;
44-
const fx = X ? i => X[i] : () => (marginLeft + width - marginRight) / 2;
45-
const fy = Y ? i => Y[i] : () => (marginTop + height - marginBottom) / 2;
41+
const fx = X ? i => X[i] : () => cx;
42+
const fy = Y ? i => Y[i] : () => cy;
4643
const k = anchor === "start" ? 0 : anchor === "end" ? 1 : 0.5;
4744
return create("svg:g")
4845
.attr("fill", "none")
@@ -64,6 +61,6 @@ export class Vector extends Mark {
6461
}
6562

6663
export function vector(data, {x, y, ...options} = {}) {
67-
([x, y] = maybeTuple(x, y));
64+
if (options.frameAnchor === undefined) ([x, y] = maybeTuple(x, y));
6865
return new Vector(data, {...options, x, y});
6966
}

test/output/vectorFrame.svg

Lines changed: 30 additions & 0 deletions
Loading

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export {default as usPresidentialForecast2016} from "./us-presidential-forecast-
138138
export {default as usRetailSales} from "./us-retail-sales.js";
139139
export {default as usStatePopulationChange} from "./us-state-population-change.js";
140140
export {default as vectorField} from "./vector-field.js";
141+
export {default as vectorFrame} from "./vector-frame.js";
141142
export {default as wealthBritainBar} from "./wealth-britain-bar.js";
142143
export {default as wealthBritainProportionPlot} from "./wealth-britain-proportion-plot.js";
143144
export {default as wordCloud} from "./word-cloud.js";

test/plots/vector-frame.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as Plot from "@observablehq/plot";
2+
3+
export default function() {
4+
return Plot.plot({
5+
inset: 12,
6+
width: 200,
7+
height: 200,
8+
marks: [
9+
Plot.frame(),
10+
Plot.vector(
11+
[null],
12+
{
13+
length: 100,
14+
rotate: -135,
15+
anchor: "start",
16+
frameAnchor: "top-right",
17+
dx: -10,
18+
dy: 10
19+
}
20+
),
21+
Plot.vector(
22+
[null],
23+
{
24+
length: 100,
25+
rotate: 45,
26+
anchor: "start",
27+
frameAnchor: "bottom-left",
28+
dx: 10,
29+
dy: -10
30+
}
31+
),
32+
Plot.vector(
33+
[null],
34+
{
35+
length: 100,
36+
rotate: 135,
37+
anchor: "start",
38+
frameAnchor: "top-left",
39+
dx: 10,
40+
dy: 10
41+
}
42+
),
43+
Plot.vector(
44+
[null],
45+
{
46+
length: 100,
47+
rotate: -45,
48+
anchor: "start",
49+
frameAnchor: "bottom-right",
50+
dx: -10,
51+
dy: -10
52+
}
53+
),
54+
Plot.text(
55+
[null],
56+
{
57+
x: null,
58+
y: null,
59+
text: () => "Ο"
60+
}
61+
)
62+
]
63+
});
64+
}

0 commit comments

Comments
 (0)