diff --git a/docs/marks/tip.md b/docs/marks/tip.md index 9ea3c7fe37..34ccb0120c 100644 --- a/docs/marks/tip.md +++ b/docs/marks/tip.md @@ -97,7 +97,7 @@ Plot.plot({ ``` ::: -If no **title** channel is supplied, the tip mark displays all channel values. You can supply additional name-value pairs by registering extra channels using the **channels** mark option. In the scatterplot of Olympic athletes below, you can hover to see the *name* and *sport* of each athlete. This is helpful for noticing patterns — tall basketball players, giant weightlifters and judoka, diminutive gymnasts — and for seeing individuals. +If no **title** channel is supplied, the tip mark displays all channel values as name-value pairs, clipping long values with an ellipsis. Set **textOverflow** to *null* to disable clipping, or use another [text overflow](../marks/text.md) mode such as *ellipsis-middle*. You can supply additional name-value pairs by registering extra channels using the **channels** mark option. In the scatterplot of Olympic athletes below, you can hover to see the *name* and *sport* of each athlete. This is helpful for noticing patterns — tall basketball players, giant weightlifters and judoka, diminutive gymnasts — and for seeing individuals. :::plot defer https://observablehq.com/@observablehq/plot-tips-additional-channels ```js @@ -253,7 +253,7 @@ These [standard text options](./text.md#text-options) control the display of tex - **textAnchor** - the [text anchor](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor) for horizontal position; *start*, *end*, or *middle* - **lineHeight** - the line height in ems; defaults to 1 - **lineWidth** - the line width in ems, for wrapping; defaults to 20 -- **textOverflow** - how to wrap or clip lines longer than the specified line width +- **textOverflow** - how to wrap or clip lines longer than the specified line width; defaults to *ellipsis* for name-value tips ## tip(*data*, *options*) {#tip} diff --git a/src/marks/text.js b/src/marks/text.js index a243c5a1c1..b8e529d812 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -121,7 +121,7 @@ export class Text extends Mark { export function maybeTextOverflow(textOverflow) { return textOverflow == null - ? null + ? textOverflow : keyword(textOverflow, "textOverflow", [ "clip", // shorthand for clip-end "ellipsis", // … ellipsis-end diff --git a/src/marks/tip.js b/src/marks/tip.js index a23bedb994..4973a37daf 100644 --- a/src/marks/tip.js +++ b/src/marks/tip.js @@ -92,6 +92,7 @@ export class Tip extends Mark { const {x, y, fx, fy} = scales; const {ownerSVGElement: svg, document} = context; const {anchor, monospace, lineHeight, lineWidth} = this; + let {textOverflow} = this; const {textPadding: r, pointerSize: m, pathFilter} = this; const {marginTop, marginLeft} = dimensions; @@ -126,6 +127,7 @@ export class Tip extends Mark { } else { sources = getSourceChannels.call(this, values.channels, scales); format = formatChannels; + if (textOverflow === undefined) textOverflow = "ellipsis-end"; } // Format the tip text, skipping any nulls. @@ -191,13 +193,10 @@ export class Tip extends Mark { title = value.trim(); value = ""; } else { - if (label || (!value && !swatch)) value = " " + value; - const [k] = cut(value, w - widthof(label), widthof, ee); - if (k >= 0) { - // value is truncated - title = value.trim(); - value = value.slice(0, k).trimEnd() + ellipsis; - } + const space = label || (!value && !swatch) ? " " : ""; + const clipped = clipper({monospace, lineWidth: lineWidth - widthof(label + space) / 100, textOverflow})(value); + if (clipped !== value) title = value.trim(); // show untruncated value in title + value = space + clipped; } const line = selection.append("tspan").attr("x", 0).attr("dy", `${lineHeight}em`).text("\u200b"); // zwsp for double-click if (label) line.append("tspan").attr("font-weight", "bold").text(label); diff --git a/test/output/tipTextOverflowClipEnd.svg b/test/output/tipTextOverflowClipEnd.svg new file mode 100644 index 0000000000..c33e2707d2 --- /dev/null +++ b/test/output/tipTextOverflowClipEnd.svg @@ -0,0 +1,31 @@ + + + + + Long sentence that gets clipped at the end + + + x + + + + + x Long sentence that gets clipped at theLong sentence that gets clipped at the end + + + \ No newline at end of file diff --git a/test/output/tipTextOverflowDefault.svg b/test/output/tipTextOverflowDefault.svg new file mode 100644 index 0000000000..e0f4aa66aa --- /dev/null +++ b/test/output/tipTextOverflowDefault.svg @@ -0,0 +1,31 @@ + + + + + Long sentence that gets an ellipsis at the end + + + x + + + + + x Long sentence that gets an ellipsis at…Long sentence that gets an ellipsis at the end + + + \ No newline at end of file diff --git a/test/output/tipTextOverflowEllipsisEnd.svg b/test/output/tipTextOverflowEllipsisEnd.svg new file mode 100644 index 0000000000..e0f4aa66aa --- /dev/null +++ b/test/output/tipTextOverflowEllipsisEnd.svg @@ -0,0 +1,31 @@ + + + + + Long sentence that gets an ellipsis at the end + + + x + + + + + x Long sentence that gets an ellipsis at…Long sentence that gets an ellipsis at the end + + + \ No newline at end of file diff --git a/test/output/tipTextOverflowEllipsisMiddle.svg b/test/output/tipTextOverflowEllipsisMiddle.svg new file mode 100644 index 0000000000..f24ebc7cd6 --- /dev/null +++ b/test/output/tipTextOverflowEllipsisMiddle.svg @@ -0,0 +1,31 @@ + + + + + Long sentence that gets an ellipsis in the middle + + + x + + + + + x Long sentence tha…llipsis in the middleLong sentence that gets an ellipsis in the middle + + + \ No newline at end of file diff --git a/test/output/tipTextOverflowEllipsisStart.svg b/test/output/tipTextOverflowEllipsisStart.svg new file mode 100644 index 0000000000..176b8706d0 --- /dev/null +++ b/test/output/tipTextOverflowEllipsisStart.svg @@ -0,0 +1,31 @@ + + + + + Long sentence that gets an ellipsis at the start + + + x + + + + + x …tence that gets an ellipsis at the startLong sentence that gets an ellipsis at the start + + + \ No newline at end of file diff --git a/test/output/tipTextOverflowNull.svg b/test/output/tipTextOverflowNull.svg new file mode 100644 index 0000000000..3262e4b7c8 --- /dev/null +++ b/test/output/tipTextOverflowNull.svg @@ -0,0 +1,31 @@ + + + + + Long sentence that does not get clipped no matter how long it gets; it can be really long + + + x + + + + + x Long sentence that does not get clipped no matter how long it gets; it can be really long + + + \ No newline at end of file diff --git a/test/plots/tip.ts b/test/plots/tip.ts index 48c4b30c00..0f8fb33086 100644 --- a/test/plots/tip.ts +++ b/test/plots/tip.ts @@ -258,6 +258,48 @@ test(async function tipLongText() { return Plot.tip([{x: "Long sentence that gets cropped after a certain length"}], {x: "x"}).plot(); }); +test(async function tipTextOverflowNull() { + return Plot.tip([{x: "Long sentence that does not get clipped no matter how long it gets; it can be really long"}], { + x: "x", + textOverflow: null, + anchor: "top" // otherwise it would be bottom + }).plot(); +}); + +test(async function tipTextOverflowClipEnd() { + return Plot.tip([{x: "Long sentence that gets clipped at the end"}], { + x: "x", + textOverflow: "clip" // shorthand for "clip-end" + }).plot(); +}); + +test(async function tipTextOverflowDefault() { + return Plot.tip([{x: "Long sentence that gets an ellipsis at the end"}], { + x: "x" + }).plot(); +}); + +test(async function tipTextOverflowEllipsisEnd() { + return Plot.tip([{x: "Long sentence that gets an ellipsis at the end"}], { + x: "x", + textOverflow: "ellipsis" // shorthand for "ellipsis-end" + }).plot(); +}); + +test(async function tipTextOverflowEllipsisMiddle() { + return Plot.tip([{x: "Long sentence that gets an ellipsis in the middle"}], { + x: "x", + textOverflow: "ellipsis-middle" + }).plot(); +}); + +test(async function tipTextOverflowEllipsisStart() { + return Plot.tip([{x: "Long sentence that gets an ellipsis at the start"}], { + x: "x", + textOverflow: "ellipsis-start" + }).plot(); +}); + test(async function tipNewLines() { return Plot.plot({ height: 40,