Skip to content

Commit 7e95fc1

Browse files
committed
Merge branch 'fil/dataless-crosshair' into fil/brush-merge
2 parents 2196c26 + baf1e48 commit 7e95fc1

File tree

9 files changed

+1357
-10
lines changed

9 files changed

+1357
-10
lines changed

docs/features/interactions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ Plot.plot({
5353

5454
These values are displayed atop the axes on the edge of the frame; unlike the tip mark, the crosshair mark will not obscure other marks in the plot.
5555

56+
When called without data, the crosshair tracks the raw pointer position and inverts the plot's scales directly. This is useful for reading coordinates from any plot, even without matching data.
57+
58+
:::plot defer
59+
```js
60+
Plot.plot({
61+
x: {type: "utc", domain: [new Date("2010-01-01"), new Date("2025-01-01")]},
62+
y: {domain: [0, 100]},
63+
marks: [Plot.frame(), Plot.gridX(), Plot.gridY(), Plot.crosshair()]
64+
})
65+
```
66+
:::
67+
5668
## Selecting
5769

5870
The [brush mark](../interactions/brush.md) lets the reader select a rectangular region by clicking and dragging. The selected region is then exposed as the plot’s `value` and can be used to filter data. Optionally, when combined with reactive marks — **inactive**, **context**, and **focus** — the brush highlights the selected data while dimming the rest.

docs/interactions/crosshair.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import penguins from "../data/penguins.ts";
1212

1313
The **crosshair mark** shows the *x* (horizontal↔︎ position) and *y* (vertical↕︎ position) value of the point closest to the [pointer](./pointer.md) on the bottom and left sides of the frame, respectively.
1414

15+
<!-- TODO: add .move() to data-based crosshair, then use it here -->
1516
:::plot defer https://observablehq.com/@observablehq/plot-crosshair
1617
```js
1718
Plot.plot({
@@ -64,6 +65,46 @@ Plot.plot({
6465

6566
The crosshair mark does not currently support any format options; values are displayed with the default format. If you are interested in this feature, please upvote [#1596](https://github.com/observablehq/plot/issues/1596). In the meantime, you can implement a custom crosshair using the [pointer transform](./pointer.md) and a [text mark](../marks/text.md).
6667

68+
## Dataless crosshair
69+
70+
When called without data, the crosshair tracks the raw pointer position and inverts the plot's scales. This is useful when you want a crosshair on any plot — even one without data that matches the crosshair's position channels — or when you want to read coordinates directly from the scales.
71+
72+
:::plot defer
73+
```js
74+
Plot.plot({
75+
x: {type: "utc", domain: [new Date("2010-01-01"), new Date("2025-01-01")]},
76+
y: {domain: [0, 100]},
77+
marks: [
78+
Plot.frame(),
79+
Plot.gridX(),
80+
Plot.gridY(),
81+
Plot.crosshair()
82+
]
83+
})
84+
```
85+
:::
86+
87+
In the future, displayed values will be automatically rounded to the coarsest precision that still distinguishes neighboring pixels.
88+
89+
## Input events
90+
91+
The crosshair dispatches [*input* events](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event) when the pointer moves. The plot's value (`plot.value`) is set to an object with **x** and **y** properties (the scale-inverted values), or null when the pointer leaves the frame.
92+
93+
```js
94+
const ch = Plot.crosshair();
95+
const plot = ch.plot({
96+
x: {domain: [0, 100]},
97+
y: {domain: [0, 100]},
98+
marks: [Plot.frame()]
99+
});
100+
101+
plot.addEventListener("input", () => {
102+
console.log(plot.value); // {x: 42, y: 73} or null
103+
});
104+
```
105+
106+
For faceted plots, the value also includes **fx** and **fy** when applicable.
107+
67108
## Crosshair options
68109

69110
The following options are supported:
@@ -92,18 +133,44 @@ Plot.crosshair(cars, {x: "economy (mpg)", y: "cylinders"})
92133

93134
Returns a new crosshair for the given *data* and *options*, drawing horizontal and vertical rules. The corresponding **x** and **y** values are also drawn just outside the bottom and left sides of the frame, respectively, typically on top of the axes. If either **x** or **y** is not specified, the crosshair will be one-dimensional.
94135

136+
## crosshair(*options*) {#crosshair-dataless}
137+
138+
```js
139+
Plot.crosshair()
140+
```
141+
142+
When called without data, returns a dataless crosshair that tracks the raw pointer position and inverts the plot's scales. The returned mark has a [move](#crosshair-move) method for programmatic control.
143+
95144
## crosshairX(*data*, *options*) {#crosshairX}
96145

97146
```js
98147
Plot.crosshairX(aapl, {x: "Date", y: "Close"})
99148
```
100149

101-
Like crosshair, but using [pointerX](./pointer.md#pointerX) when *x* is the dominant dimension, like time in a time-series chart.
150+
Like crosshair, but using [pointerX](./pointer.md#pointerX) when *x* is the dominant dimension, like time in a time-series chart. When called without data, returns a dataless crosshair restricted to the *x* dimension.
102151

103152
## crosshairY(*data*, *options*) {#crosshairY}
104153

105154
```js
106155
Plot.crosshairY(aapl, {x: "Date", y: "Close"})
107156
```
108157

109-
Like crosshair, but using [pointerY](./pointer.md#pointerY) when *y* is the dominant dimension.
158+
Like crosshair, but using [pointerY](./pointer.md#pointerY) when *y* is the dominant dimension. When called without data, returns a dataless crosshair restricted to the *y* dimension.
159+
160+
## *crosshair*.move(*value*) {#crosshair-move}
161+
162+
```js
163+
ch.move({x: new Date("2020-06-01"), y: 42})
164+
```
165+
166+
Programmatically sets the crosshair position in data space. Pass an object with **x** and/or **y** values to show the crosshair at that position, or null to hide it. The plot dispatches an *input* event, just as if the user had moved the pointer.
167+
168+
```js
169+
ch.move(null) // hide
170+
```
171+
172+
For faceted plots, include **fx** or **fy** to target a specific facet:
173+
174+
```js
175+
ch.move({x: 45, y: 17, fx: "Chinstrap"})
176+
```

src/marks/crosshair.d.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {ChannelValueSpec} from "../channel.js";
2-
import type {CompoundMark, Data, MarkOptions} from "../mark.js";
2+
import type {CompoundMark, Data, MarkOptions, RenderableMark} from "../mark.js";
33

44
/** Options for the crosshair mark. */
55
export interface CrosshairOptions extends MarkOptions {
@@ -54,15 +54,34 @@ export interface CrosshairOptions extends MarkOptions {
5454
textStrokeWidth?: MarkOptions["strokeWidth"];
5555
}
5656

57+
/**
58+
* A dataless crosshair mark that tracks the pointer position and displays
59+
* scale-inverted values. Unlike the data-driven crosshair, this does not
60+
* require data — it works directly with the plot's scales.
61+
*/
62+
export class Crosshair extends RenderableMark {
63+
/**
64+
* Programmatically sets the crosshair position in data space. Pass an object
65+
* with **x** and/or **y** values (and **fx**, **fy** for faceted plots) to
66+
* show the crosshair, or null to hide it.
67+
*/
68+
move(value: {x?: any; y?: any; fx?: any; fy?: any} | null): void;
69+
}
70+
5771
/**
5872
* Returns a new crosshair mark for the given *data* and *options*, drawing
5973
* horizontal and vertical rules centered at the point closest to the pointer.
6074
* The corresponding **x** and **y** values are also drawn just outside the
6175
* bottom and left sides of the frame, respectively, typically on top of the
6276
* axes. If either **x** or **y** is not specified, the crosshair will be
6377
* one-dimensional.
78+
*
79+
* If called without data, returns a dataless crosshair that tracks the raw
80+
* pointer position, inverting the scales with pixel-level precision. The
81+
* returned mark has a **.move**() method for programmatic control.
6482
*/
65-
export function crosshair(data?: Data, options?: CrosshairOptions): CompoundMark;
83+
export function crosshair(data: Data, options?: CrosshairOptions): CompoundMark;
84+
export function crosshair(options?: CrosshairOptions): Crosshair;
6685

6786
/**
6887
* Like crosshair, but uses the pointerX transform: the determination of the
@@ -71,7 +90,8 @@ export function crosshair(data?: Data, options?: CrosshairOptions): CompoundMark
7190
* as time in a time-series chart, or the aggregated dimension when grouping or
7291
* binning.
7392
*/
74-
export function crosshairX(data?: Data, options?: CrosshairOptions): CompoundMark;
93+
export function crosshairX(data: Data, options?: CrosshairOptions): CompoundMark;
94+
export function crosshairX(options?: CrosshairOptions): Crosshair;
7595

7696
/**
7797
* Like crosshair, but uses the pointerY transform: the determination of the
@@ -80,4 +100,5 @@ export function crosshairX(data?: Data, options?: CrosshairOptions): CompoundMar
80100
* as time in a time-series chart, or the aggregated dimension when grouping or
81101
* binning.
82102
*/
83-
export function crosshairY(data?: Data, options?: CrosshairOptions): CompoundMark;
103+
export function crosshairY(data: Data, options?: CrosshairOptions): CompoundMark;
104+
export function crosshairY(options?: CrosshairOptions): Crosshair;

0 commit comments

Comments
 (0)