Skip to content

Commit

Permalink
custom tip format (observablehq#1823)
Browse files Browse the repository at this point in the history
* custom tip format

* format order; materialize defaults; fix facets

* revert data values, for now

* fix default tip pointer

* fix paired channels

* fix paired channel order

* fix crash with inferred tick format

* pReTTier

* oops, time zones!
  • Loading branch information
mbostock authored and chaichontat committed Jan 14, 2024
1 parent 900412d commit a5ef931
Show file tree
Hide file tree
Showing 36 changed files with 1,395 additions and 79 deletions.
2 changes: 1 addition & 1 deletion src/channel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export type ChannelValue =
* object to override the scale that would normally be associated with the
* channel.
*/
export type ChannelValueSpec = ChannelValue | {value: ChannelValue; scale?: Channel["scale"]}; // TODO label
export type ChannelValueSpec = ChannelValue | {value: ChannelValue; label?: string; scale?: Channel["scale"]};

/**
* In some contexts, when specifying a mark channel’s value, you can provide a
Expand Down
4 changes: 2 additions & 2 deletions src/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {registry} from "./scales/index.js";
import {isSymbol, maybeSymbol} from "./symbol.js";
import {maybeReduce} from "./transforms/group.js";

export function createChannel(data, {scale, type, value, filter, hint}, name) {
export function createChannel(data, {scale, type, value, filter, hint, label = labelof(value)}, name) {
if (hint === undefined && typeof value?.transform === "function") hint = value.hint;
return inferChannelScale(name, {
scale,
type,
value: valueof(data, value),
label: labelof(value),
label,
filter,
hint
});
Expand Down
8 changes: 6 additions & 2 deletions src/mark.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {Channel, ChannelDomainSort, ChannelValue, ChannelValues, ChannelValueSpec} from "./channel.js";
import type {Context} from "./context.js";
import type {Dimensions} from "./dimensions.js";
import type {TipOptions} from "./marks/tip.js";
import type {plot} from "./plot.js";
import type {ScaleFunctions} from "./scales.js";
import type {InitializerFunction, SortOrder, TransformFunction} from "./transforms/basic.js";
Expand All @@ -23,6 +24,9 @@ export type FrameAnchor =
| "bottom-left"
| "left";

/** The pointer mode for the tip; corresponds to pointerX, pointerY, and pointer. */
export type TipPointer = "x" | "y" | "xy";

/**
* A mark’s data; one of:
*
Expand Down Expand Up @@ -275,8 +279,8 @@ export interface MarkOptions {
*/
title?: ChannelValue;

/** Whether to generate a tooltip for this mark. */
tip?: boolean | "x" | "y" | "xy";
/** Whether to generate a tooltip for this mark, and any tip options. */
tip?: boolean | TipPointer | (TipOptions & {pointer?: TipPointer});

/**
* How to clip the mark; one of:
Expand Down
22 changes: 16 additions & 6 deletions src/mark.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {channelDomain, createChannels, valueObject} from "./channel.js";
import {defined} from "./defined.js";
import {maybeFacetAnchor} from "./facet.js";
import {maybeKeyword, maybeNamed, maybeValue} from "./options.js";
import {arrayify, isDomainSort, isOptions, keyword, range, singleton} from "./options.js";
import {maybeNamed, maybeValue} from "./options.js";
import {arrayify, isDomainSort, isObject, isOptions, keyword, range, singleton} from "./options.js";
import {project} from "./projection.js";
import {maybeClip, styles} from "./style.js";
import {basic, initializer} from "./transforms/basic.js";
Expand Down Expand Up @@ -150,17 +150,27 @@ export function composeRender(r1, r2) {
function maybeChannels(channels) {
return Object.fromEntries(
Object.entries(maybeNamed(channels)).map(([name, channel]) => {
channel = maybeValue(channel);
channel = typeof channel === "string" ? {value: channel, label: name} : maybeValue(channel); // for shorthand extra channels, use name as label
if (channel.filter === undefined && channel.scale == null) channel = {...channel, filter: null};
return [name, channel];
})
);
}

function maybeTip(tip) {
return tip === true ? "xy" : tip === false ? null : maybeKeyword(tip, "tip", ["x", "y", "xy"]);
return tip === true
? "xy"
: tip === false || tip == null
? null
: typeof tip === "string"
? keyword(tip, "tip", ["x", "y", "xy"])
: tip; // tip options object
}

export function withTip(options, tip) {
return options?.tip === true ? {...options, tip} : options;
export function withTip(options, pointer) {
return options?.tip === true
? {...options, tip: pointer}
: isObject(options?.tip) && options.tip.pointer === undefined
? {...options, tip: {...options.tip, pointer}}
: options;
}
9 changes: 8 additions & 1 deletion src/marks/tip.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ChannelValueSpec} from "../channel.js";
import type {ChannelName, ChannelValueSpec} from "../channel.js";
import type {Data, FrameAnchor, MarkOptions, RenderableMark} from "../mark.js";
import type {TextStyles} from "./text.js";

Expand Down Expand Up @@ -61,6 +61,13 @@ export interface TipOptions extends MarkOptions, TextStyles {
* the right of the anchor position.
*/
anchor?: FrameAnchor;

/**
* How channel values are formatted for display. If a format is a string, it
* is interpreted as a (UTC) time format for temporal channels, and otherwise
* a number format.
*/
format?: {[name in ChannelName]?: boolean | string | ((d: any, i: number) => string)};
}

/**
Expand Down
Loading

0 comments on commit a5ef931

Please sign in to comment.