Skip to content

Commit fe36567

Browse files
committed
tooltip: true
1 parent 6bccc91 commit fe36567

File tree

6 files changed

+57
-17
lines changed

6 files changed

+57
-17
lines changed

src/channel.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import {registry} from "./scales/index.js";
55
import {isSymbol, maybeSymbol} from "./symbol.js";
66
import {maybeReduce} from "./transforms/group.js";
77

8-
export function createChannel(data, {scale, type, value, filter, hint}, name) {
9-
if (hint === undefined && typeof value?.transform === "function") hint = value.hint;
8+
export function createChannel(data, channel, name) {
9+
if (channel.alias) return channel;
10+
const {scale, type, value, filter, hint} = channel;
1011
return inferChannelScale(name, {
1112
scale,
1213
type,
1314
value: valueof(data, value),
1415
label: labelof(value),
1516
filter,
16-
hint
17+
hint: hint === undefined && typeof value?.transform === "function" ? value.hint : hint
1718
});
1819
}
1920

src/mark.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,9 @@ export interface MarkOptions {
438438
* by an **initializer** to declare extra channels.
439439
*/
440440
channels?: Channels;
441+
442+
/** TODO */
443+
tooltip?: boolean;
441444
}
442445

443446
/** The abstract base class for Mark implementations. */

src/mark.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export class Mark {
1414
fx,
1515
fy,
1616
sort,
17+
tooltip,
18+
tooltipAxis,
1719
dx = 0,
1820
dy = 0,
1921
margin = 0,
@@ -25,6 +27,8 @@ export class Mark {
2527
channels: extraChannels
2628
} = options;
2729
this.data = data;
30+
this.tooltip = !!tooltip;
31+
this.tooltipAxis = tooltipAxis; // TODO validate
2832
this.sort = isDomainSort(sort) ? sort : null;
2933
this.initializer = initializer(options).initializer;
3034
this.transform = this.initializer ? options.transform : basic(options).transform;
@@ -37,7 +41,7 @@ export class Mark {
3741
}
3842
this.facetAnchor = maybeFacetAnchor(facetAnchor);
3943
channels = maybeNamed(channels);
40-
if (extraChannels !== undefined) channels = {...maybeNamed(extraChannels), ...channels};
44+
if (extraChannels !== undefined) channels = {...channels, ...maybeNamed(extraChannels)};
4145
if (defaults !== undefined) channels = {...styles(this, options, defaults), ...channels};
4246
this.channels = Object.fromEntries(
4347
Object.entries(channels)
@@ -54,8 +58,8 @@ export class Mark {
5458
}
5559
return [name, channel];
5660
})
57-
.filter(([name, {value, optional}]) => {
58-
if (value != null) return true;
61+
.filter(([name, {alias, value, optional}]) => {
62+
if (value != null || alias) return true;
5963
if (optional) return false;
6064
throw new Error(`missing channel value: ${name}`);
6165
})

src/plot.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {createLegends, exposeLegends} from "./legends.js";
77
import {Mark} from "./mark.js";
88
import {axisFx, axisFy, axisX, axisY, gridFx, gridFy, gridX, gridY} from "./marks/axis.js";
99
import {frame} from "./marks/frame.js";
10+
import {tooltip} from "./marks/tooltip.js";
1011
import {arrayify, isColor, isIterable, isNone, isScaleOptions, map, yes, maybeIntervalTransform} from "./options.js";
1112
import {createScales, createScaleFunctions, autoScaleRange, exposeScales} from "./scales.js";
1213
import {innerDimensions, outerDimensions} from "./scales.js";
@@ -21,7 +22,7 @@ export function plot(options = {}) {
2122
const className = maybeClassName(options.className);
2223

2324
// Flatten any nested marks.
24-
const marks = options.marks === undefined ? [] : flatMarks(options.marks);
25+
const marks = options.marks === undefined ? [] : inferTooltip(flatMarks(options.marks));
2526

2627
// Compute the top-level facet state. This has roughly the same structure as
2728
// mark-specific facet state, except there isn’t a facetsIndex, and there’s a
@@ -127,6 +128,7 @@ export function plot(options = {}) {
127128
if (stateByMark.has(mark)) throw new Error("duplicate mark; each mark must be unique");
128129
const {facetsIndex, channels: facetChannels} = facetStateByMark.get(mark) ?? {};
129130
const {data, facets, channels} = mark.initialize(facetsIndex, facetChannels, options);
131+
resolveChannelAliases(channels, stateByMark);
130132
applyScaleTransforms(channels, options);
131133
stateByMark.set(mark, {data, facets, channels});
132134
}
@@ -338,6 +340,36 @@ function flatMarks(marks) {
338340
.map(markify);
339341
}
340342

343+
// Note: Mutates marks!
344+
function inferTooltip(marks) {
345+
for (const mark of marks) {
346+
if (mark.tooltip) {
347+
marks.push(tooltip(mark.data, tooltipOptions(mark)));
348+
break;
349+
}
350+
}
351+
return marks;
352+
}
353+
354+
function tooltipOptions(mark) {
355+
const {tooltipAxis: axis, facet, facetAnchor, fx, fy} = mark;
356+
return {axis, x: null, facet, facetAnchor, fx, fy, channels: tooltipChannels(mark)};
357+
}
358+
359+
function tooltipChannels(mark) {
360+
return Object.fromEntries(Object.keys(mark.channels).map((name) => [name, {alias: mark}]));
361+
}
362+
363+
// Note: mutates channels!
364+
function resolveChannelAliases(channels, stateByMark) {
365+
for (const name in channels) {
366+
const channel = channels[name];
367+
if (channel.alias) {
368+
channels[name] = stateByMark.get(channel.alias).channels[name];
369+
}
370+
}
371+
}
372+
341373
function markify(mark) {
342374
return typeof mark.render === "function" ? mark : new Render(mark);
343375
}

src/transforms/stack.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,54 +8,54 @@ export function stackX(stackOptions = {}, options = {}) {
88
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
99
const {y1, y = y1, x, ...rest} = options; // note: consumes x!
1010
const [transform, Y, x1, x2] = stack(y, x, "y", "x", stackOptions, rest);
11-
return {...transform, y1, y: Y, x1, x2, x: mid(x1, x2)};
11+
return {...transform, y1, y: Y, x1, x2, x: mid(x1, x2), tooltipAxis: "y"};
1212
}
1313

1414
export function stackX1(stackOptions = {}, options = {}) {
1515
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
1616
const {y1, y = y1, x} = options;
1717
const [transform, Y, X] = stack(y, x, "y", "x", stackOptions, options);
18-
return {...transform, y1, y: Y, x: X};
18+
return {...transform, y1, y: Y, x: X, tooltipAxis: "y"};
1919
}
2020

2121
export function stackX2(stackOptions = {}, options = {}) {
2222
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
2323
const {y1, y = y1, x} = options;
2424
const [transform, Y, , X] = stack(y, x, "y", "x", stackOptions, options);
25-
return {...transform, y1, y: Y, x: X};
25+
return {...transform, y1, y: Y, x: X, tooltipAxis: "y"};
2626
}
2727

2828
export function stackY(stackOptions = {}, options = {}) {
2929
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
3030
const {x1, x = x1, y, ...rest} = options; // note: consumes y!
3131
const [transform, X, y1, y2] = stack(x, y, "x", "y", stackOptions, rest);
32-
return {...transform, x1, x: X, y1, y2, y: mid(y1, y2)};
32+
return {...transform, x1, x: X, y1, y2, y: mid(y1, y2), tooltipAxis: "x"};
3333
}
3434

3535
export function stackY1(stackOptions = {}, options = {}) {
3636
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
3737
const {x1, x = x1, y} = options;
3838
const [transform, X, Y] = stack(x, y, "x", "y", stackOptions, options);
39-
return {...transform, x1, x: X, y: Y};
39+
return {...transform, x1, x: X, y: Y, tooltipAxis: "x"};
4040
}
4141

4242
export function stackY2(stackOptions = {}, options = {}) {
4343
if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions);
4444
const {x1, x = x1, y} = options;
4545
const [transform, X, , Y] = stack(x, y, "x", "y", stackOptions, options);
46-
return {...transform, x1, x: X, y: Y};
46+
return {...transform, x1, x: X, y: Y, tooltipAxis: "x"};
4747
}
4848

4949
export function maybeStackX({x, x1, x2, ...options} = {}) {
5050
if (x1 === undefined && x2 === undefined) return stackX({x, ...options});
5151
[x1, x2] = maybeZero(x, x1, x2);
52-
return {...options, x1, x2};
52+
return {...options, x1, x2, tooltipAxis: "x"};
5353
}
5454

5555
export function maybeStackY({y, y1, y2, ...options} = {}) {
5656
if (y1 === undefined && y2 === undefined) return stackY({y, ...options});
5757
[y1, y2] = maybeZero(y, y1, y2);
58-
return {...options, y1, y2};
58+
return {...options, y1, y2, tooltipAxis: "y"};
5959
}
6060

6161
// The reverse option is ambiguous: it is both a stack option and a basic

test/plots/tooltip.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ export async function tooltipBin() {
55
const olympians = await d3.csv<any>("data/athletes.csv", d3.autoType);
66
return Plot.plot({
77
marks: [
8-
Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight"})),
9-
Plot.tooltip(olympians, Plot.binX({y: "count"}, {x: "weight", axis: "x"}))
8+
Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight", tooltip: true})),
9+
// Plot.tooltip(olympians, Plot.binX({y: "count"}, {x: "weight", axis: "x"}))
1010
]
1111
});
1212
}

0 commit comments

Comments
 (0)