From 06bf163d5cb69e247c66cd38865a376ddb4568ad Mon Sep 17 00:00:00 2001 From: Trevor Bedford Date: Thu, 22 Aug 2019 19:17:42 -0700 Subject: [PATCH 1/4] frequencies: Include vertical line at projection_pivot --- src/components/frequencies/functions.js | 21 +++++++++++++++++++++ src/components/frequencies/index.js | 9 +++++++-- src/reducers/frequencies.js | 3 ++- src/util/processFrequencies.js | 7 ++++++- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/components/frequencies/functions.js b/src/components/frequencies/functions.js index 335d1db47..c92ab726a 100644 --- a/src/components/frequencies/functions.js +++ b/src/components/frequencies/functions.js @@ -74,6 +74,10 @@ const removeYAxis = (svg) => { svg.selectAll(".y.axis").remove(); }; +const removeProjectionPivot = (svg) => { + svg.selectAll(".projection-pivot").remove(); +}; + export const drawXAxis = (svg, chartGeom, scales) => { removeXAxis(svg); svg.append("g") @@ -83,6 +87,7 @@ export const drawXAxis = (svg, chartGeom, scales) => { .style("font-size", "12px") .call(axisBottom(scales.x).ticks(scales.numTicksX, ".1f")); }; + export const drawYAxis = (svg, chartGeom, scales) => { removeYAxis(svg); svg.append("g") @@ -93,6 +98,22 @@ export const drawYAxis = (svg, chartGeom, scales) => { .call(axisLeft(scales.y).ticks(scales.numTicksY)); }; +export const drawProjectionPivot = (svg, scales, projection_pivot) => { + if (projection_pivot) { + removeProjectionPivot(svg); + svg.append("g") + .attr("class", "projection-pivot") + .append("line") + .attr("x1", scales.x(parseFloat(projection_pivot))) + .attr("x2", scales.x(parseFloat(projection_pivot))) + .attr("y1", scales.y(1)) + .attr("y2", scales.y(0)) + .style("visibility", "visible") + .style("stroke", "#555") + .style("stroke-width", "2"); + } +}; + const turnMatrixIntoSeries = (categories, nPivots, matrix) => { /* WHAT IS A SERIES? diff --git a/src/components/frequencies/index.js b/src/components/frequencies/index.js index 70d50c037..812fad8dc 100644 --- a/src/components/frequencies/index.js +++ b/src/components/frequencies/index.js @@ -3,8 +3,8 @@ import { select } from "d3-selection"; import 'd3-transition' import { connect } from "react-redux"; import Card from "../framework/card"; -import { calcXScale, calcYScale, drawXAxis, drawYAxis, areListsEqual, - drawStream, processMatrix, parseColorBy } from "./functions"; +import { calcXScale, calcYScale, drawXAxis, drawYAxis, drawProjectionPivot, + areListsEqual, drawStream, processMatrix, parseColorBy } from "./functions"; import "../../css/entropy.css"; @connect((state) => { @@ -13,6 +13,7 @@ import "../../css/entropy.css"; pivots: state.frequencies.pivots, ticks: state.frequencies.ticks, matrix: state.frequencies.matrix, + projection_pivot: state.frequencies.projection_pivot, version: state.frequencies.version, browserDimensions: state.browserDimensions.browserDimensions, colorBy: state.controls.colorBy, @@ -40,6 +41,7 @@ class Frequencies extends React.Component { drawXAxis(newState.svg, chartGeom, scalesX); drawYAxis(newState.svg, chartGeom, scalesY); drawStream(newState.svgStreamGroup, newState.scales, data, {...props}); + drawProjectionPivot(newState.svg, newState.scales, props.projection_pivot); } recomputeRedrawPartial(oldState, oldProps, newProps) { /* we don't have to check width / height changes here - that's done in componentDidUpdate */ @@ -59,6 +61,9 @@ class Frequencies extends React.Component { } /* if !catChange we could transition the streams instead of redrawing them... */ drawStream(oldState.svgStreamGroup, newScales, data, {...newProps}); + if (maxYChange) { + drawProjectionPivot(oldState.svg, newScales, newProps.projection_pivot); + } return {...oldState, scales: newScales, maxY: data.maxY, categories: data.categories}; } componentDidMount() { diff --git a/src/reducers/frequencies.js b/src/reducers/frequencies.js index 3816607d1..4f4e7b904 100644 --- a/src/reducers/frequencies.js +++ b/src/reducers/frequencies.js @@ -7,6 +7,7 @@ const frequencies = (state = { pivots: undefined, ticks: undefined, matrix: undefined, + projection_pivot: undefined, version: 0 }, action) => { switch (action.type) { @@ -17,7 +18,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, version: 0}; + return {loaded: false, data: undefined, pivots: undefined, ticks: undefined, matrix: undefined, projection_pivot: undefined, version: 0}; } default: return state; diff --git a/src/util/processFrequencies.js b/src/util/processFrequencies.js index 71188bbed..2719e7d88 100644 --- a/src/util/processFrequencies.js +++ b/src/util/processFrequencies.js @@ -74,6 +74,10 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => { 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; + } if (!tree.loaded) { throw new Error("tree not loaded"); } @@ -101,6 +105,7 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => { data, pivots, ticks, - matrix + matrix, + projection_pivot }; }; From bb3c430297235821f1a13c1416b108a3bed225a5 Mon Sep 17 00:00:00 2001 From: Trevor Bedford Date: Thu, 22 Aug 2019 19:52:54 -0700 Subject: [PATCH 2/4] frequencies: Improve tooltip behavior and aesthetics This commit: 1. Makes the vertical tooltip line follow the frequency bin rather than taking up the entire vertical. 2. Makes the tooltip aligned with the pivot rather than the mouse point. This prevents the tooltip from occluding the marker line. 3. Makes the tooltip styling match the dark background / white text used in tree tooltips. This increases aesthetic consistency. --- src/components/frequencies/functions.js | 42 ++++++++++++++++--------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/components/frequencies/functions.js b/src/components/frequencies/functions.js index c92ab726a..d194f2fa6 100644 --- a/src/components/frequencies/functions.js +++ b/src/components/frequencies/functions.js @@ -109,8 +109,9 @@ export const drawProjectionPivot = (svg, scales, projection_pivot) => { .attr("y1", scales.y(1)) .attr("y2", scales.y(0)) .style("visibility", "visible") - .style("stroke", "#555") - .style("stroke-width", "2"); + .style("stroke", "rgba(55,55,55,0.9)") + .style("stroke-width", "2") + .style("stroke-dasharray", "4 4"); } }; @@ -237,20 +238,35 @@ export const drawStream = ( const date = scales.x.invert(mousex); const pivotIdx = pivots.reduce((closestIdx, val, idx, arr) => Math.abs(val - date) < Math.abs(arr[closestIdx] - date) ? idx : closestIdx, 0); const freqVal = Math.round((d[pivotIdx][1] - d[pivotIdx][0]) * 100) + "%"; - const xvalueOfPivot = scales.x(pivots[pivotIdx]); + const xValueOfPivot = scales.x(pivots[pivotIdx]); + const y1ValueOfPivot = scales.y(d[pivotIdx][1]); + const y2ValueOfPivot = scales.y(d[pivotIdx][0]); select("#vline") .style("visibility", "visible") - .attr("x1", xvalueOfPivot) - .attr("x2", xvalueOfPivot); - - const left = mousex > 0.5 * scales.x.range()[1] ? "" : `${mousex + 4}px`; - const right = mousex > 0.5 * scales.x.range()[1] ? `${scales.x.range()[1] - mousex - 4}px` : ""; + .attr("x1", xValueOfPivot) + .attr("x2", xValueOfPivot) + .attr("y1", y1ValueOfPivot) + .attr("y2", y2ValueOfPivot); + + const left = xValueOfPivot > 0.5 * scales.x.range()[1] ? "" : `${xValueOfPivot + 25}px`; + const right = xValueOfPivot > 0.5 * scales.x.range()[1] ? `${scales.x.range()[1] - xValueOfPivot + 25}px` : ""; + const top = y1ValueOfPivot > 0.5 * scales.y(0) ? `${scales.y(0) - 50}px` : `${y1ValueOfPivot + 25}px`; select("#freqinfo") .style("left", left) .style("right", right) - .style("top", `${70}px`) + .style("top", top) + .style("padding-left", "10px") + .style("padding-right", "10px") + .style("padding-top", "0px") + .style("padding-bottom", "0px") .style("visibility", "visible") + .style("background-color", "rgba(55,55,55,0.9)") + .style("color", "white") + .style("font-family", "Lato, Helvetica Neue, Helvetica, sans-serif") + .style("font-size", 18) + .style("line-height", 1) + .style("font-weight", 300) .html(`

${parseColorBy(colorBy, colorOptions)}: ${prettyString(labels[i])}

Time point: ${pivots[pivotIdx]}

Frequency: ${freqVal}

`); @@ -269,15 +285,13 @@ export const drawStream = ( .on("mouseout", handleMouseOut) .on("mousemove", handleMouseMove); - /* the vertical line to indicate the pivot point */ + /* the vertical line to indicate the highlighted frequency interval */ svgStreamGroup.append("line") .attr("id", "vline") - .attr("y1", scales.y(1)) - .attr("y2", scales.y(0)) .style("visibility", "hidden") .style("pointer-events", "none") - .style("stroke", "hsla(0,0%,100%,.9)") - .style("stroke-width", "5"); + .style("stroke", "rgba(55,55,55,0.9)") + .style("stroke-width", 4); drawLabelsOverStream(svgStreamGroup, series, pivots, labels, scales); }; From e669c7570e451a0262301f2a5dbc201a05f88f57 Mon Sep 17 00:00:00 2001 From: Trevor Bedford Date: Wed, 28 Aug 2019 11:52:05 +0800 Subject: [PATCH 3/4] Better call out frequency projection This commit updates two aspects of projection frequencies: 1. Adds a "Projection" label centered over the projected portion of the frequencies streamgraph 2. Changes tooltip labeling to "Projected frequency" when appropriate --- src/components/frequencies/functions.js | 38 +++++++++++++++++++++---- src/components/frequencies/index.js | 16 ++++++++--- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/components/frequencies/functions.js b/src/components/frequencies/functions.js index d194f2fa6..9df154922 100644 --- a/src/components/frequencies/functions.js +++ b/src/components/frequencies/functions.js @@ -74,8 +74,9 @@ const removeYAxis = (svg) => { svg.selectAll(".y.axis").remove(); }; -const removeProjectionPivot = (svg) => { +const removeProjectionInfo = (svg) => { svg.selectAll(".projection-pivot").remove(); + svg.selectAll(".projection-text").remove(); }; export const drawXAxis = (svg, chartGeom, scales) => { @@ -98,9 +99,11 @@ export const drawYAxis = (svg, chartGeom, scales) => { .call(axisLeft(scales.y).ticks(scales.numTicksY)); }; -export const drawProjectionPivot = (svg, scales, projection_pivot) => { +export const drawProjectionInfo = (svg, scales, projection_pivot) => { if (projection_pivot) { - removeProjectionPivot(svg); + + removeProjectionInfo(svg); + svg.append("g") .attr("class", "projection-pivot") .append("line") @@ -112,6 +115,21 @@ export const drawProjectionPivot = (svg, scales, projection_pivot) => { .style("stroke", "rgba(55,55,55,0.9)") .style("stroke-width", "2") .style("stroke-dasharray", "4 4"); + + const midPoint = 0.5 * (scales.x(parseFloat(projection_pivot)) + scales.x.range()[1]); + svg.append("g") + .attr("class", "projection-text") + .append("text") + .attr("x", midPoint) + .attr("y", scales.y(1) - 3) + .style("pointer-events", "none") + .style("fill", "#555") + .style("font-family", dataFont) + .style("font-size", 12) + .style("alignment-baseline", "bottom") + .style("text-anchor", "middle") + .text("Projection"); + } }; @@ -219,7 +237,7 @@ export const processMatrix = ({matrix, pivots, colorScale}) => { }; export const drawStream = ( - svgStreamGroup, scales, {categories, series}, {colorBy, colorScale, colorOptions, pivots} + svgStreamGroup, scales, {categories, series}, {colorBy, colorScale, colorOptions, pivots, projection_pivot} ) => { removeStream(svgStreamGroup); const colourer = generateColorScaleD3(categories, colorScale); @@ -252,6 +270,14 @@ export const drawStream = ( const left = xValueOfPivot > 0.5 * scales.x.range()[1] ? "" : `${xValueOfPivot + 25}px`; const right = xValueOfPivot > 0.5 * scales.x.range()[1] ? `${scales.x.range()[1] - xValueOfPivot + 25}px` : ""; const top = y1ValueOfPivot > 0.5 * scales.y(0) ? `${scales.y(0) - 50}px` : `${y1ValueOfPivot + 25}px`; + + let frequencyText = "Frequency"; + if (projection_pivot) { + if (pivots[pivotIdx] > projection_pivot) { + frequencyText = "Projected frequency"; + } + } + select("#freqinfo") .style("left", left) .style("right", right) @@ -263,13 +289,13 @@ export const drawStream = ( .style("visibility", "visible") .style("background-color", "rgba(55,55,55,0.9)") .style("color", "white") - .style("font-family", "Lato, Helvetica Neue, Helvetica, sans-serif") + .style("font-family", dataFont) .style("font-size", 18) .style("line-height", 1) .style("font-weight", 300) .html(`

${parseColorBy(colorBy, colorOptions)}: ${prettyString(labels[i])}

Time point: ${pivots[pivotIdx]}

-

Frequency: ${freqVal}

`); +

${frequencyText}: ${freqVal}

`); } diff --git a/src/components/frequencies/index.js b/src/components/frequencies/index.js index 812fad8dc..5a78a0a42 100644 --- a/src/components/frequencies/index.js +++ b/src/components/frequencies/index.js @@ -3,7 +3,7 @@ import { select } from "d3-selection"; import 'd3-transition' import { connect } from "react-redux"; import Card from "../framework/card"; -import { calcXScale, calcYScale, drawXAxis, drawYAxis, drawProjectionPivot, +import { calcXScale, calcYScale, drawXAxis, drawYAxis, drawProjectionInfo, areListsEqual, drawStream, processMatrix, parseColorBy } from "./functions"; import "../../css/entropy.css"; @@ -41,7 +41,7 @@ class Frequencies extends React.Component { drawXAxis(newState.svg, chartGeom, scalesX); drawYAxis(newState.svg, chartGeom, scalesY); drawStream(newState.svgStreamGroup, newState.scales, data, {...props}); - drawProjectionPivot(newState.svg, newState.scales, props.projection_pivot); + drawProjectionInfo(newState.svg, newState.scales, props.projection_pivot); } recomputeRedrawPartial(oldState, oldProps, newProps) { /* we don't have to check width / height changes here - that's done in componentDidUpdate */ @@ -62,7 +62,7 @@ class Frequencies extends React.Component { /* if !catChange we could transition the streams instead of redrawing them... */ drawStream(oldState.svgStreamGroup, newScales, data, {...newProps}); if (maxYChange) { - drawProjectionPivot(oldState.svg, newScales, newProps.projection_pivot); + drawProjectionInfo(oldState.svg, newScales, newProps.projection_pivot); } return {...oldState, scales: newScales, maxY: data.maxY, categories: data.categories}; } @@ -113,7 +113,15 @@ class Frequencies extends React.Component { fontSize: "14px" }} /> - + { this.domRef = c; }} id="d3frequencies"/> From ae29fe3c7f6a53d3b60223865aac02c3b268648a Mon Sep 17 00:00:00 2001 From: Trevor Bedford Date: Wed, 28 Aug 2019 21:39:17 +0800 Subject: [PATCH 4/4] Format frequency y axis as percentage --- src/components/frequencies/functions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/frequencies/functions.js b/src/components/frequencies/functions.js index 9df154922..feecfdb8e 100644 --- a/src/components/frequencies/functions.js +++ b/src/components/frequencies/functions.js @@ -4,6 +4,7 @@ import { scaleLinear } from "d3-scale"; import { axisBottom, axisLeft } from "d3-axis"; import { rgb } from "d3-color"; import { area } from "d3-shape"; +import { format } from "d3-format"; import { dataFont } from "../../globalStyles"; import { prettyString } from "../../util/stringHelpers"; import { unassigned_label } from "../../util/processFrequencies"; @@ -91,12 +92,13 @@ export const drawXAxis = (svg, chartGeom, scales) => { export const drawYAxis = (svg, chartGeom, scales) => { removeYAxis(svg); + const formatPercent = format(".0%"); svg.append("g") .attr("class", "y axis") .attr("transform", `translate(${chartGeom.spaceLeft},0)`) .style("font-family", dataFont) .style("font-size", "12px") - .call(axisLeft(scales.y).ticks(scales.numTicksY)); + .call(axisLeft(scales.y).ticks(scales.numTicksY).tickFormat(formatPercent)); }; export const drawProjectionInfo = (svg, scales, projection_pivot) => {