Skip to content

Commit 94f5d67

Browse files
working svg labels
1 parent c96bb6d commit 94f5d67

2 files changed

Lines changed: 59 additions & 62 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ check:
1616
# uv run mypy --strict --implicit-reexport hiplot
1717
uv run ty check
1818

19+
dev:
20+
uv run hiplot --port 8765

src/parallel/parallel.tsx

Lines changed: 57 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ import style from "../hiplot.scss";
1919
import { HiPlotPluginData } from "../plugin";
2020
import { ResizableH } from "../lib/resizable";
2121
import { Filter, FilterType, apply_filters } from "../filters";
22-
import { foDynamicSizeFitContent, foCreateAxisLabel } from "../lib/svghelpers";
23-
import { IS_SAFARI, redrawForeignObject } from "../lib/browsercompat";
22+
import { IS_MOBILE } from "../lib/browsercompat";
2423

2524
interface StringMapping<V> { [key: string]: V; };
2625

@@ -107,6 +106,13 @@ export class ParallelPlot extends React.Component<ParallelPlotData, ParallelPlot
107106
axis: d3.Axis<number>;
108107
d3brush = d3.brushY();
109108

109+
private labelTextFromHtml(html: string, fallback: string): string {
110+
const tmp = document.createElement("div");
111+
tmp.innerHTML = html || "";
112+
const text = (tmp.textContent || tmp.innerText || "").trim();
113+
return text.length ? text : fallback;
114+
}
115+
110116
constructor(props: ParallelPlotData) {
111117
super(props);
112118
this.state = {
@@ -159,28 +165,11 @@ export class ParallelPlot extends React.Component<ParallelPlotData, ParallelPlot
159165
this.xscale.domain(this.state.dimensions);
160166
this.dimensions_dom.filter(function(this: ParallelPlot, p) { return this.state.dimensions.indexOf(p) == -1; }.bind(this)).remove();
161167
this.dimensions_dom = this.dimensions_dom.filter(function(this: ParallelPlot, p) { return this.state.dimensions.indexOf(p) !== -1; }.bind(this));
162-
if (!this.state.dragging && !IS_SAFARI) {
168+
if (!this.state.dragging) {
163169
g = g.transition();
164170
}
165171
g.attr("transform", function(this: ParallelPlot, p) { return "translate(" + this.position(p) + ")"; }.bind(this));
166172

167-
// Safari: apply CSS transforms to foreignObjects during drag, redraw after
168-
if (IS_SAFARI) {
169-
if (this.state.dragging) {
170-
const me = this;
171-
this.dimensions_dom.each(function(dim: string) {
172-
const fo = d3.select(this).select("foreignObject");
173-
const delta = me.position(dim) - me.xscale(dim);
174-
fo.style("transform", delta !== 0 ? `translateX(${delta}px)` : null);
175-
});
176-
} else {
177-
// Not dragging - force redraw all foreignObjects to ensure correct position
178-
this.dimensions_dom.selectAll("foreignObject").each(function() {
179-
redrawForeignObject(this as SVGForeignObjectElement);
180-
});
181-
}
182-
}
183-
184173
this.update_ticks();
185174
this.updateAxisTitlesAnglesAndFontSize();
186175
// Notify parent of hidden columns count change (dimensions affects can_restore_axis)
@@ -396,22 +385,8 @@ export class ParallelPlot extends React.Component<ParallelPlotData, ParallelPlot
396385
}
397386
me.dimensions_dom.attr("transform", function(d) { return "translate(" + me.position(d) + ")"; });
398387

399-
// Safari doesn't update foreignObject positions when parent transforms change.
400-
// Apply the position delta directly to each foreignObject as a CSS transform.
401-
if (IS_SAFARI) {
402-
me.dimensions_dom.each(function(dim: string) {
403-
const fo = d3.select(this).select("foreignObject");
404-
const delta = me.position(dim) - me.xscale(dim);
405-
fo.style("transform", delta !== 0 ? `translateX(${delta}px)` : null);
406-
});
407-
}
408388
})
409389
.on("end", function(event, d: string) {
410-
// Clear Safari CSS transforms from foreignObjects
411-
if (IS_SAFARI) {
412-
me.dimensions_dom.selectAll("foreignObject").style("transform", null);
413-
}
414-
415390
if (!me.state.dragging.dragging) {
416391
// no movement, invert axis
417392
var extent = invert_axis(d);
@@ -425,15 +400,7 @@ export class ParallelPlot extends React.Component<ParallelPlotData, ParallelPlot
425400
me.setState({order: Array.from(me.state.dimensions), dragging: null}, function() {
426401
// reorder axes
427402
const parentG = d3.select(element.parentElement.parentElement);
428-
if (IS_SAFARI) {
429-
// Skip transition on Safari - it causes foreignObject positioning issues
430-
parentG.attr("transform", "translate(" + me.xscale(d) + ")");
431-
// Force redraw of the foreignObject to ensure correct position
432-
const fo = parentG.select("foreignObject").node() as SVGForeignObjectElement;
433-
if (fo) redrawForeignObject(fo);
434-
} else {
435-
parentG.transition().attr("transform", "translate(" + me.xscale(d) + ")");
436-
}
403+
parentG.transition().attr("transform", "translate(" + me.xscale(d) + ")");
437404
var extents = brush_extends();
438405
extent = extents[d];
439406
me.update_ticks(d, extent);
@@ -465,17 +432,49 @@ export class ParallelPlot extends React.Component<ParallelPlotData, ParallelPlot
465432
// @ts-ignore
466433
d3.select(this).call(me.get_axis(d));
467434
})
468-
.append(function(dim) { return foCreateAxisLabel(me.props.params_def[dim], me.props.context_menu_ref, null); })
469-
.attr("y", -20)
435+
.append("text")
436+
.text(function(dim) {
437+
const pd = me.props.params_def[dim];
438+
return me.labelTextFromHtml(pd.label_html, pd.name);
439+
})
470440
.attr("text-anchor", "left")
471441
.classed("pplot-label", true)
472-
.classed(style.pplotLabel, true);
473-
me.dimensions_dom.selectAll(".label-name").style("font-size", "12px");
474-
me.dimensions_dom.selectAll(".pplot-label").each(function(this: SVGForeignObjectElement, d: string) {
475-
foDynamicSizeFitContent(this, [-me.xscale(d) + 5, -me.xscale(d) + me.state.width - 5]);
476-
}).attr("x", 0).style("width", "1px");
442+
.classed("label-name", true)
443+
.classed(style.axisLabelText, true)
444+
.classed(style.pplotLabel, true)
445+
.each(function(dim) {
446+
d3.select(this).append("title")
447+
.text(IS_MOBILE ? "Long-press for options" : "Right click for options");
448+
})
449+
.on("contextmenu", function(event, dim: string) {
450+
if (me.props.context_menu_ref && me.props.context_menu_ref.current) {
451+
me.props.context_menu_ref.current.show(event.pageX, event.pageY, dim);
452+
event.preventDefault();
453+
event.stopPropagation();
454+
}
455+
})
456+
.on("touchstart", function(event, dim: string) {
457+
const target = this as SVGTextElement;
458+
if (!me.props.context_menu_ref || !me.props.context_menu_ref.current) {
459+
return;
460+
}
461+
const touch = (event as TouchEvent).touches[0];
462+
const timer = window.setTimeout(() => {
463+
me.props.context_menu_ref.current.show(touch.pageX, touch.pageY, dim);
464+
(target as any).__longPressTimer = null;
465+
}, 500);
466+
(target as any).__longPressTimer = timer;
467+
})
468+
.on("touchend touchcancel touchmove", function() {
469+
const target = this as SVGTextElement;
470+
const timer = (target as any).__longPressTimer;
471+
if (timer) {
472+
window.clearTimeout(timer);
473+
(target as any).__longPressTimer = null;
474+
}
475+
});
477476
me.updateAxisTitlesAnglesAndFontSize();
478-
me.dimensions_dom.selectAll("foreignObject").call(create_drag_beh());
477+
me.dimensions_dom.selectAll(".pplot-label").call(create_drag_beh());
479478

480479
// Add and store a brush for each axis.
481480
me.dimensions_dom.append("svg:g")
@@ -736,18 +735,14 @@ export class ParallelPlot extends React.Component<ParallelPlotData, ParallelPlot
736735

737736
updateAxisTitlesAnglesAndFontSize() {
738737
const FONT_SIZE = 12;
739-
this.dimensions_dom.selectAll(".label-name").each(function(this: HTMLSpanElement) {
738+
this.dimensions_dom.selectAll(".pplot-label").each(function(this: SVGTextElement) {
740739
this.style.fontSize = FONT_SIZE + "px";
741-
742-
// Use writing-mode for vertical text on all browsers
743-
// This provides a unified look and avoids Safari's foreignObject bugs
744-
this.style.writingMode = "vertical-lr";
745-
this.style.textOrientation = "mixed";
746-
this.style.transform = "";
747-
748-
const fo = this.parentElement.parentElement as any as SVGForeignObjectElement;
749-
// Position labels at the very top of the margin area
750-
fo.setAttribute("y", (-TOP_MARGIN_PIXELS + 2) + "");
740+
this.style.writingMode = "";
741+
this.style.textOrientation = "";
742+
this.setAttribute("x", "10");
743+
this.setAttribute("y", "0");
744+
this.setAttribute("text-anchor", "start");
745+
this.setAttribute("transform", "rotate(-90)");
751746
});
752747
}
753748

0 commit comments

Comments
 (0)