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

[feature] Improve smoothness of temporalWindow animation over short time frames #920

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
6 changes: 3 additions & 3 deletions src/components/tree/phyloTree/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const modifySVG = function modifySVG(elemsToUpdate, svgPropsToUpdate, tra

/* background temporal time slice */
if (extras.timeSliceHasPotentiallyChanged) {
this.addTemporalSlice();
this.showTemporalSlice();
}

/* branch labels */
Expand Down Expand Up @@ -208,7 +208,7 @@ export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPr
this.drawTips();
this.updateTipLabels();
if (this.vaccines) this.drawVaccines();
this.addTemporalSlice();
this.showTemporalSlice();
if (this.layout === "clock" && this.distance === "num_date") this.drawRegression();
if (elemsToUpdate.has(".branchLabel")) this.drawBranchLabels(this.params.branchLabelKey);
};
Expand All @@ -230,7 +230,7 @@ export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPr
.remove()
.on("start", () => inProgress++)
.on("end", step2);
this.removeTemporalSlice();
this.hideTemporalSlice();
if (!transitionTimeFadeOut) timerFlush();
};

Expand Down
104 changes: 83 additions & 21 deletions src/components/tree/phyloTree/grid.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable space-infix-ops */
import { min, max } from "d3-array";
import { transition } from "d3-transition";
import { easeLinear } from "d3-ease";
import { timerStart, timerEnd } from "../../../util/perf";
import { months } from "../../../util/globals";
import { months, animationInterpolationDuration } from "../../../util/globals";
import { numericToCalendar } from "../../../util/dateHelpers";

