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

Do frequency dates like tree dates #1096

Merged
merged 7 commits into from
May 5, 2020
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
20 changes: 16 additions & 4 deletions src/components/frequencies/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { axisBottom, axisLeft } from "d3-axis";
import { rgb } from "d3-color";
import { area } from "d3-shape";
import { format } from "d3-format";
import _range from "lodash/range";
import { dataFont } from "../../globalStyles";
import { unassigned_label } from "../../util/processFrequencies";
import { isColorByGenotype, decodeColorByGenotype } from "../../util/getGenotype";
import { numericToCalendar } from "../../util/dateHelpers";
import { createDisplayDate, calculateMajorGridSeperationForTime } from "../tree/phyloTree/grid";

/* C O N S T A N T S */
const opacity = 0.85;
Expand Down Expand Up @@ -52,11 +55,11 @@ const getOrderedCategories = (matrixCategories, colorScale) => {
return orderedCategories;
};

export const calcXScale = (chartGeom, pivots, ticks) => {
export const calcXScale = (chartGeom, pivots) => {
const x = scaleLinear()
.domain([pivots[0], pivots[pivots.length - 1]])
.range([chartGeom.spaceLeft, chartGeom.width - chartGeom.spaceRight]);
return {x, numTicksX: ticks.length};
return {x};
};

export const calcYScale = (chartGeom, maxY) => {
Expand All @@ -80,13 +83,22 @@ const removeProjectionInfo = (svg) => {
};

export const drawXAxis = (svg, chartGeom, scales) => {
const domain = scales.x.domain(),
range = scales.x.range();
const {majorStep} = calculateMajorGridSeperationForTime(
domain[1] - domain[0],
range[1] - range[0]
);
const customDate = (date) => createDisplayDate(majorStep, date);
removeXAxis(svg);
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0,${chartGeom.height - chartGeom.spaceBottom})`)
.style("font-family", dataFont)
.style("font-size", "12px")
.call(axisBottom(scales.x).ticks(scales.numTicksX, ".1f"));
.call(axisBottom(scales.x)
.tickValues(_range(domain[0], domain[1], majorStep))
.tickFormat(customDate));
};

export const drawYAxis = (svg, chartGeom, scales) => {
Expand Down Expand Up @@ -296,7 +308,7 @@ export const drawStream = (
.style("font-weight", 300)
.html(
`<p>${parseColorBy(colorBy, colorOptions)}: ${labels[i]}</p>
<p>${t("Time point")}: ${pivots[pivotIdx]}</p>
<p>${t("Time point")}: ${numericToCalendar(pivots[pivotIdx])}</p>
<p>${frequencyText}: ${freqVal}</p>`
);
}
Expand Down
3 changes: 1 addition & 2 deletions src/components/frequencies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import "../../css/entropy.css";
return {
data: state.frequencies.data,
pivots: state.frequencies.pivots,
ticks: state.frequencies.ticks,
matrix: state.frequencies.matrix,
projection_pivot: state.frequencies.projection_pivot,
version: state.frequencies.version,
Expand All @@ -36,7 +35,7 @@ class Frequencies extends React.Component {
const data = processMatrix({...props});
newState.maxY = data.maxY;
newState.categories = data.categories;
const scalesX = calcXScale(chartGeom, props.pivots, props.ticks);
const scalesX = calcXScale(chartGeom, props.pivots);
const scalesY = calcYScale(chartGeom, data.maxY);
newState.scales = {...scalesX, ...scalesY};
drawXAxis(newState.svg, chartGeom, scalesX);
Expand Down
85 changes: 43 additions & 42 deletions src/components/tree/phyloTree/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,79 +49,80 @@ const addSVGGroupsIfNeeded = (groups, svg) => {
* Create the major-grid-line separation for divergence scales.
* @param {numeric} range num years or amount of divergence present in current view
* @param {numeric} minorTicks num of minor ticks desired between each major step
* @returns {array} [0] {numeric} space between major x-axis gridlines (measure of divergence)
* [1] {numeric} space between minor x-axis gridlines (measure of divergence)
* @returns {object}
* - property `majorStep` {numeric}: space between major x-axis gridlines (measure of divergence)
* - property `minorStep` {numeric}: space between minor x-axis gridlines (measure of divergence)
*/
const calculateMajorGridSeperationForDivergence = (range, minorTicks) => {
/* make an informed guess of the step size to start with.
E.g. 0.07 => step of 0.01, 70 => step size of 10 */
const logRange = Math.floor(Math.log10(range));
let step = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties
let majorStep = Math.pow(10, logRange); // eslint-disable-line no-restricted-properties

if (range/step < 2) { // if step > 0.5*range then make more fine-grained steps
step /= 5;
} else if (range/step <5) { // if step > 0.2*range then make more fine grained steps
step /= 2;
if (range/majorStep < 2) { // if step > 0.5*range then make more fine-grained steps
majorStep /= 5;
} else if (range/majorStep <5) { // if step > 0.2*range then make more fine grained steps
majorStep /= 2;
}

let numMinorTicks = minorTicks;
if (step===5 || step===10) {
if (majorStep===5 || majorStep===10) {
numMinorTicks = 5;
}
const minorStep = step / numMinorTicks;
const minorStep = majorStep / numMinorTicks;

return [step, minorStep];
return {majorStep, minorStep};
};

/**
* Create the major-grid-line separation for temporal view.
* @param {numeric} timeRange num years in current view
* @param {numeric} pxAvailable number of pixels available for the x axis
* @returns {array} [0] {numeric} space between major x-axis gridlines (measure of time)
* [1] {numeric} space between minor x-axis gridlines (measure of time)
* @returns {object}
* - property `majorStep` {numeric}: space between major x-axis gridlines (measure of time)
* - property `minorStep` {numeric}: space between minor x-axis gridlines (measure of time)
*/
const calculateMajorGridSeperationForTime = (timeRange, pxAvailable) => {

export const calculateMajorGridSeperationForTime = (timeRange, pxAvailable) => {
const rountToNearest = (n, p) => Math.ceil(n/p)*p;

const getMinorSpacing = (majorTimeStep) => {
const getMinorSpacing = (majorStep) => {
const timesToTry = [1/365.25, 1/52, 1/12, 1, 10, 100, 1000];
for (const t of timesToTry) {
const n = majorTimeStep / t;
const n = majorStep / t;
// max number we allow is 12 (so that a major grid of a year can have minor grids of a month)
if (n <= 12) return t;
}
return majorTimeStep; // fallthrough. Only happens for _very_ large trees
return majorStep; // fallthrough. Only happens for _very_ large trees
};

/* in general, we find that 1 major point for every ~100px works well
for wider displays we shift up to 150px then 200px */
const nSteps = Math.floor(pxAvailable / (pxAvailable < 1200 ? 100 : 150)) || 1;

let majorTimeStep = timeRange / nSteps;
let majorStep = timeRange / nSteps;

/* For time views, it's nicer if the spacing is meaningful.
There's probably a better way to do this than cascading through levels */
if (majorTimeStep > 100) {
majorTimeStep = rountToNearest(majorTimeStep, 100);
} else if (majorTimeStep > 10) {
majorTimeStep = rountToNearest(majorTimeStep, 10);
} else if (majorTimeStep > 1) {
majorTimeStep = rountToNearest(majorTimeStep, 1);
} else if (majorTimeStep > (1/12)) {
if (majorStep > 100) {
majorStep = rountToNearest(majorStep, 100);
} else if (majorStep > 10) {
majorStep = rountToNearest(majorStep, 10);
} else if (majorStep > 1) {
majorStep = rountToNearest(majorStep, 1);
} else if (majorStep > (1/12)) {
/* each step is longer than a month, but shorter than a year */
majorTimeStep = rountToNearest(majorTimeStep, 1/12);
} else if (majorTimeStep > (1/52)) {
majorStep = rountToNearest(majorStep, 1/12);
} else if (majorStep > (1/52)) {
/* each step is longer than a week, but shorter than a month */
majorTimeStep = rountToNearest(majorTimeStep, 1/52);
} else if (majorTimeStep > (1/365.25)) {
majorStep = rountToNearest(majorStep, 1/52);
} else if (majorStep > (1/365.25)) {
/* each time step is longer than a day, but shorter than a week */
majorTimeStep = rountToNearest(majorTimeStep, 1/365.25);
majorStep = rountToNearest(majorStep, 1/365.25);
} else {
majorTimeStep = 1/365.25;
majorStep = 1/365.25;
}
const minorTimeStep = getMinorSpacing(majorTimeStep);
return [majorTimeStep, minorTimeStep];
const minorStep = getMinorSpacing(majorStep);
return {majorStep, minorStep};
};

/**
Expand All @@ -130,7 +131,7 @@ const calculateMajorGridSeperationForTime = (timeRange, pxAvailable) => {
* @param {numeric} numDate date in decimal format
* @returns {string} date to be displayed below major gridline
*/
const createDisplayDate = (step, numDate) => {
export const createDisplayDate = (step, numDate) => {
if (step >= 1) {
return numDate.toFixed(Math.max(0, -Math.floor(Math.log10(step))));
}
Expand All @@ -147,24 +148,24 @@ const computeXGridPoints = (xmin, xmax, layout, distanceMeasure, minorTicks, pxA
const minorGridPoints = [];

/* step is the amount (same units of xmax, xmin) of seperation between major grid lines */
const [step, minorStep] = distanceMeasure === "num_date" ?
const {majorStep, minorStep} = distanceMeasure === "num_date" ?
calculateMajorGridSeperationForTime(xmax-xmin, Math.abs(pxAvailable)) :
calculateMajorGridSeperationForDivergence(xmax-xmin, minorTicks);
const gridMin = Math.floor(xmin/step)*step;
const gridMin = Math.floor(xmin/majorStep)*majorStep;
const minVis = layout==="radial" ? xmin : gridMin;
const maxVis = xmax;

for (let ii = 0; ii <= (xmax - gridMin)/step+3; ii++) {
const pos = gridMin + step*ii;
for (let ii = 0; ii <= (xmax - gridMin)/majorStep+3; ii++) {
const pos = gridMin + majorStep*ii;
majorGridPoints.push({
position: pos,
name: distanceMeasure === "num_date" ?
createDisplayDate(step, pos) :
pos.toFixed(Math.max(0, -Math.floor(Math.log10(step)))),
createDisplayDate(majorStep, pos) :
pos.toFixed(Math.max(0, -Math.floor(Math.log10(majorStep)))),
visibility: ((pos<minVis) || (pos>maxVis)) ? "hidden" : "visible",
axis: "x"
});
for (let minorPos=pos+minorStep; minorPos<(pos+step) && minorPos<xmax; minorPos+=minorStep) {
for (let minorPos=pos+minorStep; minorPos<(pos+majorStep) && minorPos<xmax; minorPos+=minorStep) {
minorGridPoints.push({
position: minorPos,
visibility: ((minorPos<minVis) || (minorPos>maxVis+minorStep)) ? "hidden" : "visible",
Expand All @@ -179,7 +180,7 @@ const computeXGridPoints = (xmin, xmax, layout, distanceMeasure, minorTicks, pxA
const computeYGridPoints = (ymin, ymax) => {
const majorGridPoints = [];
let yStep = 0;
yStep = calculateMajorGridSeperationForDivergence(ymax-ymin)[0];
yStep = calculateMajorGridSeperationForDivergence(ymax-ymin).majorStep;
const precisionY = Math.max(0, -Math.floor(Math.log10(yStep)));
const gridYMin = Math.floor(ymin/yStep)*yStep;
const maxYVis = ymax;
Expand Down
3 changes: 1 addition & 2 deletions src/reducers/frequencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const frequencies = (state = {
loaded: false,
data: undefined,
pivots: undefined,
ticks: undefined,
matrix: undefined,
projection_pivot: undefined,
version: 0
Expand All @@ -18,7 +17,7 @@ const frequencies = (state = {
return Object.assign({}, state, {loaded: true, matrix: action.matrix, version: state.version + 1});
}
case types.DATA_INVALID: {
return {loaded: false, data: undefined, pivots: undefined, ticks: undefined, matrix: undefined, projection_pivot: undefined, version: 0};
return {loaded: false, data: undefined, pivots: undefined, matrix: undefined, projection_pivot: undefined, version: 0};
}
default:
return state;
Expand Down
6 changes: 0 additions & 6 deletions src/util/processFrequencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,6 @@ export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorS
export const processFrequenciesJSON = (rawJSON, tree, controls) => {
/* this function can throw */
const pivots = rawJSON.pivots.map((d) => Math.round(parseFloat(d) * 100) / 100);
const ticks = [pivots[0]];
const tick_step = (pivots[pivots.length - 1] - pivots[0]) / 6 * 10 / 10;
while (ticks[ticks.length - 1] < pivots[pivots.length - 1]) {
ticks.push((ticks[ticks.length - 1] + tick_step) * 10 / 10);
}
let projection_pivot = null;
if ("projection_pivot" in rawJSON) {
projection_pivot = Math.round(parseFloat(rawJSON.projection_pivot) * 100) / 100;
Expand Down Expand Up @@ -104,7 +99,6 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => {
return {
data,
pivots,
ticks,
matrix,
projection_pivot
};
Expand Down