diff --git a/.eslintrc b/.eslintrc index 858b92271..ca97234ac 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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"] diff --git a/src/components/tree/phyloTree/grid.js b/src/components/tree/phyloTree/grid.js index fc515b873..139d2da0d 100644 --- a/src/components/tree/phyloTree/grid.js +++ b/src/components/tree/phyloTree/grid.js @@ -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() { @@ -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, ((posmaxVis))?"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, ((posmaxVis+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, ((posmaxYVis))?"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 @@ -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 @@ -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));