export const hideGrid = function hideGrid() {
Expand All @@ -19,6 +21,12 @@ export const hideGrid = function hideGrid() {
const addSVGGroupsIfNeeded = (groups, svg) => {
if (!("temporalWindow" in groups)) {
groups.temporalWindow = svg.append("g").attr("id", "temporalWindow");

// Technically rects aren't groups, but store them to avoid searching for them on each "showTemporalSlice" render.
groups.temporalWindowStart = groups.temporalWindow.append('rect')
.attr('class', 'temporalWindowStart');
groups.temporalWindowEnd = groups.temporalWindow.append('rect')
.attr('class', 'temporalWindowEnd');
}
if (!("majorGrid" in groups)) {
groups.majorGrid = svg.append("g").attr("id", "majorGrid");
Expand Down Expand Up @@ -334,17 +342,24 @@ export const addGrid = function addGrid() {
timerEnd("addGrid");
};


export const removeTemporalSlice = function removeTemporalSlice() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removed function is still set as a prototype in phyloTree.js and called by the function modifySVGInStages. This will result in a complete app crash when (e.g.) the tree layout it changed

Copy link
Contributor Author

@hydrosquall hydrosquall Mar 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't realize this was used on the outside, I should have checked given that it was exported. I'll bring this back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed these to hideTemporalSlice to make it clear that no DOM is being removed, just that visibility is toggled.

this.groups.temporalWindow.selectAll("*").remove();
export const hideTemporalSlice = function hideTemporalSlice() {
this.groups.temporalWindowStart.attr('opacity', 0);
this.groups.temporalWindowEnd.attr('opacity', 0);
};

// d3-transition to ensure both rectangles move at the same rate
export const temporalWindowTransition = transition('temporalWindowTransition')
.duration(animationInterpolationDuration)
.ease(easeLinear); // the underlying animation uses linear interpolation, let's override the default easeCubic

/**
* add background grey rectangles to demarcate the temporal slice
*/
export const addTemporalSlice = function addTemporalSlice() {
this.removeTemporalSlice();
if (this.layout !== "rect" || this.distance !== "num_date") return;
export const showTemporalSlice = function showTemporalSlice() {
if (this.layout !== "rect" || this.distance !== "num_date") {
this.hideTemporalSlice();
return;
}

const xWindow = [this.xScale(this.dateRange[0]), this.xScale(this.dateRange[1])];
const height = this.yScale.range()[1];
Expand All @@ -357,25 +372,72 @@ export const addTemporalSlice = function addTemporalSlice() {

/* 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 */
this.groups.temporalWindow.append("rect")
.attr("x", rightHandTree ? xWindow[0] : 0)
.attr("width", rightHandTree ? totalWidth-xWindow[0]: xWindow[0])
.attr("y", 0)
let width_startRegion = xWindow[0];
let translateX_startRegion = 0;

// With right hand tree, the coordinate system flips (right to left)
if (rightHandTree) {
width_startRegion = totalWidth - xWindow[0];
translateX_startRegion = xWindow[0];
}

const wasStartRegionVisible = this.groups.temporalWindowStart.attr('opacity') === '1';

this.groups.temporalWindowStart
.attr('opacity', 1)
.attr("height", height)
.attr("transform", `translate(${translateX_startRegion},0)`)
.attr("fill", fill);

// Only apply animation if rectangle was already visible in the previous frame.
if (wasStartRegionVisible) {
this.groups.temporalWindowStart.transition('temporalWindowTransition')
.attr("width", width_startRegion);
} else {
this.groups.temporalWindowStart
.attr("width", width_startRegion);
}
} else {
this.groups.temporalWindowStart.attr('opacity', 0);
}

/* the gray region between the maximum selected date and the last tip */
const startingX = rightHandTree ? this.params.margins.right : xWindow[1];
const rectWidth = rightHandTree ?
xWindow[1]-this.params.margins.right :
totalWidth-this.params.margins.right-xWindow[1];
if (rectWidth > minPxThreshold) {
this.groups.temporalWindow.append("rect")
.attr("x", startingX)
.attr("width", rectWidth)
.attr("y", 0)
let xStart_endRegion = xWindow[1]; // starting X coordinate of the "end" rectangle
let width_endRegion = totalWidth - this.params.margins.right - xWindow[1];

let transform_endRegion = `translate(${totalWidth - this.params.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;
transform_endRegion = `translate(${xStart_endRegion},0)`;
}

if (width_endRegion > minPxThreshold) {
const wasEndRegionVisible = this.groups.temporalWindowEnd.attr('opacity') === '1';

this.groups.temporalWindowEnd
.attr('opacity', 1)
.attr("height", height)
.attr("fill", fill);
.attr("fill", fill)
.attr("transform", transform_endRegion);


// Only apply animation if rectangle was already visible in the previous frame.
// Unlike the startingRegion, this panel cannot depend
// on letting the SVG boundaries clip part of the rectangle.
// As a result, we'll have to animate width instead of position
// If performance becomes an issue, try add a custom clip-path with
// a fixed-width region instead.
if (wasEndRegionVisible) {
this.groups.temporalWindowEnd
.transition('temporalWindowTransition')
.attr("width", width_endRegion);
} else {
this.groups.temporalWindowEnd
.attr("width", width_endRegion);
}
} else {
this.groups.temporalWindowEnd.attr('opacity', 0);
}
};
4 changes: 2 additions & 2 deletions src/components/tree/phyloTree/phyloTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ PhyloTree.prototype.removeTipLabels = labels.removeTipLabels;
/* G R I D */
PhyloTree.prototype.hideGrid = grid.hideGrid;
PhyloTree.prototype.addGrid = grid.addGrid;
PhyloTree.prototype.addTemporalSlice = grid.addTemporalSlice;
PhyloTree.prototype.removeTemporalSlice = grid.removeTemporalSlice;
PhyloTree.prototype.showTemporalSlice = grid.showTemporalSlice;
PhyloTree.prototype.hideTemporalSlice = grid.hideTemporalSlice;

export default PhyloTree;
2 changes: 1 addition & 1 deletion src/components/tree/phyloTree/renderers.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const render = function render(svg, layout, distance, parameters, callbac
/* draw functions */
if (this.params.showGrid) {
this.addGrid();
this.addTemporalSlice();
this.showTemporalSlice();
}
this.drawBranches();
this.drawTips();
Expand Down
21 changes: 11 additions & 10 deletions src/util/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { scaleLinear, scaleSqrt } from "d3-scale";
import { hasExtension, getExtension } from "../util/extensions";

export const colorOptions = {
"country": {"key": "country", "legendTitle": "Country", "menuItem": "country", "type": "discrete"},
"region": {"key": "region", "legendTitle": "Region", "menuItem": "region", "type": "discrete"},
"num_date": {"key": "num_date", "legendTitle": "Sampling date", "menuItem": "date", "type": "continuous"},
"ep": {"key": "ep", "legendTitle": "Epitope Mutations", "menuItem": "epitope mutations", "type": "continuous"},
"ne": {"key": "ne", "legendTitle": "Non-epitope Mutations", "menuItem": "nonepitope mutations", "type": "continuous"},
"rb": {"key": "rb", "legendTitle": "Receptor Binding Mutations", "menuItem": "RBS mutations", "type": "continuous"},
"gt": {"key": "genotype", "legendTitle": "Genotype", "menuItem": "genotype", "type": "discrete"}
country: {key: "country", legendTitle: "Country", menuItem: "country", type: "discrete"},
region: {key: "region", legendTitle: "Region", menuItem: "region", type: "discrete"},
num_date: {key: "num_date", legendTitle: "Sampling date", menuItem: "date", type: "continuous"},
ep: {key: "ep", legendTitle: "Epitope Mutations", menuItem: "epitope mutations", type: "continuous"},
ne: {key: "ne", legendTitle: "Non-epitope Mutations", menuItem: "nonepitope mutations", type: "continuous"},
rb: {key: "rb", legendTitle: "Receptor Binding Mutations", menuItem: "RBS mutations", type: "continuous"},
gt: {key: "genotype", legendTitle: "Genotype", menuItem: "genotype", type: "discrete"}
};

/* static for now, then hand rolled version of https://github.com/digidem/react-dimensions */
Expand All @@ -29,7 +29,7 @@ export const defaultDistanceMeasure = "num_date";
export const defaultDateRange = 6;
export const date_select = true;
export const file_prefix = "Zika_";
export const restrictTo = {"region": "all"};
export const restrictTo = {region: "all"};
export const time_window = 3.0;
export const fullDataTimeWindow = 1.5;
export const time_ticks = [2013.0, 2013.5, 2014.0, 2014.5, 2015.0, 2015.5, 2016.0];
Expand Down Expand Up @@ -63,6 +63,7 @@ export const slowTransitionDuration = 1400; // in milliseconds
export const animationWindowWidth = 0.075; // width of animation window relative to date slider
export const minDistanceDateSlider = 0.075;
export const animationTick = 50; // animation tick in milliseconds
export const animationInterpolationDuration = 10; // how long d3 temporalWindow transitions last in milliseconds. Must be shorter than animationTick.
export const HIColorDomain = genericDomain.map((d) => {
return Math.round(100 * (d * 3.6)) / 100;
});
Expand Down Expand Up @@ -130,8 +131,8 @@ export const colors = [
["#511EA8", "#4928B4", "#4334BF", "#4041C7", "#3F50CC", "#3F5ED0", "#416CCE", "#4379CD", "#4784C7", "#4B8FC1", "#5098B9", "#56A0AF", "#5CA7A4", "#63AC99", "#6BB18E", "#73B583", "#7CB878", "#86BB6E", "#90BC65", "#9ABD5C", "#A4BE56", "#AFBD4F", "#B9BC4A", "#C2BA46", "#CCB742", "#D3B240", "#DAAC3D", "#DFA43B", "#E39B39", "#E68F36", "#E68234", "#E67431", "#E4632E", "#E1512A", "#DF4027", "#DC2F24"]
];

export const filterAbbrFwd = {"geo": "geographic location", "all": "all"};
export const filterAbbrRev = {"geographic location": "geo", "all": "all"};
export const filterAbbrFwd = {geo: "geographic location", all: "all"};
export const filterAbbrRev = {"geographic location": "geo", all: "all"};

export const titleColors = ["#4377CD", "#5097BA", "#63AC9A", "#7CB879", "#9ABE5C", "#B9BC4A", "#D4B13F", "#E49938", "#E67030", "#DE3C26"];
export const notificationDuration = 10000;
Expand Down