From 9ad5ca0b50fb0e716d0bb74ed4b23bb5fd2d8ae2 Mon Sep 17 00:00:00 2001 From: mbucknell <mbucknell@usgs.gov> Date: Thu, 16 Jan 2020 16:13:49 -0600 Subject: [PATCH] Initial attempt to get zoom/brush graph on screen. There are layout and scaling issues but it does appear. --- .../scripts/components/hydrograph/audible.js | 4 +- .../src/scripts/components/hydrograph/axes.js | 28 ++--- .../scripts/components/hydrograph/cursor.js | 4 +- .../scripts/components/hydrograph/index.js | 5 +- .../scripts/components/hydrograph/layout.js | 20 +++- .../scripts/components/hydrograph/legend.js | 4 +- .../scripts/components/hydrograph/scales.js | 16 +-- .../hydrograph/time-series-graph.js | 108 ++++++++++-------- .../scripts/components/hydrograph/tooltip.js | 12 +- 9 files changed, 116 insertions(+), 85 deletions(-) diff --git a/assets/src/scripts/components/hydrograph/audible.js b/assets/src/scripts/components/hydrograph/audible.js index 83c236e37..f82351e8c 100644 --- a/assets/src/scripts/components/hydrograph/audible.js +++ b/assets/src/scripts/components/hydrograph/audible.js @@ -3,7 +3,7 @@ import { select } from 'd3-selection'; import memoize from 'fast-memoize'; import { createSelector, createStructuredSelector } from 'reselect'; import { tsCursorPointsSelector } from './cursor'; -import { xScaleSelector, yScaleSelector } from './scales'; +import { xScaleSelector, getYScale } from './scales'; import { allTimeSeriesSelector } from './time-series'; import config from '../../config'; import { dispatch, link } from '../../lib/redux'; @@ -73,7 +73,7 @@ export const updateSound = function ({enabled, points}) { const audibleInterfaceOnSelector = state => state.timeSeriesState.audiblePlayId !== null; const audibleScaleSelector = createSelector( - yScaleSelector, + getYScale(), (yScale) => { return scaleLinear() .domain(yScale.domain()) diff --git a/assets/src/scripts/components/hydrograph/axes.js b/assets/src/scripts/components/hydrograph/axes.js index 3b9cf6c77..b089eec36 100644 --- a/assets/src/scripts/components/hydrograph/axes.js +++ b/assets/src/scripts/components/hydrograph/axes.js @@ -1,14 +1,16 @@ import { axisBottom, axisLeft, axisRight } from 'd3-axis'; -import { createSelector } from 'reselect'; +import memoize from 'fast-memoize'; import { DateTime } from 'luxon'; -import { wrap, deltaDays } from '../../utils'; -import { getYTickDetails } from './domain'; -import { layoutSelector } from './layout'; -import { xScaleSelector, yScaleSelector, secondaryYScaleSelector } from './scales'; -import { yLabelSelector, secondaryYLabelSelector, tsTimeZoneSelector, TEMPERATURE_PARAMETERS } from './time-series'; +import { createSelector } from 'reselect'; + import config from '../../config'; import { getCurrentDateRange, getCurrentParmCd } from '../../selectors/time-series-selector'; -import { convertCelsiusToFahrenheit, convertFahrenheitToCelsius, mediaQuery } from '../../utils'; +import { convertCelsiusToFahrenheit, convertFahrenheitToCelsius, mediaQuery, wrap, deltaDays } from '../../utils'; + +import { getYTickDetails } from './domain'; +import { getLayout } from './layout'; +import { xScaleSelector, getYScale, getSecondaryYScale } from './scales'; +import { yLabelSelector, secondaryYLabelSelector, tsTimeZoneSelector, TEMPERATURE_PARAMETERS } from './time-series'; const FORMAT = { @@ -104,7 +106,7 @@ export const generateDateTicks = function(startDate, endDate, period, ianaTimeZo * @param {String} parmCd - parameter code of time series to be shown on the graph. * @param {String} period - ISO duration for date range of the time series * @param {String} ianaTimeZone - Internet Assigned Numbers Authority designation for a time zone - * @return {Object} {xAxis, yAxis} - D3 Axis + * @return {Object} {xAxis, yAxis, secondardYaxis} - D3 Axis */ export const createAxes = function({xScale, yScale, secondaryYScale}, yTickSize, parmCd, period, ianaTimeZone) { // Create x-axis @@ -158,11 +160,11 @@ export const createAxes = function({xScale, yScale, secondaryYScale}, yTickSize, * Returns data necessary to render the graph axes. * @return {Object} */ -export const axesSelector = createSelector( +export const getAxes = memoize(kind => createSelector( xScaleSelector('current'), - yScaleSelector, - secondaryYScaleSelector, - layoutSelector, + getYScale(kind), + getSecondaryYScale(kind), + getLayout(kind), yLabelSelector, tsTimeZoneSelector, getCurrentParmCd, @@ -182,7 +184,7 @@ export const axesSelector = createSelector( secondaryYTitle: plotSecondaryYLabel }; } -); +)); /** diff --git a/assets/src/scripts/components/hydrograph/cursor.js b/assets/src/scripts/components/hydrograph/cursor.js index bb360edf8..88c0137ff 100644 --- a/assets/src/scripts/components/hydrograph/cursor.js +++ b/assets/src/scripts/components/hydrograph/cursor.js @@ -8,7 +8,7 @@ import { dispatch, link } from '../../lib/redux'; import {getCurrentMethodID} from '../../selectors/time-series-selector'; import { currentVariablePointsByTsIdSelector } from './drawing-data'; -import { layoutSelector } from './layout'; +import { getMainLayout } from './layout'; import { xScaleSelector } from './scales'; import { isVisibleSelector } from './time-series'; @@ -146,7 +146,7 @@ export const cursorSlider = function (elem) { input.style('right', layout.margin.right - SLIDER_OFFSET_PX + 'px'); input.style('width', maxXScaleRange - (layout.margin.left + layout.margin.right) + SLIDER_OFFSET_PX * 2 + 'px'); }, createStructuredSelector( { - layout: layoutSelector, + layout: getMainLayout, xScale: xScaleSelector('current') }))); }); diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index 8954db138..3137a0061 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -15,7 +15,7 @@ import { cursorSlider } from './cursor'; import { drawDateRangeControls } from './date-controls'; import { lineSegmentsByParmCdSelector } from './drawing-data'; import { drawGraphControls } from './graph-controls'; -import { SPARK_LINE_DIM, layoutSelector } from './layout'; +import { SPARK_LINE_DIM } from './layout'; import { drawTimeSeriesLegend } from './legend'; import { drawLoadingIndicator } from '../loading-indicator'; import { drawMethodPicker } from './method-picker'; @@ -136,8 +136,7 @@ export const attachToNode = function (store, siteno: () => siteno, availableTimeSeries: availableTimeSeriesSelector, lineSegmentsByParmCd: lineSegmentsByParmCdSelector('current', 'P7D'), - timeSeriesScalesByParmCd: timeSeriesScalesByParmCdSelector('current', 'P7D', SPARK_LINE_DIM), - layout: layoutSelector + timeSeriesScalesByParmCd: timeSeriesScalesByParmCdSelector('current', 'P7D', SPARK_LINE_DIM) }))); nodeElem.select('.provisional-data-alert') .call(link(function(elem, allTimeSeries) { diff --git a/assets/src/scripts/components/hydrograph/layout.js b/assets/src/scripts/components/hydrograph/layout.js index b42b820fc..28536ece5 100644 --- a/assets/src/scripts/components/hydrograph/layout.js +++ b/assets/src/scripts/components/hydrograph/layout.js @@ -1,5 +1,7 @@ // Define constants for the time series graph's aspect ratio and margins as well as a // selector function which will return the width/height to use. + +import memoize from 'fast-memoize'; import { createSelector } from 'reselect'; import config from '../../config'; @@ -25,6 +27,9 @@ const MARGIN_SMALL_DEVICE = { export const CIRCLE_RADIUS = 4; export const CIRCLE_RADIUS_SINGLE_PT = 1; +const BRUSH_ZOOM_HEIGHT = 40; + + export const SPARK_LINE_DIM = { width: 60, height: 30 @@ -35,17 +40,18 @@ export const SPARK_LINE_DIM = { * @param {Object} state - Redux store * @return {Object} containing width and height properties. */ -export const layoutSelector = createSelector( +export const getLayout = memoize(kind => createSelector( (state) => state.ui.width, (state) => state.ui.windowWidth, tickSelector, (width, windowWidth, tickDetails) => { + const height = kind === 'ZOOM' ? BRUSH_ZOOM_HEIGHT : width * ASPECT_RATIO; const margin = mediaQuery(config.USWDS_SITE_MAX_WIDTH) ? MARGIN : MARGIN_SMALL_DEVICE; const tickLengths = tickDetails.tickValues.map(v => tickDetails.tickFormat(v).length); const approxLabelLength = Math.max(...tickLengths) * 10; return { width: width, - height: width * ASPECT_RATIO, + height: height, windowWidth: windowWidth, margin: { ...margin, @@ -54,4 +60,12 @@ export const layoutSelector = createSelector( } }; } -); +)); + +export const getMainLayout = getLayout(); +export const getZoomLayout = getLayout('ZOOM'); + +export const layoutZoomSelector = createSelector( + (state) => state.ui.width, + (state) => state.ui.windowWidth, +) diff --git a/assets/src/scripts/components/hydrograph/legend.js b/assets/src/scripts/components/hydrograph/legend.js index 3d7db8dc1..4e56dd40d 100644 --- a/assets/src/scripts/components/hydrograph/legend.js +++ b/assets/src/scripts/components/hydrograph/legend.js @@ -3,7 +3,7 @@ import { set } from 'd3-collection'; import memoize from 'fast-memoize'; import {createSelector, createStructuredSelector} from 'reselect'; -import {CIRCLE_RADIUS, layoutSelector} from './layout'; +import {CIRCLE_RADIUS, getMainLayout} from './layout'; import { defineLineMarker, defineTextOnlyMarker, defineRectangleMarker } from './markers'; import { currentVariableLineSegmentsSelector, HASH_ID, MASK_DESC } from './drawing-data'; import config from '../../config'; @@ -228,7 +228,7 @@ export const drawTimeSeriesLegend = function(elem) { .classed('hydrograph-container', true) .call(link(drawSimpleLegend, createStructuredSelector({ legendMarkerRows: legendMarkerRowsSelector, - layout: layoutSelector + layout: getMainLayout }))); }; diff --git a/assets/src/scripts/components/hydrograph/scales.js b/assets/src/scripts/components/hydrograph/scales.js index 6adb101bc..bb7ecd573 100644 --- a/assets/src/scripts/components/hydrograph/scales.js +++ b/assets/src/scripts/components/hydrograph/scales.js @@ -2,7 +2,7 @@ import { scaleLinear, scaleSymlog } from 'd3-scale'; import memoize from 'fast-memoize'; import { createSelector } from 'reselect'; import { getYDomain, SYMLOG_PARMS } from './domain'; -import { layoutSelector } from './layout'; +import { getLayout, getMainLayout } from './layout'; import { timeSeriesSelector, TEMPERATURE_PARAMETERS } from './time-series'; import { visiblePointsSelector, pointsByTsKeySelector } from './drawing-data'; import { getVariables, getCurrentParmCd, getRequestTimeRange } from '../../selectors/time-series-selector'; @@ -65,7 +65,7 @@ export const createYScale = function (parmCd, extent, size) { * @return {Function} D3 scale function */ export const xScaleSelector = memoize(tsKey => createSelector( - layoutSelector, + getMainLayout, getRequestTimeRange(tsKey), (layout, requestTimeRange) => { return createXScale(requestTimeRange, layout.width - layout.margin.right); @@ -78,18 +78,18 @@ export const xScaleSelector = memoize(tsKey => createSelector( * @param {Object} state Redux store * @return {Function} D3 scale function */ -export const yScaleSelector = createSelector( - layoutSelector, +export const getYScale = memoize(kind => createSelector( + getLayout(kind), visiblePointsSelector, getCurrentParmCd, (layout, pointArrays, currentVarParmCd) => { const yDomain = getYDomain(pointArrays, currentVarParmCd); return createYScale(currentVarParmCd, yDomain, layout.height - (layout.margin.top + layout.margin.bottom)); } -); +)); -export const secondaryYScaleSelector = createSelector( - layoutSelector, +export const getSecondaryYScale = memoize(kind => createSelector( + getLayout(kind), visiblePointsSelector, getCurrentParmCd, (layout, pointArrays, currentVarParmCd) => { @@ -106,7 +106,7 @@ export const secondaryYScaleSelector = createSelector( currentVarParmCd, convertedYDomain, layout.height - (layout.margin.top + layout.margin.bottom) ); } -); +)); /** diff --git a/assets/src/scripts/components/hydrograph/time-series-graph.js b/assets/src/scripts/components/hydrograph/time-series-graph.js index 1255ee18f..eed5677e7 100644 --- a/assets/src/scripts/components/hydrograph/time-series-graph.js +++ b/assets/src/scripts/components/hydrograph/time-series-graph.js @@ -4,7 +4,7 @@ import { line as d3Line, curveStepAfter } from 'd3-shape'; import {link} from '../../lib/redux'; import {addSVGAccessibility} from '../../accessibility'; -import {appendAxes, axesSelector} from './axes'; +import {appendAxes, getAxes} from './axes'; import config from '../../config'; import { currentVariableLineSegmentsSelector, @@ -12,15 +12,14 @@ import { HASH_ID, MASK_DESC } from './drawing-data'; -import {CIRCLE_RADIUS_SINGLE_PT, layoutSelector} from './layout'; +import {CIRCLE_RADIUS_SINGLE_PT, getMainLayout, getZoomLayout} from './layout'; import {createStructuredSelector} from 'reselect'; -import {xScaleSelector, yScaleSelector} from './scales'; +import {xScaleSelector, getYScale} from './scales'; import {descriptionSelector, isVisibleSelector, titleSelector} from './time-series'; import {getAgencyCode, getMonitoringLocationName} from '../../selectors/time-series-selector'; import {createTooltipFocus, createTooltipText} from './tooltip'; import {mediaQuery} from '../../utils'; - const plotDataLine = function(elem, {visible, lines, tsKey, xScale, yScale}) { if (!visible) { return; @@ -229,54 +228,71 @@ const watermark = function (elem) { // for Safari browser elem.style('-webkit-transform', transform); - }, layoutSelector)); + }, getMainLayout)); }; -export const drawTimeSeriesGraph = function(elem, siteNo, showMLName) { - elem.append('div') +export const drawTimeSeriesGraph = function(elem, brushZoomElem, siteNo, showMLName) { + const graphDiv = elem.append('div') .attr('class', 'hydrograph-container') .call(watermark) .call(createTitle, siteNo, showMLName) - .call(createTooltipText) - .append('svg') - .attr('xmlns', 'http://www.w3.org/2000/svg') - .classed('hydrograph-svg', true) - .call(link((elem, layout) => { + .call(createTooltipText); + graphDiv.append('svg') + .attr('xmlns', 'http://www.w3.org/2000/svg') + .classed('hydrograph-svg', true) + .call(link((elem, layout) => { + elem.attr('viewBox', `0 0 ${layout.width + layout.margin.left + layout.margin.right} ${layout.height + layout.margin.top + layout.margin.bottom}`); + elem.attr('width', layout.width); + elem.attr('height', layout.height); + }, getMainLayout)) + .call(link(addSVGAccessibility, createStructuredSelector({ + title: titleSelector, + description: descriptionSelector, + isInteractive: () => true, + idPrefix: () => 'hydrograph' + }))) + .call(plotSvgDefs) + .call(svg => { + svg.append('g') + .call(link((elem, layout) => elem.attr('transform', `translate(${layout.margin.left},${layout.margin.top})`), getMainLayout)) + .call(link(appendAxes, getAxes())) + .call(link(plotDataLines, createStructuredSelector({ + visible: isVisibleSelector('current'), + tsLinesMap: currentVariableLineSegmentsSelector('current'), + xScale: xScaleSelector('current'), + yScale: getYScale(), + tsKey: () => 'current' + }))) + .call(link(plotDataLines, createStructuredSelector({ + visible: isVisibleSelector('compare'), + tsLinesMap: currentVariableLineSegmentsSelector('compare'), + xScale: xScaleSelector('compare'), + yScale: getYScale(), + tsKey: () => 'compare' + }))) + .call(createTooltipFocus) + .call(link(plotAllMedianPoints, createStructuredSelector({ + visible: isVisibleSelector('median'), + xscale: xScaleSelector('current'), + yscale: getYScale(), + seriesPoints: getCurrentVariableMedianStatPoints + }))); + }); + + //Create brush/zoom context + graphDiv.append('svg') + .attr('xmlns', 'http://www.w3.org/2000/svg') + .call(link((elem, layout) => { elem.attr('viewBox', `0 0 ${layout.width + layout.margin.left + layout.margin.right} ${layout.height + layout.margin.top + layout.margin.bottom}`); elem.attr('width', layout.width); elem.attr('height', layout.height); - }, layoutSelector)) - .call(link(addSVGAccessibility, createStructuredSelector({ - title: titleSelector, - description: descriptionSelector, - isInteractive: () => true, - idPrefix: () => 'hydrograph' - }))) - .call(plotSvgDefs) - .call(svg => { - svg.append('g') - .call(link((elem, layout) => elem.attr('transform', `translate(${layout.margin.left},${layout.margin.top})`), layoutSelector)) - .call(link(appendAxes, axesSelector)) - .call(link(plotDataLines, createStructuredSelector({ - visible: isVisibleSelector('current'), - tsLinesMap: currentVariableLineSegmentsSelector('current'), - xScale: xScaleSelector('current'), - yScale: yScaleSelector, - tsKey: () => 'current' - }))) - .call(link(plotDataLines, createStructuredSelector({ - visible: isVisibleSelector('compare'), - tsLinesMap: currentVariableLineSegmentsSelector('compare'), - xScale: xScaleSelector('compare'), - yScale: yScaleSelector, - tsKey: () => 'compare' - }))) - .call(createTooltipFocus) - .call(link(plotAllMedianPoints, createStructuredSelector({ - visible: isVisibleSelector('median'), - xscale: xScaleSelector('current'), - yscale: yScaleSelector, - seriesPoints: getCurrentVariableMedianStatPoints - }))); - }); + }, getZoomLayout + )) + .call(svg => { + svg.append('g') + .call(link((elem, layout) => elem.attr('transform', `translate(${layout.margin.left},${layout.margin.top})`), + getZoomLayout + )) + .call(link(appendAxes, getAxes('ZOOM'))); + }); }; \ No newline at end of file diff --git a/assets/src/scripts/components/hydrograph/tooltip.js b/assets/src/scripts/components/hydrograph/tooltip.js index 5bfaa9cd7..2cbcd69e4 100644 --- a/assets/src/scripts/components/hydrograph/tooltip.js +++ b/assets/src/scripts/components/hydrograph/tooltip.js @@ -9,8 +9,8 @@ import { dispatch, link, initAndUpdate } from '../../lib/redux'; import { Actions } from '../../store'; import { cursorTimeSelector, tsCursorPointsSelector } from './cursor'; import { classesForPoint, MASK_DESC } from './drawing-data'; -import { layoutSelector } from './layout'; -import { xScaleSelector, yScaleSelector } from './scales'; +import { getMainLayout } from './layout'; +import { xScaleSelector, getYScale } from './scales'; import { tsTimeZoneSelector, TEMPERATURE_PARAMETERS } from './time-series'; import { getCurrentVariable, getCurrentParmCd } from '../../selectors/time-series-selector'; import config from '../../config'; @@ -55,7 +55,7 @@ const updateFocusLine = function(elem, {cursorTime, yScale, xScale}) { */ export const tooltipPointsSelector = memoize(tsKey => createSelector( xScaleSelector(tsKey), - yScaleSelector, + getYScale(), tsCursorPointsSelector(tsKey), (xScale, yScale, cursorPoints) => { return Object.keys(cursorPoints).reduce((tooltipPoints, tsID) => { @@ -218,7 +218,7 @@ export const createTooltipText = function (elem) { comparePoints: tsCursorPointsSelector('compare'), qualifiers: qualifiersSelector, unitCode: unitCodeSelector, - layout: layoutSelector, + layout: getMainLayout, ianaTimeZone: tsTimeZoneSelector, currentParmCd: getCurrentParmCd }))); @@ -268,7 +268,7 @@ const createFocusCircles = function (elem, tooltipPoints, circleContainer) { export const createTooltipFocus = function(elem) { elem.call(link(initAndUpdate(createFocusLine, updateFocusLine), createStructuredSelector({ xScale: xScaleSelector('current'), - yScale: yScaleSelector, + yScale: getYScale(), cursorTime: cursorTimeSelector('current') }))); @@ -303,6 +303,6 @@ export const createTooltipFocus = function(elem) { })); }, createStructuredSelector({ xScale: xScaleSelector('current'), - layout: layoutSelector + layout: getMainLayout }))); }; -- GitLab