Skip to content

Commit

Permalink
feat: anchor tooltip to mark (#959)
Browse files Browse the repository at this point in the history
* feat: enable positioning the tooltip relative to the hovered mark

* refactor: clean up

* test: add coverage for calculatePositionRelativeToCursor()

* feat: added x and y offset for tooltips relative to hovered marks

* chore: made diagonal positioning consistent for relative mark tooltips

* test: add test coverage

* chore: clean up

* chore: clean up

* trigger build

* build: remove unnecessary script

* chore: simplify statement

* refactor: use utility methods to validate tooltip position

* fix: remove commented code

* refactor: simplify arguments to calculatePositionRelativeToCursor()

* test: add tests for position utilities

* feat: add options controls to example pages

* fix: incorrect form value

* refactor: simplify code

---------

Co-authored-by: Marshall Peterson <mpeterson@adobe.com>
Co-authored-by: Connor Lamoureux <connorl@adobe.com>
  • Loading branch information
3 people authored Nov 5, 2024
1 parent 2a97e8e commit 5a95192
Show file tree
Hide file tree
Showing 7 changed files with 424 additions and 54 deletions.
4 changes: 4 additions & 0 deletions examples/examples.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ body {
top: 1em;
right: 1em;
}

.options {
margin-top: 24px;
}
32 changes: 25 additions & 7 deletions examples/vega-examples.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>Tooltip Examples</title>
Expand All @@ -16,6 +16,13 @@
<body>
<h1>Vega Tooltip Examples</h1>
<a href="index.html" class="large">Back to overview</a>
<div class="options">
<div>
<span>anchor:</span>
<label> <input type="radio" name="anchor" value="cursor" id="anchor-cursor" checked /> cursor </label>
<label> <input type="radio" name="anchor" value="mark" id="anchor-mark" /> mark </label>
</div>
</div>
<div>
<div id="vis-images" class="tooltip-example"></div>
<div id="vis-arc" class="tooltip-example"></div>
Expand All @@ -35,12 +42,23 @@ <h1>Vega Tooltip Examples</h1>
await vegaEmbed(id, vgSpec, { tooltip: handler.call }).catch(console.error);
}

addVgExample("specs/images.json", "#vis-images");
addVgExample("specs/arc.json", "#vis-arc");
addVgExample("specs/choropleth.json", "#vis-choropleth");
addVgExample("specs/force.json", "#vis-force", { theme: "dark" });
addVgExample("specs/heatmap.json", "#vis-heatmap");
addVgExample("specs/voronoi.json", "#vis-voronoi");
function renderCharts() {
const anchor = document.getElementById("anchor-cursor").checked ? "cursor" : "mark";

addVgExample("specs/images.json", "#vis-images", { anchor });
addVgExample("specs/arc.json", "#vis-arc", { anchor });
addVgExample("specs/choropleth.json", "#vis-choropleth", { anchor });
addVgExample("specs/force.json", "#vis-force", { anchor, theme: "dark" });
addVgExample("specs/heatmap.json", "#vis-heatmap", { anchor });
addVgExample("specs/voronoi.json", "#vis-voronoi", { anchor });
}

// Initial render
renderCharts();

// Re-render charts when the checkbox is clicked
document.getElementById("anchor-cursor").addEventListener("change", renderCharts);
document.getElementById("anchor-mark").addEventListener("change", renderCharts);
</script>
</body>
</html>
47 changes: 33 additions & 14 deletions examples/vega-lite-examples.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>Tooltip Examples</title>
Expand All @@ -17,6 +17,13 @@
<body>
<h1>Vega-Lite Tooltip Examples</h1>
<a href="index.html" class="large">Back to overview</a>
<div class="options">
<div>
<span>anchor:</span>
<label> <input type="radio" name="anchor" value="cursor" id="anchor-cursor" checked /> cursor </label>
<label> <input type="radio" name="anchor" value="mark" id="anchor-mark" /> mark </label>
</div>
</div>
<div>
<div id="vis-histogram" class="tooltip-example"></div>
<div id="vis-scatter" class="tooltip-example"></div>
Expand Down Expand Up @@ -50,19 +57,31 @@ <h1>Vega-Lite Tooltip Examples</h1>
});
}

addVlExample("specs/histogram.json", "#vis-histogram");
addVlExample("specs/scatter.json", "#vis-scatter");
addVlExample("specs/bubble_multiple_aggregation.json", "#vis-bubble-multi-aggr");
addVlExample("specs/trellis_barley.json", "#vis-trellis-barley");
addVlExample("specs/scatter_binned.json", "#vis-scatter-binned");
addVlExample("specs/bar.json", "#vis-bar");
addVlExample("specs/stacked_bar_weather.json", "#vis-stacked-bar");
addVlExample("specs/bar_layered_transparent.json", "#vis-layered-bar");
addVlExample("specs/line_color.json", "#vis-color-line");
addVlExample("specs/overlay_area_short.json", "#vis-overlay-area");
addVlExample("specs/bar_format_tooltip.json", "#vis-bar-format-tooltip", {
formatTooltip: (value, sanitize) => `The value of ${sanitize(value.a)} is ${sanitize(value.b)}`
});
function renderCharts() {
const anchor = document.getElementById("anchor-cursor").checked ? "cursor" : "mark";

addVlExample("specs/histogram.json", "#vis-histogram", { anchor });
addVlExample("specs/scatter.json", "#vis-scatter", { anchor });
addVlExample("specs/bubble_multiple_aggregation.json", "#vis-bubble-multi-aggr", { anchor });
addVlExample("specs/trellis_barley.json", "#vis-trellis-barley", { anchor });
addVlExample("specs/scatter_binned.json", "#vis-scatter-binned", { anchor });
addVlExample("specs/bar.json", "#vis-bar", { anchor });
addVlExample("specs/stacked_bar_weather.json", "#vis-stacked-bar", { anchor });
addVlExample("specs/bar_layered_transparent.json", "#vis-layered-bar", { anchor });
addVlExample("specs/line_color.json", "#vis-color-line", { anchor });
addVlExample("specs/overlay_area_short.json", "#vis-overlay-area", { anchor });
addVlExample("specs/bar_format_tooltip.json", "#vis-bar-format-tooltip", {
anchor,
formatTooltip: (value, sanitize) => `The value of ${sanitize(value.a)} is ${sanitize(value.b)}`,
});
}

