Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clip tree to improve visibility of axes #1383

Merged
merged 2 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/components/tree/phyloTree/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPr
/* STEP 2: move tips */
const step2 = () => {
if (!--inProgress) { /* decrement counter. When hits 0 run block */
this.setClipMask();
const updateTips = createUpdateCall(".tip", svgPropsToUpdate);
genericSelectAndModify(this.svg, ".tip", updateTips, transitionTimeMoveTips);
setTimeout(step3, transitionTimeMoveTips);
Expand Down Expand Up @@ -370,6 +371,9 @@ export const change = function change({
}

/* Finally, actually change the SVG elements themselves */
if (svgHasChangedDimensions) {
this.setClipMask();
}
const extras = { removeConfidences, showConfidences, newBranchLabellingKey };
extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance;
extras.hideTipLabels = animationInProgress;
Expand Down
1 change: 0 additions & 1 deletion src/components/tree/phyloTree/defaultParams.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const createDefaultParams = () => ({
tickLabelFill: darkGrey,
minorTicks: 4,
orientation: [1, 1],
margins: {left: 30, right: 15, top: 10, bottom: 40},
showGrid: true,
fillSelected: "#A73",
radiusSelected: 5,
Expand Down
16 changes: 8 additions & 8 deletions src/components/tree/phyloTree/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ const addSVGGroupsIfNeeded = (groups, svg) => {
.attr('class', 'temporalWindowEnd');
}
if (!("majorGrid" in groups)) {
groups.majorGrid = svg.append("g").attr("id", "majorGrid");
groups.majorGrid = svg.append("g").attr("id", "majorGrid").attr("clip-path", "url(#treeClip)");
}
if (!("minorGrid" in groups)) {
groups.minorGrid = svg.append("g").attr("id", "minorGrid");
groups.minorGrid = svg.append("g").attr("id", "minorGrid").attr("clip-path", "url(#treeClip)");
}
if (!("gridText" in groups)) {
groups.gridText = svg.append("g").attr("id", "gridText");
Expand Down Expand Up @@ -409,7 +409,7 @@ export const addGrid = function addGrid() {
.style("fill", this.params.tickLabelFill)
.style("text-anchor", "middle")
.attr("x", Math.abs(this.xScale.range()[1]-this.xScale.range()[0]) / 2)
.attr("y", this.yScale.range()[1] + this.params.margins.bottom - 6);
.attr("y", parseInt(this.svg.attr("height"), 10) - 1);
}
if (yAxisLabel) {
this.groups.axisText
Expand Down Expand Up @@ -453,7 +453,7 @@ export const showTemporalSlice = function showTemporalSlice() {
const rightHandTree = this.params.orientation[0] === -1;
const rootXPos = this.xScale(this.nodes[0].x);
let totalWidth = rightHandTree ? this.xScale.range()[0] : this.xScale.range()[1];
totalWidth += (this.params.margins.left + this.params.margins.right);
totalWidth += (this.margins.left + this.margins.right);

/* the gray region between the root (ish) and the minimum date */
if (Math.abs(xWindow[0]-rootXPos) > minPxThreshold) { /* don't render anything less than this num of px */
Expand Down Expand Up @@ -488,13 +488,13 @@ export const showTemporalSlice = function showTemporalSlice() {

/* the gray region between the maximum selected date and the last tip */
let xStart_endRegion = xWindow[1]; // starting X coordinate of the "end" rectangle
let width_endRegion = totalWidth - this.params.margins.right - xWindow[1];
let width_endRegion = totalWidth - this.margins.right - xWindow[1];

let transform_endRegion = `translate(${totalWidth - this.params.margins.right},0) scale(-1,1)`;
let transform_endRegion = `translate(${totalWidth - this.margins.right},0) scale(-1,1)`;
// With a right hand tree, the coordinate system flips (right to left)
if (rightHandTree) {
xStart_endRegion = this.params.margins.right;
width_endRegion = xWindow[1] - this.params.margins.right;
xStart_endRegion = this.margins.right;
width_endRegion = xWindow[1] - this.margins.right;
transform_endRegion = `translate(${xStart_endRegion},0)`;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/tree/phyloTree/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const drawBranchLabels = function drawBranchLabels(key) {
const visibility = createBranchLabelVisibility(key, this.layout, this.zoomNode.n.tipCount);

if (!("branchLabels" in this.groups)) {
this.groups.branchLabels = this.svg.append("g").attr("id", "branchLabels");
this.groups.branchLabels = this.svg.append("g").attr("id", "branchLabels").attr("clip-path", "url(#treeClip)");
}
this.groups.branchLabels
.selectAll(".branchLabel")
Expand Down
89 changes: 47 additions & 42 deletions src/components/tree/phyloTree/layouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,15 +271,15 @@ export const setDistance = function setDistance(distanceAttribute) {
* which are used to map the x,y coordinates to the screen
* @param {margins} -- object with "right, left, top, bottom" margins
*/
export const setScales = function setScales(margins) {
export const setScales = function setScales() {

if (this.layout==="scatter" && !this.scatterVariables.xContinuous) {
this.xScale = scalePoint().round(false).align(0.5);
this.xScale = scalePoint().round(false).align(0.5).padding(0.5);
} else {
this.xScale = scaleLinear();
}
if (this.layout==="scatter" && !this.scatterVariables.yContinuous) {
this.yScale = scalePoint().round(false).align(0.5);
this.yScale = scalePoint().round(false).align(0.5).padding(0.5);
} else {
this.yScale = scaleLinear();
}
Expand All @@ -288,25 +288,25 @@ export const setScales = function setScales(margins) {
const height = parseInt(this.svg.attr("height"), 10);
if (this.layout === "radial" || this.layout === "unrooted") {
// Force Square: TODO, harmonize with the map to screen
const xExtend = width - (margins["left"] || 0) - (margins["right"] || 0);
const yExtend = height - (margins["top"] || 0) - (margins["top"] || 0);
const xExtend = width - this.margins.left - this.margins.right;
const yExtend = height - this.margins.bottom - this.margins.top;
const minExtend = min([xExtend, yExtend]);
const xSlack = xExtend - minExtend;
const ySlack = yExtend - minExtend;
this.xScale.range([0.5 * xSlack + margins["left"] || 0, width - 0.5 * xSlack - (margins["right"] || 0)]);
this.yScale.range([0.5 * ySlack + margins["top"] || 0, height - 0.5 * ySlack - (margins["bottom"] || 0)]);
this.xScale.range([0.5 * xSlack + this.margins.left, width - 0.5 * xSlack - this.margins.right]);
this.yScale.range([0.5 * ySlack + this.margins.top, height - 0.5 * ySlack - this.margins.bottom]);

} else {
// for rectangular layout, allow flipping orientation of left/right and top/bottom
if (this.params.orientation[0] > 0) {
this.xScale.range([margins["left"] || 0, width - (margins["right"] || 0)]);
this.xScale.range([this.margins.left, width - this.margins.right]);
} else {
this.xScale.range([width - (margins["right"] || 0), margins["left"] || 0]);
this.xScale.range([width - this.margins.right, this.margins.left]);
}
if (this.params.orientation[1] > 0) {
this.yScale.range([margins["top"] || 0, height - (margins["bottom"] || 0)]);
this.yScale.range([this.margins.top, height - this.margins.bottom]);
} else {
this.yScale.range([height - (margins["bottom"] || 0), margins["top"] || 0]);
this.yScale.range([height - this.margins.bottom, this.margins.top]);
}
}
};
Expand All @@ -318,39 +318,19 @@ export const setScales = function setScales(margins) {
*/
export const mapToScreen = function mapToScreen() {
timerStart("mapToScreen");
/* pad margins if tip labels are visible */
/* padding width based on character count */
const tmpMargins = {
left: this.params.margins.left,
right: this.params.margins.right,
top: this.params.margins.top,
bottom: this.params.margins.bottom};
if (this.layout==="rect" || this.layout==="unrooted" || this.layout==="scatter") {
// legend is 12px, but 6px is enough to prevent tips being obscured
tmpMargins.top += 6;
}

const inViewTerminalNodes = this.nodes.filter((d) => d.terminal).filter((d) => d.inView);
if (inViewTerminalNodes.length < this.params.tipLabelBreakL1) {

let fontSize = this.params.tipLabelFontSizeL1;
if (inViewTerminalNodes.length < this.params.tipLabelBreakL2) {
fontSize = this.params.tipLabelFontSizeL2;
}
if (inViewTerminalNodes.length < this.params.tipLabelBreakL3) {
fontSize = this.params.tipLabelFontSizeL3;
}
/* set up space (padding) for axes etc, as we don't want the branches & tips to occupy the entire SVG! */
this.margins = {
left: (this.layout==="scatter" || this.layout==="clock") ? 40 : 5, // space for y-axis label
right: 5 + getTipLabelPadding(this.params, inViewTerminalNodes),
top: this.layout==="radial" ? 10 : 15, // avoid tips rendering behind legend
bottom: 35 // space for x-axis labels
};

let padBy = 0;
inViewTerminalNodes.forEach((d) => {
if (padBy < d.n.name.length) {
padBy = 0.65 * d.n.name.length * fontSize;
}
});
tmpMargins.right += padBy;
}

/* set the range of the x & y scales */
this.setScales(tmpMargins);
/* construct & set the range of the x & y scales */
this.setScales();

let nodesInDomain = this.nodes.filter((d) => d.inView && d.y!==undefined && d.x!==undefined);
// scatterplots further restrict nodes used for domain calcs - if not rendering branches,
Expand Down Expand Up @@ -510,7 +490,10 @@ export const mapToScreen = function mapToScreen() {
timerEnd("mapToScreen");
};

const JITTER_MIN_STEP_SIZE = 50; // pixels

function padCategoricalScales(domain, scale) {
if (scale.step() > JITTER_MIN_STEP_SIZE) return scale.padding(0.5); // balanced padding when we can jitter
if (domain.length<=4) return scale.padding(0.4);
if (domain.length<=6) return scale.padding(0.3);
if (domain.length<=10) return scale.padding(0.2);
Expand All @@ -522,7 +505,7 @@ function padCategoricalScales(domain, scale) {
*/
function jitter(axis, scale, nodes) {
const step = scale.step();
if (step < 50) return; // don't jitter if there's little space between bands
if (scale.step() <= JITTER_MIN_STEP_SIZE) return;
const rand = []; // pre-compute a small set of pseudo random numbers for speed
for (let i=1e2; i--;) {
rand.push((Math.random()-0.5)*step*0.5); // occupy 50%
Expand All @@ -538,3 +521,25 @@ function jitter(axis, scale, nodes) {
}
recurse(nodes[0]);
}


function getTipLabelPadding(params, inViewTerminalNodes) {
let padBy = 0;
if (inViewTerminalNodes.length < params.tipLabelBreakL1) {

let fontSize = params.tipLabelFontSizeL1;
if (inViewTerminalNodes.length < params.tipLabelBreakL2) {
fontSize = params.tipLabelFontSizeL2;
}
if (inViewTerminalNodes.length < params.tipLabelBreakL3) {
fontSize = params.tipLabelFontSizeL3;
}

inViewTerminalNodes.forEach((d) => {
if (padBy < d.n.name.length) {
padBy = 0.65 * d.n.name.length * fontSize;
}
});
}
return padBy;
}
1 change: 1 addition & 0 deletions src/components/tree/phyloTree/phyloTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ PhyloTree.prototype.render = renderers.render;
PhyloTree.prototype.clearSVG = renderers.clearSVG;

/* D R A W I N G F U N C T I O N S */
PhyloTree.prototype.setClipMask = renderers.setClipMask;
PhyloTree.prototype.drawTips = renderers.drawTips;
PhyloTree.prototype.drawBranches = renderers.drawBranches;
PhyloTree.prototype.drawVaccines = renderers.drawVaccines;
Expand Down
40 changes: 36 additions & 4 deletions src/components/tree/phyloTree/renderers.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const render = function render(svg, layout, distance, parameters, callbac
});

/* draw functions */
this.setClipMask();
if (this.params.showGrid) {
this.addGrid();
this.showTemporalSlice();
Expand Down Expand Up @@ -99,7 +100,7 @@ export const drawTips = function drawTips() {
const params = this.params;

if (!("tips" in this.groups)) {
this.groups.tips = this.svg.append("g").attr("id", "tips");
this.groups.tips = this.svg.append("g").attr("id", "tips").attr("clip-path", "url(#treeClip)");
}
this.groups.tips
.selectAll(".tip")
Expand Down Expand Up @@ -173,7 +174,7 @@ export const drawBranches = function drawBranches() {
/* PART 1: draw the branch Ts (i.e. the bit connecting nodes parent branch ends to child branch beginnings)
Only rectangular & radial trees have this, so we remove it for clock / unrooted layouts */
if (!("branchTee" in this.groups)) {
this.groups.branchTee = this.svg.append("g").attr("id", "branchTee");
this.groups.branchTee = this.svg.append("g").attr("id", "branchTee").attr("clip-path", "url(#treeClip)");
}
if (this.layout === "clock" || this.layout === "scatter" || this.layout === "unrooted") {
this.groups.branchTee.selectAll("*").remove();
Expand Down Expand Up @@ -207,7 +208,7 @@ export const drawBranches = function drawBranches() {
this.updateColorBy();
/* PART 2b: Draw the stems */
if (!("branchStem" in this.groups)) {
this.groups.branchStem = this.svg.append("g").attr("id", "branchStem");
this.groups.branchStem = this.svg.append("g").attr("id", "branchStem").attr("clip-path", "url(#treeClip)");
}
this.groups.branchStem
.selectAll('.branch')
Expand Down Expand Up @@ -246,7 +247,7 @@ export const drawRegression = function drawRegression() {
" L " + this.xScale.range()[1].toString() + " " + rightY.toString();

if (!("regression" in this.groups)) {
this.groups.regression = this.svg.append("g").attr("id", "regression");
this.groups.regression = this.svg.append("g").attr("id", "regression").attr("clip-path", "url(#treeClip)");
}

this.groups.regression
Expand Down Expand Up @@ -371,3 +372,34 @@ export const branchStrokeForHover = function branchStrokeForHover(d) {
if (!d) { return; }
handleBranchHoverColor(d, getEmphasizedColor(d.parent.branchStroke), getEmphasizedColor(d.branchStroke));
};

/**
* Create / update the clipping mask which is attached to branches, tips, branch-labels
* and regression lines. In theory, we can clip to exactly the {xy}Scale range, however
* in practice, elements (or portions of elements) render outside this.
*/
export const setClipMask = function setClipMask() {
const [xMin, xMax, yMin, yMax] = [...this.xScale.range(), ...this.yScale.range()];
const x0 = xMin - 5;
const width = xMax - xMin + 20; // RHS overflow is not problematic
const y0 = yMin - 15; // some overflow at top is ok
const height = yMax - yMin + 20; // extra padding to allow tips & lowest major axis line to render

if (!this.groups.clipPath) {
this.groups.clipPath = this.svg.append("g").attr("id", "clipGroup");
this.groups.clipPath.append("clipPath")
.attr("id", "treeClip")
.append("rect")
.attr("x", x0)
.attr("y", y0)
.attr("width", width)
.attr("height", height);
} else {
this.groups.clipPath.select('rect')
.attr("x", x0)
.attr("y", y0)
.attr("width", width)
.attr("height", height);
}

};