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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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,