Skip to content

Commit c18a6eb

Browse files
committed
render transform!
1 parent e44cf42 commit c18a6eb

File tree

6 files changed

+60
-53
lines changed

6 files changed

+60
-53
lines changed

src/mark.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export interface MarkOptions {
126126
/** A custom mark initializer. */
127127
initializer?: InitializerFunction;
128128

129+
/** A custom render transform. */
130+
render?: RenderFunction;
131+
129132
/**
130133
* The horizontal facet position channel, for mark-level faceting, bound to
131134
* the *fx* scale.

src/mark.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export class Mark {
2222
marginBottom = margin,
2323
marginLeft = margin,
2424
clip,
25-
channels: extraChannels
25+
channels: extraChannels,
26+
render
2627
} = options;
2728
this.data = data;
2829
this.sort = isDomainSort(sort) ? sort : null;
@@ -78,6 +79,11 @@ export class Mark {
7879
throw new Error(`super-faceting cannot use x or y`);
7980
}
8081
}
82+
if (render != null) {
83+
if (typeof render !== "function") throw new TypeError(`invalid render transform: ${render}`);
84+
this._render = this.render;
85+
this.render = render;
86+
}
8187
}
8288
initialize(facets, facetChannels, plotOptions) {
8389
let data = arrayify(this.data);

src/marks/tip.js

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -129,35 +129,36 @@ export class Tip extends Mark {
129129
)
130130
);
131131

132+
function postrender() {
133+
const {width, height} = context.ownerSVGElement.getBBox();
134+
g.selectChildren().each(function (i) {
135+
const x = X ? X[i] : cx;
136+
const y = Y ? Y[i] : cy;
137+
const {width: w, height: h} = this.getBBox();
138+
let c;
139+
if (anchor === undefined) {
140+
const fitLeft = x + w + r * 2 < width;
141+
const fitRight = x - w - r * 2 > 0;
142+
const fitTop = y + h + m + r * 2 + 7 < height;
143+
const fitBottom = y - h - m - r * 2 > 0;
144+
const cx = (/-left$/.test(c) ? fitLeft || !fitRight : fitLeft && !fitRight) ? "left" : "right";
145+
const cy = (/^top-/.test(c) ? fitTop || !fitBottom : fitTop && !fitBottom) ? "top" : "bottom";
146+
c = `${cy}-${cx}`;
147+
}
148+
const path = this.firstChild;
149+
const text = this.lastChild;
150+
path.setAttribute("d", getPath(c, m, r, w, h));
151+
text.setAttribute("y", `${+getLineOffset(c, text.childNodes.length, lineHeight).toFixed(6)}em`);
152+
text.setAttribute("transform", getTextTransform(c, m, r, w, h));
153+
});
154+
}
155+
132156
// Wait until the Plot is inserted into the page, so that we can use getBBox
133157
// to compute the text dimensions. Perhaps this could be done synchronously;
134158
// getting the dimensions of the SVG is easy, and although accurate text
135159
// metrics are hard, we could use approximate heuristics.
136-
if (typeof requestAnimationFrame !== "undefined") {
137-
requestAnimationFrame(() => {
138-
const {width, height} = g.node().ownerSVGElement.getBBox();
139-
g.selectChildren().each(function (i) {
140-
const x = X ? X[i] : cx;
141-
const y = Y ? Y[i] : cy;
142-
const {width: w, height: h} = this.getBBox();
143-
let c;
144-
if (anchor === undefined) {
145-
const fitLeft = x + w + r * 2 < width;
146-
const fitRight = x - w - r * 2 > 0;
147-
const fitTop = y + h + m + r * 2 + 7 < height;
148-
const fitBottom = y - h - m - r * 2 > 0;
149-
const cx = (/-left$/.test(c) ? fitLeft || !fitRight : fitLeft && !fitRight) ? "left" : "right";
150-
const cy = (/^top-/.test(c) ? fitTop || !fitBottom : fitTop && !fitBottom) ? "top" : "bottom";
151-
c = `${cy}-${cx}`;
152-
}
153-
const path = this.firstChild;
154-
const text = this.lastChild;
155-
path.setAttribute("d", getPath(c, m, r, w, h));
156-
text.setAttribute("y", `${+getLineOffset(c, text.childNodes.length, lineHeight).toFixed(6)}em`);
157-
text.setAttribute("transform", getTextTransform(c, m, r, w, h));
158-
});
159-
});
160-
}
160+
if (context.ownerSVGElement.isConnected) Promise.resolve().then(postrender);
161+
else if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(postrender);
161162

162163
return g.node();
163164
}

src/plot.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ export function plot(options = {}) {
234234
.call(applyInlineStyles, style)
235235
.node();
236236

237+
// TODO Cleaner.
238+
context.ownerSVGElement = svg;
239+
237240
// Render facets.
238241
if (facets !== undefined) {
239242
const facetDomains = {x: fx?.domain(), y: fy?.domain()};

test/output/tipDot.svg

Lines changed: 1 addition & 13 deletions
Loading

test/plots/tip.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,27 @@ import * as d3 from "d3";
44
export async function tipDot() {
55
const penguins = await d3.csv<any>("data/penguins.csv", d3.autoType);
66
return Plot.plot({
7-
style: "overflow: visible;",
87
marks: [
9-
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"}),
10-
Plot.tip(
11-
penguins,
12-
Plot.select(
13-
(I) => [
14-
d3.least(I, (i) => penguins[i].culmen_length_mm),
15-
d3.greatest(I, (i) => penguins[i].culmen_length_mm),
16-
d3.least(I, (i) => penguins[i].culmen_depth_mm),
17-
d3.greatest(I, (i) => penguins[i].culmen_depth_mm)
18-
],
19-
{x: "culmen_length_mm", y: "culmen_depth_mm"}
20-
)
21-
)
8+
Plot.dot(penguins, {
9+
x: "culmen_length_mm",
10+
y: "culmen_depth_mm"
11+
}),
12+
Plot.tip(penguins, {
13+
x: "culmen_length_mm",
14+
y: "culmen_depth_mm",
15+
render(index, ...args) {
16+
const mark = this;
17+
let i = 0;
18+
index = d3.sort(index, (i) => penguins[i].culmen_length_mm);
19+
let g = mark._render([index[i]], ...args);
20+
setTimeout(function tick() {
21+
if (!g.isConnected) return;
22+
g.replaceWith((g = mark._render([index[(i = (i + 1) % index.length)]], ...args)));
23+
setTimeout(tick, 100);
24+
}, 100);
25+
return g;
26+
}
27+
})
2228
]
2329
});
2430
}

0 commit comments

Comments
 (0)