// Initial render
renderCharts();

// Re-render charts when the checkbox is clicked
document.getElementById("anchor-cursor").addEventListener("change", renderCharts);
document.getElementById("anchor-mark").addEventListener("change", renderCharts);
</script>
</body>
</html>
14 changes: 5 additions & 9 deletions src/Handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {TooltipHandler} from 'vega-typings';

import {createDefaultStyle, DEFAULT_OPTIONS, Options} from './defaults';
import {calculatePosition} from './position';
import {calculatePositionRelativeToCursor, calculatePositionRelativeToMark} from './position';

/**
* The tooltip handler class.
Expand Down Expand Up @@ -54,8 +54,6 @@ export class Handler {
* The tooltip handler function.
*/
private tooltipHandler(handler: any, event: MouseEvent, item: any, value: any) {

Check warning on line 56 in src/Handler.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type

Check warning on line 56 in src/Handler.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type

Check warning on line 56 in src/Handler.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type
// console.log(handler, event, item, value);

// append a div element that we use as a tooltip unless it already exists
this.el = document.getElementById(this.options.id);
if (!this.el) {
Expand Down Expand Up @@ -84,12 +82,10 @@ export class Handler {
// make the tooltip visible
this.el.classList.add('visible', `${this.options.theme}-theme`);

const {x, y} = calculatePosition(
event,
this.el.getBoundingClientRect(),
this.options.offsetX,
this.options.offsetY,
);
const {x, y} =
this.options.anchor === 'mark'
? calculatePositionRelativeToMark(handler, event, item, this.el.getBoundingClientRect(), this.options)
: calculatePositionRelativeToCursor(event, this.el.getBoundingClientRect(), this.options);

this.el.style.top = `${y}px`;
this.el.style.left = `${x}px`;
Expand Down
51 changes: 39 additions & 12 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,52 @@ import defaultStyle from './style';

const EL_ID = 'vg-tooltip-element';

export const DEFAULT_OPTIONS = {
export type Position = 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

export interface Options {
/**
* X offset.
*/
offsetX: 10,
offsetX?: number;

/**
* Y offset.
*/
offsetY: 10,
offsetY?: number;

/**
* ID of the tooltip element.
*/
id: EL_ID,
id?: string;

/**
* ID of the tooltip CSS style.
*/
styleId: 'vega-tooltip-style',
styleId?: string;

/**
* The name of the theme. You can use the CSS class called [THEME]-theme to style the tooltips.
*
* There are two predefined themes: "light" (default) and "dark".
*/
theme: 'light',
theme?: string;

/**
* Do not use the default styles provided by Vega Tooltip. If you enable this option, you need to use your own styles. It is not necessary to disable the default style when using a custom theme.
*/
disableDefaultStyle: false,
disableDefaultStyle?: boolean;

/**
* HTML sanitizer function that removes dangerous HTML to prevent XSS.
*
* This should be a function from string to string. You may replace it with a formatter such as a markdown formatter.
*/
sanitize: escapeHTML,
sanitize?: (value: any) => string;

Check warning on line 46 in src/defaults.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type

/**
* The maximum recursion depth when printing objects in the tooltip.
*/
maxDepth: 2,
maxDepth?: number;

/**
* A function to customize the rendered HTML of the tooltip.
Expand All @@ -55,16 +57,41 @@ export const DEFAULT_OPTIONS = {
* @param baseURL The `baseURL` from `options.baseURL`
* @returns {string} The returned string will become the `innerHTML` of the tooltip element
*/
formatTooltip: formatValue,
formatTooltip?: (value: any, valueToHtml: (value: any) => string, maxDepth: number, baseURL: string) => string;

Check warning on line 60 in src/defaults.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type

Check warning on line 60 in src/defaults.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected any. Specify a different type

/**
* The baseurl to use in image paths.
*/
baseURL?: string;

/**
* The snap reference for the tooltip.
*/
anchor?: 'cursor' | 'mark';

/**
* The position of the tooltip relative to the anchor.
*
* Only valid when `anchor` is set to 'mark'.
*/
position?: Position | Position[];
}

export const DEFAULT_OPTIONS: Required<Options> = {
offsetX: 10,
offsetY: 10,
id: EL_ID,
styleId: 'vega-tooltip-style',
theme: 'light',
disableDefaultStyle: false,
sanitize: escapeHTML,
maxDepth: 2,
formatTooltip: formatValue,
baseURL: '',
anchor: 'cursor',
position: ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
};

export type Options = Partial<typeof DEFAULT_OPTIONS>;

/**
* Escape special HTML characters.
*
Expand Down
Loading

0 comments on commit 5a95192

Please sign in to comment.