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

allow negative dates, y labels, better spacing #610

Merged
merged 3 commits into from
Aug 4, 2018
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
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