Skip to content

Commit

Permalink
Merge pull request #610 from nextstrain/fix_grid
Browse files Browse the repository at this point in the history
allow negative dates, y labels, better spacing
  • Loading branch information
jameshadfield authored Aug 4, 2018
2 parents 25df23a + 289add4 commit 78f737a
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 92 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ rules:
import/prefer-default-export: off
no-multi-spaces: ["error", { ignoreEOLComments: true }]
no-labels: off
no-unused-labels: off
no-continue: off
no-unneeded-ternary: ["error", { "defaultAssignment": true }]
quote-props: ["error", "as-needed"]
Expand Down
206 changes: 114 additions & 92 deletions src/components/tree/phyloTree/grid.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable space-infix-ops */
import { max } from "d3-array";
import { min, max } from "d3-array";
import { timerStart, timerEnd } from "../../../util/perf";

export const hideGrid = function hideGrid() {
Expand All @@ -8,87 +8,143 @@ export const hideGrid = function hideGrid() {
this.svg.selectAll(".gridTick").style('visibility', 'hidden');
};

const calculateMajorGridSeperation = (range) => {
const logRange = Math.floor(Math.log10(range));
let step = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties
if (range/step < 2) {
step /= 5;
} else if (range/step <5) {
step /= 2;
}
return step;
};

/**
* add a grid to the svg
* @param {layout}
*/
export const addGrid = function addGrid(layout, yMinView, yMaxView) {
timerStart("addGrid");
export const addGrid = function addGrid(layout) {
if (typeof layout==="undefined") {layout=this.layout;} // eslint-disable-line no-param-reassign
if (layout==="unrooted") return;

const xmin = (this.xScale.domain()[0]>0)?this.xScale.domain()[0]:0.0;
const ymin = this.yScale.domain()[1];
const ymax = this.yScale.domain()[0];
const xmax = layout === "radial" ?
max([this.xScale.domain()[1], this.yScale.domain()[1], -this.xScale.domain()[0], -this.yScale.domain()[0]]) :
this.xScale.domain()[1];
timerStart("addGrid");

const offset = layout==="radial"?this.nodes[0].depth:0.0;
const viewTop = yMaxView ? yMaxView+this.params.margins.top : this.yScale.range()[0];
const viewBottom = yMinView ? yMinView-this.params.margins.bottom : this.yScale.range()[1];


/* should we re-draw the grid? */
/* not running this block as it failed when the broswer dimensions had changed
if (!this.gridParams) {
this.gridParams = [xmin, xmax, ymin, ymax, viewTop, viewBottom, layout];
} else if (
xmin === this.gridParams[0] && xmax === this.gridParams[1] &&
ymin === this.gridParams[2] && ymax === this.gridParams[3] &&
viewTop === this.gridParams[4] && viewBottom === this.gridParams[5] &&
layout === this.gridParams[6]
) {
console.log("bailing - no difference");
return;
}
*/
/* [xmin, xmax] is the domain of the x-axis (rectangular & clock layouts) or polar-axis (radial layouts)
[ymin, ymax] for rectangular layouts is [1, n] where n is the number of tips (in the view)
clock layouts is [min_divergence_in_view, max_divergence_in_view]
radial layouts is the radial domain (negative means "left of north") measured in radians */
const ymin = min(this.yScale.domain());
const ymax = max(this.yScale.domain());
const xmin = layout==="radial" ? this.nodes[0].depth : this.xScale.domain()[0];
const xmax = layout==="radial" ?
xmin + max([this.xScale.domain()[1], this.yScale.domain()[1], -this.xScale.domain()[0], -this.yScale.domain()[0]]) :
this.xScale.domain()[1];

/* yes - redraw and update gridParams */
this.gridParams = [xmin, xmax, ymin, ymax, viewTop, viewBottom, layout];
/* step is the amount (same units of xmax, xmin) of seperation between major grid lines */
const step = calculateMajorGridSeperation(xmax-xmin);

/* determine grid points (i.e. on the x/polar axis where lines/circles will be drawn through)
Major grid points are thicker and have text
Minor grid points have no text */
const majorGridPoints = [];
const minorGridPoints = [];
determineGridPoints: {
const gridMin = Math.floor(xmin/step)*step;
const minVis = layout==="radial" ? xmin : gridMin;
const maxVis = xmax;
for (let ii = 0; ii <= (xmax - gridMin)/step+3; ii++) {
const pos = gridMin + step*ii;
majorGridPoints.push([pos, ((pos<minVis)||(pos>maxVis))?"hidden":"visible", "x"]);
}
const numMinorTicks = this.distanceMeasure === "num_date" ? this.params.minorTicksTimeTree : this.params.minorTicks;
const minorStep = step / numMinorTicks;
for (let ii = 0; ii <= (xmax - gridMin)/minorStep+30; ii++) {
const pos = gridMin + minorStep*ii;
minorGridPoints.push([pos, ((pos<minVis)||(pos>maxVis+minorStep))?"hidden":"visible", "x"]);
}
}

const gridline = function gridline(xScale, yScale, layoutShadow) {
return (x) => {
const xPos = xScale(x[0]-offset);
let tmp_d="";
/* HOF, which returns the fn which constructs the SVG path string
to draw the axis lines (circles for radial trees).
"gridPoint" is an element from majorGridPoints or minorGridPoints */
const gridline = (xScale, yScale, layoutShadow) => (gridPoint) => {
let svgPath="";
if (gridPoint[2] === "x") {
if (layoutShadow==="rect" || layoutShadow==="clock") {
tmp_d = 'M'+xPos.toString() +
const xPos = xScale(gridPoint[0]);
svgPath = 'M'+xPos.toString() +
" " +
viewBottom.toString() +
yScale.range()[1].toString() +
" L " +
xPos.toString() +
" " +
viewTop.toString();
yScale.range()[0].toString();
} else if (layoutShadow==="radial") {
tmp_d = 'M '+xPos.toString() +
const xPos = xScale(gridPoint[0]-xmin);
svgPath = 'M '+xPos.toString() +
" " +
yScale(0).toString() +
" A " +
(xPos - xScale(0)).toString() +
" " +
(yScale(x[0]) - yScale(offset)).toString() +
(yScale(gridPoint[0]) - yScale(xmin)).toString() +
" 0 1 0 " +
xPos.toString() +
" " +
(yScale(0)+0.001).toString();
}
return tmp_d;
};
} else if (gridPoint[2] === "y") {
const yPos = yScale(gridPoint[0]);
svgPath = `M${xScale(xmin) + 20} ${yPos} L ${xScale(xmax)} ${yPos}`;
}
return svgPath;
};

/* add text labels to the major grid points */

/* HOF which returns a function which calculates the x position of text labels */
const xTextPos = (xScale, layoutShadow) => (gridPoint) => {
if (gridPoint[2] === "x") { // "normal" labels on the x-axis / polar-axis
return layoutShadow==="radial" ? xScale(0) : xScale(gridPoint[0]);
}
// clock layout y positions (which display divergence)
return xScale.range()[0]-15;
};

/* same as xTextPos HOF, but for y-values */
const yTextPos = (yScale, layoutShadow) => (gridPoint) => {
if (gridPoint[2] === "x") {
return layoutShadow === "radial" ? yScale(gridPoint[0]-xmin)-5 : yScale.range()[1] + 18;
}
return yScale(gridPoint[0]);
};

const logRange = Math.floor(Math.log10(xmax - xmin));
const roundingLevel = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties
const gridMin = Math.floor((xmin+offset)/roundingLevel)*roundingLevel;
const gridPoints = [];
for (let ii = 0; ii <= (xmax + offset - gridMin)/roundingLevel+10; ii++) {
const pos = gridMin + roundingLevel*ii;
if (pos>offset) {
gridPoints.push([pos, pos-offset>xmax?"hidden":"visible", "x"]);
/* HOF which returns a function which calculates the text anchor string */
const textAnchor = (layoutShadow) => (gridPoint) => {
if (gridPoint[2] === "x") {
return layoutShadow === "radial" ? "end" : "middle";
}
return "start";
};

/* for clock layouts, add y-points to the majorGridPoints array
Note that these don't have lines drawn, only text */
let yStep = 0;
if (this.layout==="clock") {
yStep = calculateMajorGridSeperation(ymax-ymin);
const gridYMin = Math.floor(ymin/yStep)*yStep;
const maxYVis = ymax;
const minYVis = gridYMin;
for (let ii = 1; ii <= (ymax - gridYMin)/yStep+10; ii++) {
const pos = gridYMin + yStep*ii;
majorGridPoints.push([pos, ((pos<minYVis)||(pos>maxYVis))?"hidden":"visible", "y"]);
}
}

const majorGrid = this.svg.selectAll('.majorGrid').data(gridPoints);
/* D3 commands to add grid + text to the DOM */

// add major grid to svg
const majorGrid = this.svg.selectAll('.majorGrid').data(majorGridPoints);
majorGrid.exit().remove(); // EXIT
majorGrid.enter().append("path") // ENTER
.merge(majorGrid) // ENTER + UPDATE
Expand All @@ -99,43 +155,7 @@ export const addGrid = function addGrid(layout, yMinView, yMaxView) {
.style("stroke", this.params.majorGridStroke)
.style("stroke-width", this.params.majorGridWidth);

const xTextPos = (xScale, layoutShadow) => (x) => {
if (x[2] === "x") {
return layoutShadow === "radial" ? xScale(0) : xScale(x[0]);
}
return xScale.range()[1];
};
const yTextPos = (yScale, layoutShadow) => (x) => {
if (x[2] === "x") {
return layoutShadow === "radial" ? yScale(x[0]-offset) : viewBottom + 18;
}
return yScale(x[0]);
};


let logRangeY = 0;
if (this.layout==="clock") {
const roundingLevelY = Math.pow(10, logRangeY); // eslint-disable-line no-restricted-properties
logRangeY = Math.floor(Math.log10(ymax - ymin));
const offsetY=0;
const gridMinY = Math.floor((ymin+offsetY)/roundingLevelY)*roundingLevelY;
for (let ii = 0; ii <= (ymax + offsetY - gridMinY)/roundingLevelY+10; ii++) {
const pos = gridMinY + roundingLevelY*ii;
if (pos>offsetY) {
gridPoints.push([pos, pos-offsetY>ymax ? "hidden" : "visible", "y"]);
}
}
}

const minorRoundingLevel = roundingLevel /
(this.distanceMeasure === "num_date"? this.params.minorTicksTimeTree : this.params.minorTicks);
const minorGridPoints = [];
for (let ii = 0; ii <= (xmax + offset - gridMin)/minorRoundingLevel+50; ii++) {
const pos = gridMin + minorRoundingLevel*ii;
if (pos>offset) {
minorGridPoints.push([pos, pos-offset>xmax+minorRoundingLevel?"hidden":"visible"]);
}
}
// add minor grid to SVG
const minorGrid = this.svg.selectAll('.minorGrid').data(minorGridPoints);
minorGrid.exit().remove(); // EXIT
minorGrid.enter().append("path") // ENTER
Expand All @@ -147,18 +167,20 @@ export const addGrid = function addGrid(layout, yMinView, yMaxView) {
.style("stroke", this.params.minorGridStroke)
.style("stroke-width", this.params.minorGridWidth);

const gridLabels = this.svg.selectAll('.gridTick').data(gridPoints);
const precision = Math.max(0, 1-logRange);
const precisionY = Math.max(0, 1-logRangeY);

/* draw the text labels for majorGridPoints */
const gridLabels = this.svg.selectAll('.gridTick').data(majorGridPoints);
const precisionX = Math.max(0, -Math.floor(Math.log10(step)));
const precisionY = Math.max(0, -Math.floor(Math.log10(yStep)));
gridLabels.exit().remove(); // EXIT
gridLabels.enter().append("text") // ENTER
.merge(gridLabels) // ENTER + UPDATE
.text((d) => d[0].toFixed(d[2]==='y' ? precisionY : precision))
.text((d) => d[0].toFixed(d[2]==='y' ? precisionY : precisionX))
.attr("class", "gridTick")
.style("font-size", this.params.tickLabelSize)
.style("font-family", this.params.fontFamily)
.style("fill", this.params.tickLabelFill)
.style("text-anchor", this.layout==="radial" ? "end" : "middle")
.style("text-anchor", textAnchor(layout))
.style("visibility", (d) => d[1])
.attr("x", xTextPos(this.xScale, layout))
.attr("y", yTextPos(this.yScale, layout));
Expand Down

0 comments on commit 78f737a

Please sign in to comment.