diff --git a/app/assets/graphics/content/east_coast_mar_20.jpg b/app/assets/graphics/content/east_coast_mar_20.jpg new file mode 100644 index 00000000..8a3ed7b3 Binary files /dev/null and b/app/assets/graphics/content/east_coast_mar_20.jpg differ diff --git a/app/assets/graphics/content/east_coast_mar_avg.jpg b/app/assets/graphics/content/east_coast_mar_avg.jpg new file mode 100644 index 00000000..ed1bd6f1 Binary files /dev/null and b/app/assets/graphics/content/east_coast_mar_avg.jpg differ diff --git a/app/assets/graphics/content/hubei_vir_after.png b/app/assets/graphics/content/hubei_vir_after.png new file mode 100644 index 00000000..cced58e4 Binary files /dev/null and b/app/assets/graphics/content/hubei_vir_after.png differ diff --git a/app/assets/graphics/content/hubei_vir_before.png b/app/assets/graphics/content/hubei_vir_before.png new file mode 100644 index 00000000..ffa5c6b3 Binary files /dev/null and b/app/assets/graphics/content/hubei_vir_before.png differ diff --git a/app/assets/graphics/content/no2_south_america.png b/app/assets/graphics/content/no2_south_america.png new file mode 100644 index 00000000..1e239b62 Binary files /dev/null and b/app/assets/graphics/content/no2_south_america.png differ diff --git a/app/assets/graphics/content/wuhan_bef_after.png b/app/assets/graphics/content/wuhan_bef_after.png new file mode 100644 index 00000000..987f4d70 Binary files /dev/null and b/app/assets/graphics/content/wuhan_bef_after.png differ diff --git a/app/assets/scripts/components/about/index.js b/app/assets/scripts/components/about/index.js index c5bf9379..23485e1b 100644 --- a/app/assets/scripts/components/about/index.js +++ b/app/assets/scripts/components/about/index.js @@ -10,19 +10,22 @@ import { InpageTitle, InpageBody } from '../../styles/inpage'; +import Constrainer from '../../styles/constrainer'; +import Prose from '../../styles/type/prose'; -import { - Fold, - FoldTitle -} from '../../styles/fold'; +import { glsp } from '../../styles/utils/theme-values'; -import Constrainer from '../../styles/constrainer'; +const PageConstrainer = styled(Constrainer)` + padding-top: ${glsp(4)}; + padding-bottom: ${glsp(4)}; -import Prose from '../../styles/type/prose'; + ${Prose} { + max-width: 50rem; + } -const AboutProse = styled(Prose)` - grid-row: 1; - grid-column: span 8; + > *:not(:last-child) { + margin-bottom: ${glsp(2)}; + } `; export default class About extends React.Component { @@ -38,18 +41,37 @@ export default class About extends React.Component { - - - - The tool -

- The COVID EO dashbord provides streaming data about the - pandemic to inform decisionmakers in government, community - leaders, health responders, and the business community. -

-
-
-
+ + +

+ As the world moved indoors to shelter from the global pandemic sparked by the novel coronavirus, we + could perceive changes on our planet. The sky seemed a little bluer, the air a little fresher, the animals in + our yards more abundant. +

+

+ NASA’s continuous and sometimes near-real-time measurements of Earth allow for understanding both the systems + changes themselves and the potential impact on economies and society during the pandemic – and as the world + slowly returns to operations. +

+

+ This dashboard features data collected and analyzed by the National Aeronautics and Space Administration (NASA). + Information about Earth systems is gathered by a fleet of powerful global Earth-Observing satellites, instruments + aboard the International Space Station, from airborne science campaigns, and via ground observations. With this + data we have been able to monitor some of those changes andand this is allowing us to track and compare these + changes over time. +

+

+ NASA will further expand the impact of the data sets presented in this dashboard in collaboration with + European Space Agency (ESA) and the Japan Aerospace Exploration Agency (JAXA), who also have created their own + online data portals to provide an even richer picture of what is happening on our home planet during this time of + world crisis by developing a fourth dashboard that will combine the unique data of each agency for more comparisons + and deeper understandings now and over time. +

+

+ To learn more about NASA Earth Science, go to nasa.gov/earth. +

+
+
diff --git a/app/assets/scripts/components/common/accordion.js b/app/assets/scripts/components/common/accordion.js index 26f5b57c..8850e1d1 100644 --- a/app/assets/scripts/components/common/accordion.js +++ b/app/assets/scripts/components/common/accordion.js @@ -165,7 +165,7 @@ function AccordionFoldCmp ({ ); } AccordionFoldCmp.propTypes = { - as: T.node, + as: T.any, id: T.string, className: T.string, isFoldExpanded: T.bool, diff --git a/app/assets/scripts/components/common/app.js b/app/assets/scripts/components/common/app.js index c08c60b0..72392d2d 100644 --- a/app/assets/scripts/components/common/app.js +++ b/app/assets/scripts/components/common/app.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import T from 'prop-types'; +import { withRouter } from 'react-router'; import styled from 'styled-components'; import MetaTags from './meta-tags'; @@ -38,6 +39,17 @@ class App extends Component { this.resizeListener = this.resizeListener.bind(this); } + componentDidMount () { + window.scrollTo(0, 0); + } + + // Handle cases where the page is updated without changing + componentDidUpdate (prevProps) { + if (this.props.location && this.props.location.pathname !== prevProps.location.pathname) { + window.scrollTo(0, 0); + } + } + resizeListener ({ width }) { this.setState({ useShortTitle: width < mediaRanges.small[0] @@ -63,7 +75,8 @@ class App extends Component { App.propTypes = { pageTitle: T.string, - children: T.node + children: T.node, + location: T.object }; -export default App; +export default withRouter(App); diff --git a/app/assets/scripts/components/common/data-browser/bisector.layer.js b/app/assets/scripts/components/common/data-browser/bisector.layer.js index 0e9e376a..ae240635 100644 --- a/app/assets/scripts/components/common/data-browser/bisector.layer.js +++ b/app/assets/scripts/components/common/data-browser/bisector.layer.js @@ -10,19 +10,26 @@ import { } from 'date-fns'; import { themeVal } from '../../../styles/utils/general'; +import { utcDate, bisectByDate } from '../../../utils/utils'; -const roundDate = (date, interval) => { - if (interval === 'day') { - const h = getHours(date); - return h >= 12 - ? startOfDay(add(date, { days: 1 })) - : startOfDay(date); +const getClosestDate = (data, date, timeUnit) => { + // If we're working with a discrete domain, get the closest value. + if (data.length > 2) { + return bisectByDate(data, date, d => utcDate(d)); } else { - const days = getDaysInMonth(date); - const d = getDate(date); - return d >= days / 2 - ? startOfMonth(add(date, { months: 1 })) - : startOfMonth(date); + // If we only have start and end, round based on time unit. + if (timeUnit === 'day') { + const h = getHours(date); + return h >= 12 + ? startOfDay(add(date, { days: 1 })) + : startOfDay(date); + } else { + const days = getDaysInMonth(date); + const d = getDate(date); + return d >= days / 2 + ? startOfMonth(add(date, { months: 1 })) + : startOfMonth(date); + } } }; @@ -48,8 +55,6 @@ const styles = props => css` export default { styles, init: ctx => { - const { timeUnit } = ctx.props; - const bisectorG = ctx.dataCanvas .append('g') .attr('class', 'bisector'); @@ -67,7 +72,7 @@ export default { .style('pointer-events', 'all') .on('mouseover', function () { const xPos = d3.mouse(this)[0]; - const date = roundDate(ctx.xScale.invert(xPos), timeUnit); + const date = getClosestDate(ctx.props.xDomain, ctx.xScale.invert(xPos), ctx.props.timeUnit); const xPosSnap = ctx.xScale(date); bisectorG.select('.bisector-interact').style('display', ''); ctx.onInternalAction('bisector.show', { date, x: xPosSnap }); @@ -78,7 +83,7 @@ export default { }) .on('mousemove', function () { const xPos = d3.mouse(this)[0]; - const date = roundDate(ctx.xScale.invert(xPos), timeUnit); + const date = getClosestDate(ctx.props.xDomain, ctx.xScale.invert(xPos), ctx.props.timeUnit); const xPosSnap = ctx.xScale(date); const { height } = ctx.getSize(); bisectorG.select('.bisector-interact') @@ -90,7 +95,7 @@ export default { }) .on('click', function () { const xPos = d3.mouse(this)[0]; - const date = roundDate(ctx.xScale.invert(xPos), timeUnit); + const date = getClosestDate(ctx.props.xDomain, ctx.xScale.invert(xPos), ctx.props.timeUnit); ctx.props.onAction('date.set', { date }); }); }, diff --git a/app/assets/scripts/components/common/data-browser/chart.js b/app/assets/scripts/components/common/data-browser/chart.js index ce1f3bc2..2b985a10 100644 --- a/app/assets/scripts/components/common/data-browser/chart.js +++ b/app/assets/scripts/components/common/data-browser/chart.js @@ -58,7 +58,7 @@ const formatDate = (date, interval) => { if (interval === 'day') { return format(date, 'dd MMMM yyyy'); } else { - return format(date, "MMMM yy''"); + return format(date, 'MMMM yyyy'); } }; @@ -165,9 +165,14 @@ class DataBrowserChart extends React.Component { // --------------------------------------------------- // Functions + const xDomain = [ + props.xDomain[0], + props.xDomain[props.xDomain.length - 1] + ]; + this.xScale = d3 .scaleTime() - .domain(props.xDomain) + .domain(xDomain) .range([0, width]); // this.yScale = d3 diff --git a/app/assets/scripts/components/common/data-browser/data-extent.layer.js b/app/assets/scripts/components/common/data-browser/data-extent.layer.js index a3707aaa..8751d4a7 100644 --- a/app/assets/scripts/components/common/data-browser/data-extent.layer.js +++ b/app/assets/scripts/components/common/data-browser/data-extent.layer.js @@ -10,6 +10,9 @@ const styles = props => css` stroke: ${props.swatch}; stroke-linecap: round; } + .point { + fill: ${props.swatch}; + } } `; @@ -21,27 +24,54 @@ export default { }, update: ctx => { - const { dataCanvas, xScale } = ctx; + const { dataCanvas, xScale, props } = ctx; const { height } = ctx.getSize(); - // Limit data to existing date domain. - const dateDomain = xScale.domain(); + const dataSeries = dataCanvas.select('.data-extent'); - const lines = dataSeries.selectAll('.line').data([dateDomain]); - - // Remove old. - lines.exit().remove(); - // Handle new. - lines - .enter() - .append('line') - .attr('class', 'line') - .attr('fill', 'none') - .merge(lines) - // Update current. - .attr('x1', d => xScale(utcDate(d[0]))) - .attr('y1', height - 8) - .attr('x2', d => xScale(utcDate(d[1]))) - .attr('y2', height - 8); + const isDiscrete = props.xDomain.length > 2; + + // When we have more than 2 date points, we are showing the individual dates + // that are available. + // These are represented as circles. Continuous data is represented as + // a line. + if (isDiscrete) { + dataSeries.selectAll('.line').style('display', 'none'); + const points = dataSeries.selectAll('.point').data(props.xDomain); + + // Remove old. + points.exit().remove(); + // Handle new. + points + .enter() + .append('circle') + .attr('r', 4) + .attr('class', 'point') + .merge(points) + // Update current. + .style('display', '') + .attr('cx', d => xScale(utcDate(d))) + .attr('cy', d => height - 8); + } else { + dataSeries.selectAll('.poin').style('display', 'none'); + const dateDomain = xScale.domain(); + const lines = dataSeries.selectAll('.line').data([dateDomain]); + + // Remove old. + lines.exit().remove(); + // Handle new. + lines + .enter() + .append('line') + .attr('class', 'line') + .attr('fill', 'none') + .merge(lines) + // Update current. + .style('display', '') + .attr('x1', d => xScale(utcDate(d[0]))) + .attr('y1', height - 8) + .attr('x2', d => xScale(utcDate(d[1]))) + .attr('y2', height - 8); + } } }; diff --git a/app/assets/scripts/components/common/data-browser/xaxis.layer.js b/app/assets/scripts/components/common/data-browser/xaxis.layer.js index e153d7f8..d1676cdb 100644 --- a/app/assets/scripts/components/common/data-browser/xaxis.layer.js +++ b/app/assets/scripts/components/common/data-browser/xaxis.layer.js @@ -18,7 +18,7 @@ export default { const { height } = ctx.getSize(); const xAxis = d3fc.axisBottom(xScale) - .tickFormat(d3.timeFormat('%b %y\'')); + .tickFormat(d3.timeFormat('%b \'%y')); svg.select('.x.axis') .attr('transform', `translate(${left},${height + top + 8})`) diff --git a/app/assets/scripts/components/common/layers/index.js b/app/assets/scripts/components/common/layers/index.js index b59ae0dd..dc446e28 100644 --- a/app/assets/scripts/components/common/layers/index.js +++ b/app/assets/scripts/components/common/layers/index.js @@ -1,11 +1,13 @@ import no2 from './layer-no2'; import population from './layer-population'; import carCount from './layer-car-count'; -import nightlights from './layer-nightlights'; +import nightlightsViirs from './layer-nightlights-viirs'; +import nightlightsHd from './layer-nightlights-hd'; export default [ no2, population, carCount, - nightlights + nightlightsViirs, + nightlightsHd ]; diff --git a/app/assets/scripts/components/common/layers/layer-car-count.js b/app/assets/scripts/components/common/layers/layer-car-count.js index fe538847..7f07927b 100644 --- a/app/assets/scripts/components/common/layers/layer-car-count.js +++ b/app/assets/scripts/components/common/layers/layer-car-count.js @@ -3,18 +3,20 @@ import config from '../../../config'; export default { id: 'car-count', name: 'Car count', - type: 'raster', + type: 'raster-timeseries', + domain: ['2019-12-10', '2020-01-05', '2020-01-12', '2020-01-19', '2020-01-24', '2020-01-29', '2020-02-05', '2020-02-10', '2020-02-17', '2020-02-18', '2020-02-22', '2020-03-05', '2020-03-12', '2020-03-17', '2020-03-24', '2020-03-29', '2020-03-31', '2020-04-05', '2020-04-17', '2020-04-28', '2020-04-29', '2020-05-07'], + timeUnit: 'day', source: { type: 'raster', tiles: [ - `${config.api}/{z}/{x}/{y}@1x?url=s3://covid-eo-data/ALOS_SAMPLE/alos2-s1-beijing_2019_12_10.tif&resampling_method=nearest&bidx=1&rescale=0%2C65536` + `${config.api}/{z}/{x}/{y}@1x?url=s3://covid-eo-data/ALOS_SAMPLE/alos2-s1-beijing_{date}.tif&resampling_method=nearest&bidx=1&rescale=1%2C65536` ] }, - exclusiveWith: ['no2'], - // swatch: { - // color: '#F55E2C', - // name: 'Orange' - // }, + exclusiveWith: ['no2', 'gibs-population', 'nightlights-viirs', 'nightlights-hd'], + swatch: { + color: '#666666', + name: 'Grey' + }, legend: { type: 'gradient', min: 'less', diff --git a/app/assets/scripts/components/common/layers/layer-nightlights-hd.js b/app/assets/scripts/components/common/layers/layer-nightlights-hd.js new file mode 100644 index 00000000..a8352778 --- /dev/null +++ b/app/assets/scripts/components/common/layers/layer-nightlights-hd.js @@ -0,0 +1,36 @@ +import config from '../../../config'; + +export default { + id: 'nightlights-hd', + name: 'Nightlights HD', + type: 'raster-timeseries', + timeUnit: 'month', + domain: [ + '2020-01-01', + '2020-05-01' + ], + source: { + type: 'raster', + tiles: [ + `${config.api}/{z}/{x}/{y}@1x?url=s3://covid-eo-data/BMHD_30M_MONTHLY/BMHD_VNP46A2_{spotlightId}_{date}_cog.tif&resampling_method=bilinear&bidx=1%2C2%2C3` + ] + }, + exclusiveWith: ['no2', 'gibs-population', 'car-count', 'nightlights-viirs'], + swatch: { + color: '#f2a73a', + name: 'Gold' + }, + legend: { + type: 'gradient', + min: 'less', + max: 'more', + stops: [ + '#08041d', + '#1f0a46', + '#52076c', + '#f57c16', + '#f7cf39' + ] + }, + info: null +}; diff --git a/app/assets/scripts/components/common/layers/layer-nightlights-viirs.js b/app/assets/scripts/components/common/layers/layer-nightlights-viirs.js new file mode 100644 index 00000000..0a86d821 --- /dev/null +++ b/app/assets/scripts/components/common/layers/layer-nightlights-viirs.js @@ -0,0 +1,29 @@ +import config from '../../../config'; + +export default { + id: 'nightlights-viirs', + name: 'Nightlights VIIRS', + type: 'raster-timeseries', + timeUnit: 'day', + domain: [ + '2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04', '2020-01-05', '2020-01-06', '2020-01-07', '2020-01-08', '2020-01-09', '2020-01-10', '2020-01-11', '2020-01-12', '2020-01-13', '2020-01-14', '2020-01-15', '2020-01-16', '2020-01-17', '2020-01-18', '2020-01-19', '2020-01-20', '2020-01-21', '2020-01-22', '2020-01-23', '2020-01-24', '2020-01-25', '2020-01-26', '2020-01-27', '2020-01-28', '2020-01-29', '2020-01-30', '2020-01-31', '2020-02-01', '2020-02-02', '2020-02-03', '2020-02-04', '2020-02-05', '2020-02-06', '2020-02-07', '2020-02-08', '2020-02-09', '2020-02-10', '2020-02-11', '2020-02-12', '2020-02-13', '2020-02-14', '2020-02-15', '2020-02-16', '2020-02-17', '2020-02-18', '2020-02-19', '2020-02-20', '2020-02-21', '2020-02-22', '2020-02-23', '2020-02-24', '2020-02-25', '2020-02-26', '2020-02-27', '2020-02-28', '2020-02-29', '2020-03-01', '2020-03-02', '2020-03-03', '2020-03-04', '2020-03-05', '2020-03-06', '2020-03-07', '2020-03-08', '2020-03-09', '2020-03-10', '2020-03-11', '2020-03-12', '2020-03-13', '2020-03-14', '2020-03-15', '2020-03-16', '2020-03-17', '2020-03-18', '2020-03-19', '2020-03-20', '2020-03-21', '2020-03-22', '2020-03-23', '2020-03-24', '2020-03-25', '2020-03-26', '2020-03-27', '2020-03-28', '2020-03-29', '2020-03-30', '2020-03-31', '2020-04-01', '2020-04-02', '2020-04-03', '2020-04-04', '2020-04-05', '2020-04-06', '2020-04-07', '2020-04-08', '2020-04-09', '2020-04-10', '2020-04-11', '2020-04-12', '2020-04-13', '2020-04-14', '2020-04-15', '2020-04-16', '2020-04-17', '2020-04-18', '2020-04-19', '2020-04-20', '2020-04-21', '2020-04-22', '2020-04-23', '2020-04-24', '2020-04-25', '2020-04-26', '2020-04-27', '2020-04-28', '2020-04-29', '2020-04-30', '2020-05-01', '2020-05-02', '2020-05-03', '2020-05-04', '2020-05-05', '2020-05-06', '2020-05-07', '2020-05-08', '2020-05-09', '2020-05-10', '2020-05-11', '2020-05-12', '2020-05-13', '2020-05-14', '2020-05-15', '2020-05-16', '2020-05-17', '2020-05-18', '2020-05-19', '2020-05-20' + ], + source: { + type: 'raster', + tiles: [ + `${config.api}/{z}/{x}/{y}@1x?url=s3://covid-eo-data/BM_500M_DAILY/VNP46A2_V011_{spotlightName}_{date}_cog.tif&resampling_method=nearest&bidx=1&rescale=0%2C100&color_map=viridis` + ] + }, + exclusiveWith: ['no2', 'gibs-population', 'car-count', 'nightlights-hd'], + swatch: { + color: '#f2a73a', + name: 'Gold' + }, + legend: { + type: 'gradient', + min: 'less', + max: 'more', + stops: ['#08041d', '#1f0a46', '#52076c', '#f57c16', '#f7cf39'] + }, + info: null +}; diff --git a/app/assets/scripts/components/common/layers/layer-nightlights.js b/app/assets/scripts/components/common/layers/layer-nightlights.js deleted file mode 100644 index da56f3c7..00000000 --- a/app/assets/scripts/components/common/layers/layer-nightlights.js +++ /dev/null @@ -1,31 +0,0 @@ -import config from '../../../config'; - -export default { - id: 'nightlights', - name: 'Nightlights', - type: 'raster', - source: { - type: 'raster', - tiles: [ - `${config.api}/{z}/{x}/{y}@1x?url=s3://covid-eo-data/viirs/sample/BMHD_Sample_Tokyo_cog.tif` - ] - }, - exclusiveWith: ['no2'], - swatch: { - color: '#f2a73a', - name: 'Gold' - }, - legend: { - type: 'gradient', - min: 'less', - max: 'more', - stops: [ - '#08041d', - '#1f0a46', - '#52076c', - '#f57c16', - '#f7cf39' - ] - }, - info: null -}; diff --git a/app/assets/scripts/components/common/layers/layer-no2.js b/app/assets/scripts/components/common/layers/layer-no2.js index 5d86dcd4..3f2aa351 100644 --- a/app/assets/scripts/components/common/layers/layer-no2.js +++ b/app/assets/scripts/components/common/layers/layer-no2.js @@ -8,7 +8,7 @@ export default { description: 'Acute harm due to NO2 exposure is only likely to arise in occupational settings. Direct exposure to the skin can cause irritations and burns.', type: 'raster-timeseries', domain: [ - '2010-10-01', + '2019-03-01', '2020-03-01' ], source: { @@ -17,7 +17,7 @@ export default { `${config.api}/{z}/{x}/{y}@1x?url=s3://covid-eo-data/OMNO2d_HRM/OMI_trno2_0.10x0.10_{date}_Col3_V4.nc.tif&resampling_method=bilinear&bidx=1&rescale=0%2C1e16&color_map=custom_no2&color_formula=gamma r {gamma}` ] }, - exclusiveWith: ['gibs-population', 'car-count', 'nightlights'], + exclusiveWith: ['gibs-population', 'car-count', 'nightlights-viirs', 'nightlights-hd'], enabled: true, compare: { enabled: true, diff --git a/app/assets/scripts/components/common/layers/layer-population.js b/app/assets/scripts/components/common/layers/layer-population.js index 54b3e048..0e11c4f2 100644 --- a/app/assets/scripts/components/common/layers/layer-population.js +++ b/app/assets/scripts/components/common/layers/layer-population.js @@ -8,7 +8,7 @@ export default { 'https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/GPW_Population_Density_2020/default/2020-05-14T00:00:00Z/GoogleMapsCompatible_Level7/{z}/{y}/{x}.png' ] }, - exclusiveWith: ['no2', 'car-count'], + exclusiveWith: ['no2', 'car-count', 'nightlights-viirs', 'nightlights-hd'], swatch: { color: '#F55E2C', name: 'Orange' diff --git a/app/assets/scripts/components/common/layers/types.js b/app/assets/scripts/components/common/layers/types.js index adee06c3..0f727b85 100644 --- a/app/assets/scripts/components/common/layers/types.js +++ b/app/assets/scripts/components/common/layers/types.js @@ -1,9 +1,16 @@ import { format, sub } from 'date-fns'; -const prepDateSource = (source, date) => ({ - ...source, - tiles: source.tiles.map((t) => t.replace('{date}', format(date, 'yyyyMM'))) -}); +const prepDateSource = (source, date, timeUnit = 'month') => { + const formats = { + month: 'yyyyMM', + day: 'yyyy_MM_dd' + }; + + return { + ...source, + tiles: source.tiles.map((t) => t.replace('{date}', format(date, formats[timeUnit]))) + }; +}; const prepGammaSource = (source, knobPos) => { // Gamma is calculated with the following scale: @@ -18,9 +25,11 @@ const prepGammaSource = (source, knobPos) => { }; }; -const prepSource = (source, date, knobPos) => { - source = prepDateSource(source, date); - source = prepGammaSource(source, knobPos); +const prepSource = (layerInfo, source, date, knobPos) => { + if (layerInfo.legend.type === 'gradient-adjustable') { + source = prepGammaSource(source, knobPos); + } + source = prepDateSource(source, date, layerInfo.timeUnit); return source; }; @@ -68,12 +77,12 @@ export const layerTypes = { // END update checks. // Update layer tiles. - const tiles = prepSource(source, date, knobPos).tiles; + const tiles = prepSource(layerInfo, source, date, knobPos).tiles; replaceRasterTiles(mbMap, id, tiles); // Update/init compare layer tiles. if (comparing) { - const source5years = prepSource(source, sub(date, { years: 5 }), knobPos); + const source5years = prepSource(layerInfo, source, sub(date, { years: 5 }), knobPos); if (mbMapComparing.getSource(id)) { replaceRasterTiles(mbMapComparing, id, source5years.tiles); } else { @@ -84,7 +93,7 @@ export const layerTypes = { type: 'raster', source: id }, - 'admin-1-boundary-bg' + 'admin-0-boundary-bg' ); } } @@ -106,14 +115,14 @@ export const layerTypes = { if (mbMap.getSource(id)) { mbMap.setLayoutProperty(id, 'visibility', 'visible'); } else { - mbMap.addSource(id, prepSource(source, date, layerInfo.knobCurrPos)); + mbMap.addSource(id, prepSource(layerInfo, source, date, layerInfo.knobCurrPos)); mbMap.addLayer( { id: id, type: 'raster', source: id }, - 'admin-1-boundary-bg' + 'admin-0-boundary-bg' ); } } @@ -140,7 +149,7 @@ export const layerTypes = { type: 'raster', source: id }, - 'admin-1-boundary-bg' + 'admin-0-boundary-bg' ); } } diff --git a/app/assets/scripts/components/common/line-chart/bisector.layer.js b/app/assets/scripts/components/common/line-chart/bisector.layer.js index 5725c136..abbeeea3 100644 --- a/app/assets/scripts/components/common/line-chart/bisector.layer.js +++ b/app/assets/scripts/components/common/line-chart/bisector.layer.js @@ -1,31 +1,10 @@ import * as d3 from 'd3'; import { css } from 'styled-components'; +import { isWithinInterval } from 'date-fns'; import { themeVal } from '../../../styles/utils/general'; -import { utcDate } from '../../../utils/utils'; - -const bisectByDate = (data, date) => { - // Define bisector function. Is used to find where this date would fin in the - // data array - const bisect = d3.bisector(data => (new Date(data.date)).getTime()).left; - const mouseDate = date.getTime(); - // Returns the index to the current data item. - const i = bisect(data, mouseDate); - - if (i === 0) { - return data[i]; - } else if (i === data.length) { - return data[i - 1]; - } else { - const docR = data[i]; - const docL = data[i - 1]; - const deltaL = mouseDate - (new Date(docL.date)).getTime(); - const deltaR = (new Date(docR.date)).getTime() - mouseDate; - return deltaL > deltaR - ? docR - : docL; - } -}; +import { utcDate, bisectByDate } from '../../../utils/utils'; +import { _rgba } from '../../../styles/utils/theme-values'; const styles = props => css` /* Bisector specific styles */ @@ -39,10 +18,18 @@ const styles = props => css` stroke-linecap: round; } .bisector-select { - stroke: ${themeVal('color.base')}; - stroke-width: 4px; + stroke: ${themeVal('color.baseAlphaD')}; + stroke-width: 2px; stroke-linecap: round; } + .bisector-select-label { + font-size: 0.625rem; + font-weight: ${themeVal('type.base.bold')}; + + span { + background-color: ${_rgba(themeVal('color.surface'), 0.80)}; + } + } } `; @@ -57,8 +44,18 @@ export default { .attr('class', 'bisector-interact') .style('display', 'none'); - // bisectorG.append('line') - // .attr('class', 'bisector-select'); + bisectorG.append('line') + .attr('class', 'bisector-select') + .style('display', 'none'); + // Using a foreign object is needed to include a div with the text. + // The main reason for this is to be able to easily add a background to + // the text. + bisectorG.append('foreignObject') + .attr('class', 'bisector-select-label') + .attr('transform', 'rotate(-90)') + .append('xhtml:div') + .append('span') + .text('Timeline date (closest)'); bisectorG.append('rect') .attr('class', 'trigger-rect') @@ -93,24 +90,40 @@ export default { }, update: ctx => { - // const { selectedDate } = ctx.props; + const { selectedDate } = ctx.props; const { width, height } = ctx.getSize(); ctx.dataCanvas .select('.bisector') + .raise() .style('display', '') .raise() .select('.trigger-rect') .attr('width', width) .attr('height', height); - // const xPos = ctx.xScale(selectedDate); + const domain = ctx.xScale.domain(); + if (selectedDate && isWithinInterval(selectedDate, { start: domain[0], end: domain[1] })) { + const closestDataPoint = bisectByDate(ctx.props.data, selectedDate); + const xPos = ctx.xScale(utcDate(closestDataPoint.date)); - // ctx.dataCanvas.select('.bisector-select') - // .attr('y2', 0) - // .attr('y1', height) - // .attr('x1', xPos) - // .attr('x2', xPos); + ctx.dataCanvas.select('.bisector-select') + .style('display', '') + .attr('y2', 0) + .attr('y1', height) + .attr('x1', xPos) + .attr('x2', xPos); + + ctx.dataCanvas.select('.bisector-select-label') + .style('display', '') + .attr('x', -height) + .attr('y', xPos + 2) + .attr('width', height) + .attr('height', 16); + } else { + ctx.dataCanvas.select('.bisector-select').style('display', 'none'); + ctx.dataCanvas.select('.bisector-select-label').style('display', 'none'); + } } }; diff --git a/app/assets/scripts/components/common/line-chart/chart.js b/app/assets/scripts/components/common/line-chart/chart.js index 8e5433c7..fa60fb78 100644 --- a/app/assets/scripts/components/common/line-chart/chart.js +++ b/app/assets/scripts/components/common/line-chart/chart.js @@ -90,7 +90,7 @@ const ChartWrapper = styled(SizeAwareElement)` class DataBrowserChart extends React.Component { constructor (props) { super(props); - this.margin = { top: 16, right: 32, bottom: 80, left: 48 }; + this.margin = { top: 16, right: 32, bottom: 48, left: 48 }; // Control whether the chart was rendered. // The size aware element fires a onChange event once it is rendered // But at that time the chart is not ready yet so we can't update the size. @@ -270,13 +270,13 @@ class DataBrowserChart extends React.Component { {!noIndicator && ( <> Indicator {!noIndicatorConfidence && (confidence)} -
{round(doc.indicator)} {!noIndicatorConfidence && ({doc.cMax} - {doc.cMin})}
+
{round(doc.indicator)} {!noIndicatorConfidence && ({doc.indicator_conf_high} - {doc.indicator_conf_low})}
)} {!noBaseline && ( <> Baseline {!noBaselineConfidence && (confidence)} -
{round(doc.baseline)} {!noBaselineConfidence && ({doc.baselineMax} - {doc.baselineMin})}
+
{round(doc.baseline)} {!noBaselineConfidence && ({doc.baseline_conf_high} - {doc.baseline_conf_low})}
)} diff --git a/app/assets/scripts/components/common/line-chart/data-baseline.layer.js b/app/assets/scripts/components/common/line-chart/data-baseline.layer.js index 888923f6..6fb11d0e 100644 --- a/app/assets/scripts/components/common/line-chart/data-baseline.layer.js +++ b/app/assets/scripts/components/common/line-chart/data-baseline.layer.js @@ -1,14 +1,12 @@ -import * as d3 from 'd3'; import { css } from 'styled-components'; -import { utcDate } from '../../../utils/utils'; import { themeVal } from '../../../styles/utils/general'; -import { bisectorPoints } from './utils'; +import { dataPoints, confidenceLines } from './utils'; const styles = props => css` /* Data specific styles */ .data-baseline { - .baseline { + .line { stroke: ${themeVal('color.secondary')}; &.base { @@ -45,76 +43,24 @@ export default { update: ctx => { const { dataCanvas, props, xScale, yScale } = ctx; if (props.noBaseline) return; - // Limit data to existing date domain. - const data = props.data; const dataSeries = dataCanvas.select('.data-baseline'); - if (!props.noBaselineConfidence) { - const area = d3.area() - .x(d => xScale(utcDate(d.date))) - .y0(d => yScale(d.baselineMax)) - .y1(d => yScale(d.baselineMin)); - - const baselineRange = dataSeries.selectAll('.range').data([data]); - - // Remove old. - baselineRange.exit().remove(); - // Handle new. - baselineRange - .enter() - .append('path') - .attr('class', 'range') - .merge(baselineRange) - // Update current. - .attr('d', area); - } - - // Create data array for all the lines: min, max, and base. - // This allows us to reuse the line function and code. - const mapData = prop => data.map(d => ({ - date: d.date, - value: d[prop] - })); - let linesData = [ - { - id: 'base', - data: mapData('baseline') - } - ]; - - if (!props.noBaselineConfidence) { - linesData = [ - { - id: 'max', - data: mapData('baselineMax') - }, - ...linesData, - { - id: 'min', - data: mapData('baselineMin') - } - ]; - } - - const lines = dataSeries.selectAll('.baseline').data(linesData); - - // Remove old. - lines.exit().remove(); - // Handle new. - lines - .enter() - .append('path') - .attr('class', d => `baseline ${d.id}`) - .attr('fill', 'none') - .merge(lines) - // Update current. - .attr('d', d => ctx.line(d.data)); + dataSeries.call( + confidenceLines(props.data) + .includeConfidence(!props.noBaselineConfidence) + .x(xScale) + .y(yScale) + .accessor(d => d.baseline) + .accessorHigh(d => d.baseline_conf_high) + .accessorLow(d => d.baseline_conf_low) + ); // Show the bisector point if needed. const bisectorPointData = ctx.state.bisecting ? [ctx.state.doc] : []; dataSeries.call( - bisectorPoints(bisectorPointData) + dataPoints(bisectorPointData) + .pointClass('bisector-point') .x(xScale) .y(yScale) .yProp('baseline') diff --git a/app/assets/scripts/components/common/line-chart/data-highlight-bands.layer.js b/app/assets/scripts/components/common/line-chart/data-highlight-bands.layer.js index ea551159..5464225a 100644 --- a/app/assets/scripts/components/common/line-chart/data-highlight-bands.layer.js +++ b/app/assets/scripts/components/common/line-chart/data-highlight-bands.layer.js @@ -2,6 +2,7 @@ import { css } from 'styled-components'; import { utcDate } from '../../../utils/utils'; import { themeVal } from '../../../styles/utils/general'; +import { _rgba } from '../../../styles/utils/theme-values'; const styles = props => css` /* Data specific styles */ @@ -11,6 +12,14 @@ const styles = props => css` fill: ${themeVal('color.tertiary')}; } } + + .data-bands-labels { + .label { + font-size: 0.75rem; + fill: ${_rgba(themeVal('type.base.color'), 0.96)}; + font-weight: ${themeVal('type.base.bold')}; + } + } `; export default { @@ -18,6 +27,8 @@ export default { init: ctx => { ctx.dataCanvas .append('g').attr('class', 'data-bands'); + ctx.dataCanvas + .append('g').attr('class', 'data-bands-labels'); }, update: ctx => { @@ -28,9 +39,10 @@ export default { // Limit data to existing date domain. const data = props.highlightBands; - const dataContainer = dataCanvas.select('.data-bands'); - - const bands = dataContainer.selectAll('.band').data(data); + const bands = dataCanvas + .select('.data-bands') + .selectAll('.band') + .data(data); // Remove old. bands.exit().remove(); @@ -41,9 +53,32 @@ export default { .attr('class', 'band') .merge(bands) // Update current. - .attr('x', d => xScale(utcDate(d[0]))) + .attr('x', d => xScale(utcDate(d.interval[0]))) .attr('y', 0) - .attr('width', d => xScale(utcDate(d[1])) - xScale(utcDate(d[0]))) + .attr('width', d => xScale(utcDate(d.interval[1])) - xScale(utcDate(d.interval[0]))) .attr('height', height); + + const labels = dataCanvas + .select('.data-bands-labels') + .selectAll('.label') + .raise() + .data(data); + + // Remove old. + labels.exit().remove(); + // Handle new. + labels + .enter() + .append('text') + .attr('class', 'label') + .merge(labels) + // Update current. + .text(d => d.label) + .attr('y', d => xScale(utcDate(d.interval[0]))) + .attr('x', 0) + .attr('transform', 'rotate(-90)') + .attr('text-anchor', 'end') + .attr('dy', '1.15em') + .attr('dx', '-0.5em'); } }; diff --git a/app/assets/scripts/components/common/line-chart/data-series.layer.js b/app/assets/scripts/components/common/line-chart/data-series.layer.js index 31db38bd..0d5d0a70 100644 --- a/app/assets/scripts/components/common/line-chart/data-series.layer.js +++ b/app/assets/scripts/components/common/line-chart/data-series.layer.js @@ -1,22 +1,25 @@ -import * as d3 from 'd3'; import { css } from 'styled-components'; -import { utcDate } from '../../../utils/utils'; import { themeVal } from '../../../styles/utils/general'; -import { bisectorPoints } from './utils'; +import { dataPoints, confidenceLines } from './utils'; const styles = props => css` /* Data specific styles */ .data-series { .line { - stroke-width: 2px; stroke: ${themeVal('color.primary')}; - } - .point { - fill: ${themeVal('color.primary')}; + + &.base { + stroke-width: 2px; + } + + &.min, &.max { + stroke-width: 1px; + stroke-opacity: 0.16; + } } - .confidence-area { + .range { fill-opacity: 0.32; fill: ${themeVal('color.primary')}; } @@ -40,57 +43,23 @@ export default { const { dataCanvas, props, xScale, yScale } = ctx; if (props.noIndicator) return; - // Limit data to existing date domain. - const data = props.data.map(d => ({ - date: d.date, - value: d.indicator - })); - const dataSeries = dataCanvas.select('.data-series'); - if (!props.noIndicatorConfidence) { - const area = d3.area() - .x(d => xScale(utcDate(d.date))) - .y0(d => yScale(d.cMax)) - .y1(d => yScale(d.cMin)); - - const confidenceArea = dataSeries.selectAll('.confidence-area').data([props.data]); - - // Remove old. - confidenceArea.exit().remove(); - // Handle new. - confidenceArea - .enter() - .append('path') - .attr('class', 'confidence-area') - .merge(confidenceArea) - // Update current. - .attr('d', area); - } - - const line = d3.line() - .defined(d => d.value !== null) - .x(d => xScale(utcDate(d.date))) - .y(d => yScale(d.value)); - - const lines = dataSeries.selectAll('.line').data([data]); - - // Remove old. - lines.exit().remove(); - // Handle new. - lines - .enter() - .append('path') - .attr('class', 'line') - .attr('fill', 'none') - .merge(lines) - // Update current. - .attr('d', line); + dataSeries.call( + confidenceLines(props.data) + .includeConfidence(!props.noIndicatorConfidence) + .x(xScale) + .y(yScale) + .accessor(d => d.indicator) + .accessorHigh(d => d.indicator_conf_high) + .accessorLow(d => d.indicator_conf_low) + ); // Show the bisector point if needed. const bisectorPointData = ctx.state.bisecting ? [ctx.state.doc] : []; dataSeries.call( - bisectorPoints(bisectorPointData) + dataPoints(bisectorPointData) + .pointClass('bisector-point') .x(xScale) .y(yScale) .yProp('indicator') diff --git a/app/assets/scripts/components/common/line-chart/utils.js b/app/assets/scripts/components/common/line-chart/utils.js index 62937d36..4906ad43 100644 --- a/app/assets/scripts/components/common/line-chart/utils.js +++ b/app/assets/scripts/components/common/line-chart/utils.js @@ -1,13 +1,16 @@ +import * as d3 from 'd3'; + import { utcDate } from '../../../utils/utils'; -export function bisectorPoints (data) { +export function dataPoints (data) { let _yprop = 'value'; + let _klass = null; let _x = null; let _y = null; function main (context) { const points = context - .selectAll('.bisector-point') + .selectAll(`.${_klass}`) .data(data); // Remove old. @@ -17,7 +20,7 @@ export function bisectorPoints (data) { .enter() .append('circle') .attr('r', 4) - .attr('class', 'bisector-point') + .attr('class', _klass) .merge(points) // Update current. .attr('cx', d => _x(utcDate(d.date))) @@ -39,5 +42,114 @@ export function bisectorPoints (data) { return main; }; + main.pointClass = _ => { + _klass = _; + return main; + }; + + return main; +} + +export function confidenceLines (data) { + let _accessor = v => v; + let _accessorHigh = v => v; + let _accessorLow = v => v; + let _includeConfidence = true; + let _x = null; + let _y = null; + + function main (context) { + const line = d3.line() + .defined(d => d.value !== null) + .x(d => _x(utcDate(d.date))) + .y(d => _y(d.value)); + + if (_includeConfidence) { + const area = d3.area() + .x(d => _x(utcDate(d.date))) + .y0(d => _y(_accessorHigh(d))) + .y1(d => _y(_accessorLow(d))); + + const baselineRange = context + .selectAll('.range') + .data([data]); + + // Remove old. + baselineRange.exit().remove(); + // Handle new. + baselineRange + .enter() + .append('path') + .attr('class', 'range') + .merge(baselineRange) + // Update current. + .attr('d', area); + } + + // Create data array for all the lines: min, max, and base. + // This allows us to reuse the line function and code. + const mapData = accessor => data.map(d => ({ + date: d.date, + value: accessor(d) + })); + let linesData = [ + { id: 'base', data: mapData(_accessor) } + ]; + + if (_includeConfidence) { + linesData = [ + { id: 'max', data: mapData(_accessorHigh) }, + ...linesData, + { id: 'min', data: mapData(_accessorLow) } + ]; + } + + const lines = context + .selectAll('.line') + .data(linesData); + + // Remove old. + lines.exit().remove(); + // Handle new. + lines + .enter() + .append('path') + .attr('class', d => `line ${d.id}`) + .attr('fill', 'none') + .merge(lines) + // Update current. + .attr('d', d => line(d.data)); + } + + main.accessor = _ => { + _accessor = _; + return main; + }; + + main.accessorHigh = _ => { + _accessorHigh = _; + return main; + }; + + main.accessorLow = _ => { + _accessorLow = _; + return main; + }; + + main.includeConfidence = _ => { + _includeConfidence = _; + return main; + }; + + main.x = _ => { + _x = _; + return main; + }; + + main.y = _ => { + _y = _; + return main; + }; + return main; } diff --git a/app/assets/scripts/components/common/line-chart/yaxis.layer.js b/app/assets/scripts/components/common/line-chart/yaxis.layer.js index e24a2954..2406f278 100644 --- a/app/assets/scripts/components/common/line-chart/yaxis.layer.js +++ b/app/assets/scripts/components/common/line-chart/yaxis.layer.js @@ -1,10 +1,8 @@ import * as d3fc from '@d3fc/d3fc-axis'; import { css } from 'styled-components'; -import { rgba } from 'polished'; -import { themeVal, stylizeFunction } from '../../../styles/utils/general'; - -const _rgba = stylizeFunction(rgba); +import { themeVal } from '../../../styles/utils/general'; +import { _rgba } from '../../../styles/utils/theme-values'; const styles = props => css` /* YAxis specific styles */ diff --git a/app/assets/scripts/components/common/page-header.js b/app/assets/scripts/components/common/page-header.js index dff676c9..de26045c 100644 --- a/app/assets/scripts/components/common/page-header.js +++ b/app/assets/scripts/components/common/page-header.js @@ -15,9 +15,9 @@ import { wrapApiResult } from '../../redux/reduxeed'; import Button from '../../styles/button/button'; import Dropdown, { DropTitle, DropMenu, DropMenuItem } from './dropdown'; -import datasetsList from '../datasets'; +import indicatorsList from '../indicators'; -const { appTitle, appShortTitle, appVersion } = config; +const { appTitle, appShortTitle, appVersion, baseUrl } = config; const PageHead = styled.header` position: relative; @@ -62,7 +62,7 @@ const PageTitle = styled.h1` content: ''; height: 48px; width: 56px; - background: url('/assets/graphics/layout/app-logo-sprites.png'); + background: url(${`${baseUrl}/assets/graphics/layout/app-logo-sprites.png`}); background-size: auto 100%; background-repeat: none; background-position: top right; @@ -201,6 +201,18 @@ class PageHeader extends React.Component { } > + +
  • + + About + +
  • +
    Spotlight areas {spotlightAreas && spotlightAreas.map(ss => ( @@ -224,20 +236,32 @@ class PageHeader extends React.Component { triggerElement={ } > - Datasets - {datasetsList.filter(d => !!d.LongForm).map(d => ( +
  • + + About + +
  • +
    + Indicators + + {indicatorsList.filter(d => !!d.LongForm).map(d => (
  • {d.name} diff --git a/app/assets/scripts/components/common/panel-block.js b/app/assets/scripts/components/common/panel-block.js index fcaa40af..da4b11c9 100644 --- a/app/assets/scripts/components/common/panel-block.js +++ b/app/assets/scripts/components/common/panel-block.js @@ -14,11 +14,11 @@ export const PanelBlock = styled.section` flex: 1; position: relative; z-index: 10; - box-shadow: 0 -1px 0 0 ${themeVal('color.baseAlphaB')}; + box-shadow: inset 0 -1px 0 0 ${themeVal('color.baseAlphaB')}; `; export const PanelBlockHeader = styled.header` - box-shadow: 0 1px 0 0 ${themeVal('color.baseAlphaB')}; + box-shadow: inset 0 -1px 0 0 ${themeVal('color.baseAlphaB')}; background: ${_tint(0.02, themeVal('color.surface'))}; position: relative; z-index: 10; diff --git a/app/assets/scripts/components/common/panel.js b/app/assets/scripts/components/common/panel.js index 8743b5b8..5d0aa07b 100644 --- a/app/assets/scripts/components/common/panel.js +++ b/app/assets/scripts/components/common/panel.js @@ -32,7 +32,7 @@ const PanelHeader = styled.header` box-shadow: 0 1px 0 0 ${themeVal('color.baseAlphaB')}; background: ${_tint(0.02, themeVal('color.surface'))}; position: relative; - z-index: 10; + z-index: 100; display: flex; justify-content: flex-start; align-items: flex-start; diff --git a/app/assets/scripts/components/common/timeline.js b/app/assets/scripts/components/common/timeline.js index 64e54406..76b147f3 100644 --- a/app/assets/scripts/components/common/timeline.js +++ b/app/assets/scripts/components/common/timeline.js @@ -20,6 +20,30 @@ const checkSameDate = (date, compareDate, interval) => { } }; +const getNextDate = (domain, date, timeUnit) => { + // If we're working with a discrete domain, get the closest value. + if (domain.length > 2) { + const currIdx = domain.findIndex(d => isSameDay(d, date)); + if (currIdx < 0 || currIdx >= domain.length - 1) return null; + return domain[currIdx + 1]; + } else { + // If we only have start and end, round based on time unit. + return add(date, getOperationParam(timeUnit)); + } +}; + +const getPrevDate = (domain, date, timeUnit) => { + // If we're working with a discrete domain, get the closest value. + if (domain.length > 2) { + const currIdx = domain.findIndex(d => isSameDay(d, date)); + if (currIdx <= 0) return null; + return domain[currIdx - 1]; + } else { + // If we only have start and end, round based on time unit. + return sub(date, getOperationParam(timeUnit)); + } +}; + const getOperationParam = (interval) => { if (interval === 'day') { return { days: 1 }; @@ -30,9 +54,9 @@ const getOperationParam = (interval) => { const formatDate = (date, interval) => { if (interval === 'day') { - return format(date, "dd MMM yy''"); + return format(date, "dd MMM ''yy"); } else { - return format(date, "MMM yy''"); + return format(date, "MMM ''yy"); } }; @@ -167,24 +191,24 @@ class Timeline extends React.Component { variation='base-plain' size='small' useIcon='chevron-left--small' - title='Previous day' + title='Previous entry' hideText onClick={() => - onAction('date.set', { date: sub(date, getOperationParam(timeUnit)) })} + onAction('date.set', { date: getPrevDate(dateDomain, date, timeUnit) })} > - Previous day + Previous entry diff --git a/app/assets/scripts/components/datasets/dataset-no2.js b/app/assets/scripts/components/datasets/dataset-no2.js deleted file mode 100644 index 56a42d46..00000000 --- a/app/assets/scripts/components/datasets/dataset-no2.js +++ /dev/null @@ -1,326 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { NavLink } from 'react-router-dom'; - -import Prose from '../../styles/type/prose'; -import Constrainer from '../../styles/constrainer'; -import Gridder from '../../styles/gridder'; -import InpageHGroup from '../../styles/inpage-hgroup'; -import Button from '../../styles/button/button'; -import { Fold, FoldDetails, FoldTitle } from '../../styles/fold'; -import MediaImage from '../../styles/media-image'; -import { - IntroLead, - IntroDescription, - StatsFold, - StatsHeader, - StatsList, - DetectionLead, - DetectionFold -} from '../../styles/datasets'; - -import { themeVal } from '../../styles/utils/general'; -import { glsp } from '../../styles/utils/theme-values'; -import Heading from '../../styles/type/heading'; - -const DetectionStep1 = styled(Fold)` - padding-bottom: 0; - - ${Gridder} { - align-items: center; - } - - ${MediaImage} { - grid-column: full-start / content-7; - grid-row: 1; - } - - ${FoldDetails} { - grid-column: content-8 / content-end; - text-align: left; - } -`; - -const DetectionStep2 = styled(Fold)` - ${Gridder} { - align-items: center; - } - - ${FoldDetails} { - grid-column: content-start / content-6; - text-align: left; - grid-row: 1; - } - - ${MediaImage} { - grid-column: content-7 / full-end; - grid-row: 1; - } -`; - -const EffectEntry = styled.section` - ${Heading} { - margin-bottom: ${glsp(1)}; - } -`; - -const EffectsFold = styled(Fold)` - background: ${themeVal('color.baseAlphaB')}; - - ${EffectEntry} { - grid-row: 3; - grid-column: span 4; - } - - ${/* sc-selector */EffectEntry}:first-of-type { - grid-row: 2; - grid-column: 5 / span 8; - } -`; - -const FactsFold = styled(Fold)` - padding-bottom: ${glsp(10)}; - - ${Gridder} { - align-items: center; - } - - /* stylelint-disable-next-line */ - ${InpageHGroup} { - grid-row: 1; - grid-column: content-start / content-7; - } - - ${Prose} { - grid-column: content-start / content-end; - grid-row: 2; - margin-bottom: ${glsp(4)}; - } - - ${MediaImage} { - grid-column: content-6 / full-end; - grid-row: 3; - - figcaption { - max-width: 46rem; - padding-right: ${glsp(2)}; - } - } -`; - -const metadata = { - id: 'no2', - name: 'Nitrogen Dioxide', - color: '#2276AC' -}; - -class NO2LongForm extends React.Component { - render () { - return ( - - - - - Some intro lead, like the key takeaway of this particular dataset - - -

    Some more in depth information

    -

    - Consectetur adipisicing elit. Vitae laboriosam recusandae esse, - rem hic nobis. Labore voluptatum eligendi nisi eius, quod ipsa - soluta odio placeat hic ad, repudiandae, velit earum! -

    -
    -
    -
    - - - - - This dataset in the wild - - - -
    Last year
    -
    100
    -
    Last month
    -
    2000
    -
    -
    -
    - - - - How was this created? - - - - - - - -

    - Lorem ipsum dolor sit amet consectetur adipisicing elit. - Numquam rerum minus nesciunt necessitatibus ea id veritatis, - et ut porro possimus accusamus assumenda, pariatur fugit - praesentium magnam enim vero non, a repellat quae distinctio - neque voluptatem. Dignissimos saepe animi praesentium - sapiente. -

    -

    - Neque reprehenderit ullam numquam nulla tempore ea natus - voluptates. Sunt dolor reiciendis dolore impedit asperiores - fugiat, quas saepe quibusdam itaque deleniti ratione - corporis incidunt dignissimos quia nostrum aliquam possimus - unde, voluptatem temporibus repellendus aliquid officia. - Molestiae sit animi sequi velit consectetur est facere - excepturi possimus incidunt! Pariatur blanditiis illo eaque - asperiores harum. -

    -
    -
    - -
    -
    - - - - - - -

    - Blanditiis ea commodi vero ipsa hic nam corporis! At - similique aspernatur ab, praesentium veniam placeat autem - iste veritatis voluptates amet nesciunt suscipit optio qui - voluptatum tempore. Dolores quaerat consequuntur nulla vero - expedita. -

    -
    -
    - -
    -
    - - - -

    - Some important lead, like the conclusion of the previous topic. - Distinctio ullam quaerat esse id consectetur vitae praesentium - facilis facere voluptate. -

    -
    -
    -
    - - - - - Some content in the form of columns - - - - Ducimus -

    - Exercitationem voluptatum expedita vel rerum nemo quidem quas - error. Assumenda harum dolores quia error illo odio et numquam - magnam qui? Dolores dolorem quisquam aperiam id a ipsum rerum - nesciunt magnam pariatur? Ipsa nemo sequi ad recusandae commodi - non, voluptatum nobis iste a temporibus quidem natus labore - ullam distinctio illum nesciunt molestiae! Ducimus autem ea - aperiam in maxime quae cupiditate magni aut? Quis, blanditiis. - Ipsum esse sapiente ex reprehenderit veniam aliquam maiores - labore. -

    -
    - - Aspernatur -

    - Ab quibusdam, consequatur magni magnam nisi molestiae doloremque - architecto hic reiciendis atque error accusamus vero sapiente, - in enim! Nam, iste quas cum doloremque molestiae autem odit - animi eius error aspernatur voluptas nobis. -

    -
    - - Flora -

    - Temporibus non aliquid numquam eius. Doloremque dolore fugit, - quam qui nihil possimus inventore. Provident nisi dignissimos - aliquid assumenda dolor nemo adipisci voluptas. -

    -
    - - Nobis -

    - Officiis nulla aliquam deserunt facilis consequuntur laborum - voluptas nobis eaque, in aperiam officia excepturi eius, - provident porro? Nobis praesentium quis eos excepturi. -

    -
    -
    -
    - - - - - -

    - Distinctio sunt sequi est, ex velit, eligendi id ducimus nemo - vero tempora, maiores laudantium dignissimos saepe beatae facere - facilis in corrupti? Facere, modi. Voluptatum eum adipisci ad - illum explicabo eveniet facere. Unde enim tempore dolore - doloribus inventore obcaecati aspernatur cum possimus voluptatum - fugit omnis quis molestiae id eos consectetur tempora delectus - quos maiores voluptatem quas doloremque, consequatur ex impedit. - Iure unde est quae doloribus necessitatibus libero aliquid - deleniti odit quas. Iste vel assumenda quae optio quo quam - labore, asperiores provident. A ducimus ipsa amet ullam iste - consequuntur ex, delectus quaerat qui perferendis. -

    -
    - - [See Author et al. 2017 Obcaecati, unde sit commodi molestiae - placeat ipsa]. - -
    -
    -
    - ); - } -} - -NO2LongForm.propTypes = {}; - -export default { - ...metadata, - LongForm: NO2LongForm -}; diff --git a/app/assets/scripts/components/datasets/index.js b/app/assets/scripts/components/datasets/index.js deleted file mode 100644 index 657ace8c..00000000 --- a/app/assets/scripts/components/datasets/index.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * How to add a new Dataset page: - * 1) Create a file for the page inside scripts/components/datasets/. - * The name of the file should be `dataset-[name].js - * The file will have to export an object with the following properties: - * - id: string - * - color: string - * - name: string - * - LongForm: ReactComponent - * - * The react component will dictate how the dataset page is rendered. If set - * to null, no page will be created for the dataset. - * - * The file should look something like: - * const metadata = { - * id: 'no2', - * name: 'Nitrogen Dioxide', - * color: '#2276AC' - * }; - * - * class NO2LongForm extends React.Component { - * render () { - * return

    This is the content for the no2 dataset

    - * } - * } - * - * export default { - * ...metadata, - * LongForm: NO2LongForm - * }; - * - * 2) Import the page below. - * - * 3) Add the page to the datasets array - * - */ -import no2 from './dataset-no2'; -import population from './dataset-population'; - -const datasets = [ - no2, - population -]; - -export default datasets; - -export const getDataset = (id) => datasets.find((d) => d.id === id); diff --git a/app/assets/scripts/components/home/index.js b/app/assets/scripts/components/home/index.js index 9b587f54..3ba4b63d 100644 --- a/app/assets/scripts/components/home/index.js +++ b/app/assets/scripts/components/home/index.js @@ -1,7 +1,9 @@ import React from 'react'; import styled from 'styled-components'; +import { NavLink } from 'react-router-dom'; import App from '../common/app'; +import Button from '../../styles/button/button'; import { Inpage, InpageHeader, @@ -10,20 +12,35 @@ import { InpageTitle, InpageBody } from '../../styles/inpage'; - +import InpageHGroup from '../../styles/inpage-hgroup'; import { - Fold -} from '../../styles/fold'; + PageConstrainer +} from '../../styles/hub-pages'; +import Prose from '../../styles/type/prose'; -import Constrainer from '../../styles/constrainer'; +import { filterComponentProps } from '../../utils/utils'; +import { glsp } from '../../styles/utils/theme-values'; -import Prose from '../../styles/type/prose'; +const IntroActions = styled.div` + display: grid; + grid-gap: ${glsp()}; + grid-template-columns: repeat(12, 1fr); + padding: ${glsp(1, 0)}; -const HomeProse = styled(Prose)` - grid-row: 1; - grid-column: span 8; + > * { + grid-column: auto / span 3; + } + + /* stylelint-disable-next-line */ + ${InpageHGroup} { + grid-row: 1; + grid-column: content-start / content-7; + } `; +const propsToFilter = ['size', 'useIcon', 'variation']; +const CleanNavLink = filterComponentProps(NavLink, propsToFilter); + export default class Home extends React.Component { render () { return ( @@ -32,23 +49,66 @@ export default class Home extends React.Component { - Welcome to COVID-19 Dashboard + Welcome to the COVID-19 Data Dashboard - - - -

    - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Voluptatem iste ducimus neque sunt voluptatum veritatis totam. Minima aut quisquam ea adipisci velit exercitationem nesciunt quam maiores, deserunt, ex aliquam molestias? Asperiores, nam ab beatae mollitia accusamus quis repudiandae corrupti animi architecto magnam fuga distinctio vitae laborum a, eum quisquam neque omnis, minus suscipit perspiciatis vel. Aspernatur est a tenetur doloremque excepturi possimus omnis repellendus error. -

    -

    - Eaque libero numquam, debitis harum corrupti rerum odio assumenda nihil molestiae quibusdam, unde accusantium aperiam laborum blanditiis cumque, non labore maiores. Tempore, dicta ipsa repellendus, obcaecati cumque eos amet in similique voluptate molestiae quos tempora deleniti incidunt, iste quasi illo quidem quibusdam excepturi consequuntur suscipit et facilis enim. Similique sapiente ex corporis. Eius, porro iusto? -

    -
    -
    -
    + + +

    + As communities around the world have changed their behavior in response + to the spread of COVID-19, NASA satellites have observed associated + changes in the environment. +

    +

    + The NASA COVID-19 data dashboard can be used to explore these changes + on a global scale. Powered by publicly available NASA Earth observing + data, the dashboard highlights 10 key indicators – 4 environmental and + 6 economic – that show how the planet is responding to our changing + behavior in response to COVID-19. +

    +

    + Use the dashboard to interact with real NASA data and investigate how + social distancing measures and regional shelter-in-place guidelines have + affected Earth’s air, land, and water. Explore individual 'Spotlight Areas' + to see how the indicators in each specific location have changed through + time. +

    +
    + + + + + + +
    diff --git a/app/assets/scripts/components/indicators/hub/index.js b/app/assets/scripts/components/indicators/hub/index.js new file mode 100644 index 00000000..13290634 --- /dev/null +++ b/app/assets/scripts/components/indicators/hub/index.js @@ -0,0 +1,98 @@ +import React from 'react'; +import T from 'prop-types'; +import { connect } from 'react-redux'; + +import App from '../../common/app'; +import { + Inpage, + InpageHeader, + InpageHeaderInner, + InpageHeadline, + InpageTitle, + InpageBody +} from '../../../styles/inpage'; +import Prose from '../../../styles/type/prose'; +import Heading from '../../../styles/type/heading'; +import { + PageConstrainer, + EntriesList, + EntryNavLink +} from '../../../styles/hub-pages'; + +import indicatorsList from '../'; + +class IndicatorsHub extends React.Component { + render () { + const { indicatorsList } = this.props; + + return ( + + + + + + Indicators + + + + + + +

    + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Iure + molestias deserunt blanditiis veritatis, porro exercitationem + quaerat pariatur fugit nam iusto cum ullam animi? Velit + voluptatibus provident deserunt, corrupti natus porro. + Expedita repudiandae qui at ab eveniet nihil laborum eligendi + numquam nemo error, fuga, quasi natus debitis. Soluta labore + sed, rem autem alias accusamus dignissimos, nam aut suscipit + voluptas, harum nemo! +

    +

    + Porro aliquid sed veritatis cumque maiores adipisci ea et + perspiciatis officia deserunt perferendis assumenda mollitia + ab nihil quas similique aspernatur labore ipsa asperiores, eum + minima repudiandae, at fugiat! Totam, delectus! Ab aut + necessitatibus delectus pariatur eaque eveniet velit + consequuntur nam odio minus. Non est reiciendis, eveniet aut, + ut esse ratione libero temporibus inventore, enim vitae alias + necessitatibus error pariatur in. +

    +
    + + + Indicators + + + {indicatorsList.map((item) => ( +
  • + + {item.name} + +
  • + ))} + + + + + + ); + } +} + +IndicatorsHub.propTypes = { + indicatorsList: T.array +}; + +function mapStateToProps (state, props) { + return { + indicatorsList: indicatorsList.filter(d => !!d.LongForm) + }; +} + +const mapDispatchToProps = {}; + +export default connect(mapStateToProps, mapDispatchToProps)(IndicatorsHub); diff --git a/app/assets/scripts/components/indicators/index.js b/app/assets/scripts/components/indicators/index.js new file mode 100644 index 00000000..d67ee2ac --- /dev/null +++ b/app/assets/scripts/components/indicators/index.js @@ -0,0 +1,49 @@ +/** + * How to add a new indicator page: + * 1) Create a file for the page inside scripts/components/indicators/. + * The name of the file should be `indicator-[name].js + * The file will have to export an object with the following properties: + * - id: string + * - color: string + * - name: string + * - LongForm: ReactComponent + * + * The react component will dictate how the indicator page is rendered. If set + * to null, no page will be created for the indicator. + * + * The file should look something like: + * const metadata = { + * id: 'no2', + * name: 'Nitrogen Dioxide', + * color: '#2276AC' + * }; + * + * class NO2LongForm extends React.Component { + * render () { + * return

    This is the content for the no2 indicator

    + * } + * } + * + * export default { + * ...metadata, + * LongForm: NO2LongForm + * }; + * + * 2) Import the page below. + * + * 3) Add the page to the indicators array + * + */ +import no2 from './indicator-no2'; +import bm from './indicator-nightlights'; +import population from './indicator-population'; + +const indicators = [ + bm, + no2, + population +]; + +export default indicators; + +export const getIndicator = (id) => indicators.find((d) => d.id === id); diff --git a/app/assets/scripts/components/indicators/indicator-nightlights.js b/app/assets/scripts/components/indicators/indicator-nightlights.js new file mode 100644 index 00000000..55d620e5 --- /dev/null +++ b/app/assets/scripts/components/indicators/indicator-nightlights.js @@ -0,0 +1,229 @@ +import React from 'react'; +import styled from 'styled-components'; +import ReactCompareImage from 'react-compare-image'; + +import Prose from '../../styles/type/prose'; +import Gridder from '../../styles/gridder'; +import InpageHGroup from '../../styles/inpage-hgroup'; +import { Fold, FoldDetails } from '../../styles/fold'; +import MediaImage, { MediaCompare } from '../../styles/media-image'; + +import { glsp } from '../../styles/utils/theme-values'; +import config from '../../config'; + +const { baseUrl } = config; + +const IntroFold = styled(Fold)` + padding-bottom: 0; + + ${Prose} { + grid-column: content-start / content-10; + } +`; + +const ResearchFold = styled(Fold)` + padding-bottom: 0; + + ${Gridder} { + align-items: center; + } + + ${MediaImage} { + grid-column: content-8 / full-end; + grid-row: 1; + } + + ${FoldDetails} { + grid-column: content-start / content-8; + text-align: left; + } +`; + +const DataFold = styled(Fold)` + padding-bottom: 0; + + ${Gridder} { + align-items: center; + } + + ${MediaCompare} { + grid-column: full-start / content-8; + grid-row: 1; + } + + ${FoldDetails} { + grid-column: content-8 / content-end; + text-align: left; + } +`; + +const FactsFold = styled(Fold)` + padding-bottom: ${glsp(6)}; + + ${Gridder} { + align-items: center; + } + + /* stylelint-disable-next-line */ + ${InpageHGroup} { + grid-row: 1; + grid-column: content-start / content-7; + } + + ${Prose} { + grid-column: content-start / content-end; + grid-row: 2; + margin-bottom: ${glsp(2)}; + } +`; + +const CreditsFold = styled(FactsFold)` + padding-bottom: 0; + + ${Prose} { + grid-column: content-start / content-7; + } +`; + +const metadata = { + id: 'bm', + name: 'Nighttime Lights', + color: '#2276AC' +}; + +class BMLongForm extends React.Component { + render () { + return ( + + + + +

    + Images of the Earth at night give us an extraordinary view of human activity over time. The nighttime environment illuminates Earth features like city infrastructure, lightning flashes, fishing boats navigating open water, gas flares, aurora, and natural hazards like lava flowing from an active volcano. Paired with the moonlight, researchers can also spot snow and ice, as well as other reflective surfaces that allow nighttime land and ocean analysis. +

    +

    + During the COVID-19 pandemic, researchers are using night light observations to track variations in energy use, migration, and transportation in response to social distancing and lockdown measures. +

    +
    +
    +
    + + + + + + +

    + Nightlights data are collected by the Visible Infrared Radiometer Suite (VIIRS) Day/Night Band (DNB) on the Suomi-National Polar-Orbiting Partnership (Suomi-NPP) platform, a joint NOAA (National Oceanic and Atmospheric Administration) and NASA satellite. The images are produced by NASA’s Black Marble products suite. + All data are calibrated daily, corrected, and validated with ground measurements for science-ready analysis. +

    +

    + New research funded by NASA’s Rapid Response and Novel Research in the Earth Sciences + (RRNES) program seeks to better understand what nightlights can tell us + about the impacts of COVID-19. +

    +
    +
    + +
    +
    + + + + + + +

    + Each spotlight city has a slider for turning night lights on and off. The + darker purple indicates fewer night lights while the lighter yellow + indicates more night lights. By comparing regions before and after + guidelines to shelter-in-place began, researchers are able to visualize + the extent to which social distancing measures impacted various economic + activities based on whether or not light pollution increased or decreased, + which highways were shut down, and which cities stayed the same. +

    +

    + The products featured are 500-meter (VNP46) and 30-meter Black Marble + High Definition (HD) nighttime lights. Black Marble HD downscales radiances + from the 500-meter product to street level using optical imagery from + Landsat 8, a NASA and USGS (U.S. Geological Survey) satellite, along with + OpenStreetMap ancillary layers. This helps visualize neighborhoods and + commercial centers that have less activity – or closures – due to social + distancing restrictions. +

    +
    +
    + + + +
    +
    + + + + + +

    + Black Marble data courtesy of Universities Space Research Association (USRA) + Earth from Space Institute (EfSI) and NASA Goddard Space Flight Center's + Terrestrial Information Systems Laboratory using VIIRS day-night band data + from the Suomi National Polar-orbiting Partnership and Landsat-8 Operational + Land Imager (OLI) data from the U.S. Geological Survey. +

    +
    +
    +
    + + + + + +

    NASA Features

    + +

    Explore the Data

    + +

    Explore the Missions

    + +
    +
    +
    +
    + ); + } +} + +BMLongForm.propTypes = {}; + +export default { + ...metadata, + LongForm: BMLongForm +}; diff --git a/app/assets/scripts/components/indicators/indicator-no2.js b/app/assets/scripts/components/indicators/indicator-no2.js new file mode 100644 index 00000000..6edfb23a --- /dev/null +++ b/app/assets/scripts/components/indicators/indicator-no2.js @@ -0,0 +1,283 @@ +import React from 'react'; +import styled from 'styled-components'; +import ReactCompareImage from 'react-compare-image'; + +import Prose from '../../styles/type/prose'; +import Constrainer from '../../styles/constrainer'; +import Gridder from '../../styles/gridder'; +import InpageHGroup from '../../styles/inpage-hgroup'; +import { Fold, FoldDetails } from '../../styles/fold'; +import { + IntroLead +} from '../../styles/datasets'; +import MediaImage, { MediaCompare } from '../../styles/media-image'; + +import { glsp } from '../../styles/utils/theme-values'; +import config from '../../config'; + +const { baseUrl } = config; + +const IntroFold = styled(Fold)` + padding-bottom: 0; + + ${Gridder} { + align-items: center; + } + + ${MediaCompare} { + grid-column: full-start / content-8; + grid-row: 1; + } + + ${FoldDetails} { + grid-column: content-8 / content-end; + text-align: left; + } +`; + +const FactsFold = styled(Fold)` + padding-bottom: ${glsp(6)}; + + ${Gridder} { + align-items: center; + } + + /* stylelint-disable-next-line */ + ${InpageHGroup} { + grid-row: 1; + grid-column: content-start / content-7; + } + + ${Prose} { + grid-column: content-start / content-end; + grid-row: 2; + margin-bottom: ${glsp(2)}; + } +`; + +const CreditsFold = styled(FactsFold)` + padding-bottom: 0; + + ${Prose} { + grid-column: content-start / content-7; + } +`; + +const InterpretDataFold = styled(Fold)` + padding-bottom: 0; + + ${Gridder} { + align-items: center; + } + + ${MediaImage} { + grid-column: content-start / content-8; + grid-row: 1; + + figcaption { + max-width: 30rem; + } + } + + ${FoldDetails} { + grid-column: content-8 / content-end; + text-align: left; + } +`; + +const metadata = { + id: 'no2', + name: 'Nitrogen Dioxide', + color: '#2276AC' +}; + +class NO2LongForm extends React.Component { + render () { + return ( + + + + + Since the onset of COVID-19, atmospheric concentrations of nitrogen dioxide have changed by as much as 60% in some regions. + + + + + + + + +

    + NO2 is a common air pollutant primarily emitted from the burning of fossil fuels in cars, + power plants and industrial facilities. Lower to the ground, NO2 can directly irritate peoples' + lungs and contributes to the production of particulate pollution and smog when it reacts with sunlight. +

    +

    + During the COVID-19 pandemic, scientists observed considerable decreases in NO2 levels + around the world. These decreases are predominantly associated with changing human behavior in + response to the spread of COVID-19. As communities worldwide have implemented lockdown restrictions + in an attempt to stem the spread of the virus, the reduction in human transportation activity has + resulted in less NO2 emitted into the atmosphere. +

    +

    + These changes are particularly apparent over large urban areas and economic corridors, which + typically have high levels of automobile traffic, airline flights, and other related activity. +

    +

    + NASA has been able to observe subsequent rebounds in nitrogen dioxide as the lockdown restrictions ease. +

    +
    +
    + + +
    + NO2 levels fell by as much as 30% over much of the Northeast U.S. Credit: NASA Scientific Visualization Studio +
    +
    +
    +
    + + + + + +

    + Ongoing research by + scientists in the Atmospheric Chemistry and Dynamics Laboratory at NASA’s + Goddard Space Flight Center and new research funded + by NASA's Rapid Response and Novel research in the Earth Sciences + (RRNES) program element seek to better understand the atmospheric effects + of the COVID-19 shutdowns. +

    +

    + For nitrogen dioxide levels related to COVID-19, NASA uses data collected + by the joint NASA/Royal Netherlands Meteorological Institute (KNMI) Ozone Monitoring Instrument (OMI) aboard + the Aura satellite, as well as data collected by the Tropospheric + Monitoring Instrument (TROPOMI) aboard the European Commission’s Copernicus + Sentinel-5P satellite, built by the European Space Agency. +

    +

    + OMI, which launched in 2004, is the predecessor to TROPOMI. Although TROPOMI, + which launched in 2017, provides higher resolution information, the longer + OMI data record provides good context for the current TROPOMI observations. +

    +

    + Scientists will be using these data to investigate how travel bans and lockdown + orders related to COVID-19 are impacting regional air quality and chemistry, as + well as why these restrictions may be having inconsistent effects on air quality + around the world. +

    +
    +
    +
    + + + + + + +

    + Each spotlight city has a slider for turning nitrogen dioxide data on and off. The darker purple + indicates higher levels of nitrogen dioxide associated with increased travel and economic activity, + while the lighter blues indicate lower levels of NO2 and decreased activity. +

    +

    + Nitrogen dioxide has a relatively short lifetime in the atmosphere. Once it is emitted, it lasts + only a few hours before it dissipates, so it does not travel far from its source. +

    +

    + Because nitrogen dioxide is primarily emitted from burning fossil fuels, changes in its + atmospheric concentration can be related to changes in human activity if the data are properly + processed and interpreted. +

    +

    + However, care must be taken when interpreting satellite NO 2 data, as the quantity observed by + satellite is not exactly the same as the abundance at ground level, and natural variations in + weather (e.g., temperature, wind speed, solar intensity) influence the amount of NO 2 in the + atmosphere. In addition, the OMI and TROPOMI instruments cannot observe the NO 2 + abundance underneath clouds. For more information on processing and cautions related to + interpreting this data, please click here. +

    +

    + Scientists will be using this data to investigate how travel bans and lockdown orders related to COVID-19 are impacting regional air quality and chemistry. +

    +
    +
    + + NO2 levels over South America from the Ozone Monitoring Instrument. The dark green areas in the northwest indicate areas of no data, most likely associated with cloud cover or snow. + +
    +
    + + + + + +

    + Nitrogen dioxide data courtesy of NASA Goddard Space Flight Center's Atmospheric Chemistry and Dynamics Laboratory using OMI data from the Aura satellite. +

    +
    +
    +
    + + + + + +

    NASA Features

    + +

    Explore the Data

    + +

    Explore the Missions

    + +
    +
    +
    +
    + ); + } +} + +NO2LongForm.propTypes = {}; + +export default { + ...metadata, + LongForm: NO2LongForm +}; diff --git a/app/assets/scripts/components/datasets/dataset-population.js b/app/assets/scripts/components/indicators/indicator-population.js similarity index 100% rename from app/assets/scripts/components/datasets/dataset-population.js rename to app/assets/scripts/components/indicators/indicator-population.js diff --git a/app/assets/scripts/components/datasets/single/index.js b/app/assets/scripts/components/indicators/single/index.js similarity index 55% rename from app/assets/scripts/components/datasets/single/index.js rename to app/assets/scripts/components/indicators/single/index.js index f1b11c5a..94edbea7 100644 --- a/app/assets/scripts/components/datasets/single/index.js +++ b/app/assets/scripts/components/indicators/single/index.js @@ -14,27 +14,27 @@ import { } from '../../../styles/inpage'; import UhOh from '../../uhoh'; -import { getDataset } from '../'; +import { getIndicator } from '../'; -class DatasetsSingle extends React.Component { +class IndicatorsSingle extends React.Component { render () { - const { dataset } = this.props; + const { indicator } = this.props; - if (!dataset || !dataset.LongForm) return ; + if (!indicator || !indicator.LongForm) return ; return ( - + - {dataset.name} - Dataset + {indicator.name} + Indicator - + @@ -42,17 +42,17 @@ class DatasetsSingle extends React.Component { } } -DatasetsSingle.propTypes = { - dataset: T.object +IndicatorsSingle.propTypes = { + indicator: T.object }; function mapStateToProps (state, props) { - const { datasetId } = props.match.params; + const { indicatorId } = props.match.params; return { - dataset: getDataset(datasetId) + indicator: getIndicator(indicatorId) }; } const mapDispatchToProps = {}; -export default connect(mapStateToProps, mapDispatchToProps)(DatasetsSingle); +export default connect(mapStateToProps, mapDispatchToProps)(IndicatorsSingle); diff --git a/app/assets/scripts/components/sandbox/line-chart.js b/app/assets/scripts/components/sandbox/line-chart.js index db8d64e1..a9ccbf12 100644 --- a/app/assets/scripts/components/sandbox/line-chart.js +++ b/app/assets/scripts/components/sandbox/line-chart.js @@ -44,162 +44,168 @@ const chartData = { indicator: [6, 21] }, highlightBands: [ - ['2019-03-01', '2019-05-01'], - ['2020-01-01', '2020-02-01'] + { + label: 'Detection', + interval: ['2019-12-01', '2019-12-31'] + }, + { + label: 'Emergency state', + interval: ['2020-01-16', '2020-03-15'] + } ], data: [ { date: '2019-01-01', indicator: 13, - cMax: 16, - cMin: 10, + indicator_conf_high: 16, + indicator_conf_low: 10, baseline: 27, - baselineMax: 32, - baselineMin: 23 + baseline_conf_high: 32, + baseline_conf_low: 23 }, { date: '2019-02-01', indicator: 11, - cMax: 13, - cMin: 9, + indicator_conf_high: 13, + indicator_conf_low: 9, baseline: 25, - baselineMax: 29, - baselineMin: 22 + baseline_conf_high: 29, + baseline_conf_low: 22 }, { date: '2019-03-01', indicator: 14, - cMax: 16, - cMin: 12, + indicator_conf_high: 16, + indicator_conf_low: 12, baseline: 28, - baselineMax: 32, - baselineMin: 25 + baseline_conf_high: 32, + baseline_conf_low: 25 }, { date: '2019-04-01', indicator: 17, - cMax: 20, - cMin: 14, + indicator_conf_high: 20, + indicator_conf_low: 14, baseline: 31, - baselineMax: 36, - baselineMin: 27 + baseline_conf_high: 36, + baseline_conf_low: 27 }, { date: '2019-05-01', indicator: 6, - cMax: 7, - cMin: 5, + indicator_conf_high: 7, + indicator_conf_low: 5, baseline: 20, - baselineMax: 23, - baselineMin: 19 + baseline_conf_high: 23, + baseline_conf_low: 19 }, { date: '2019-06-01', indicator: 9, - cMax: 10, - cMin: 8, + indicator_conf_high: 10, + indicator_conf_low: 8, baseline: 23, - baselineMax: 26, - baselineMin: 21 + baseline_conf_high: 26, + baseline_conf_low: 21 }, { date: '2019-07-01', indicator: 8, - cMax: 9, - cMin: 8, + indicator_conf_high: 9, + indicator_conf_low: 8, baseline: 22, - baselineMax: 25, - baselineMin: 21 + baseline_conf_high: 25, + baseline_conf_low: 21 }, { date: '2019-08-01', indicator: 6, - cMax: 6, - cMin: 6, + indicator_conf_high: 6, + indicator_conf_low: 6, baseline: 20, - baselineMax: 22, - baselineMin: 19 + baseline_conf_high: 22, + baseline_conf_low: 19 }, { date: '2019-09-01', indicator: 7, - cMax: 8, - cMin: 5, + indicator_conf_high: 8, + indicator_conf_low: 5, baseline: 21, - baselineMax: 24, - baselineMin: 19 + baseline_conf_high: 24, + baseline_conf_low: 19 }, { date: '2019-10-01', indicator: 7, - cMax: 10, - cMin: 6, + indicator_conf_high: 10, + indicator_conf_low: 6, baseline: 21, - baselineMax: 26, - baselineMin: 19 + baseline_conf_high: 26, + baseline_conf_low: 19 }, { date: '2019-11-01', indicator: 17, - cMax: 22, - cMin: 15, + indicator_conf_high: 22, + indicator_conf_low: 15, baseline: 31, - baselineMax: 38, - baselineMin: 28 + baseline_conf_high: 38, + baseline_conf_low: 28 }, { date: '2019-12-01', indicator: 10, - cMax: 11, - cMin: 9, + indicator_conf_high: 11, + indicator_conf_low: 9, baseline: 24, - baselineMax: 27, - baselineMin: 22 + baseline_conf_high: 27, + baseline_conf_low: 22 }, { date: '2020-01-01', indicator: 21, - cMax: 25, - cMin: 15, + indicator_conf_high: 25, + indicator_conf_low: 15, baseline: 35, - baselineMax: 41, - baselineMin: 28 + baseline_conf_high: 41, + baseline_conf_low: 28 }, { date: '2020-02-01', indicator: 6, - cMax: 7, - cMin: 5, + indicator_conf_high: 7, + indicator_conf_low: 5, baseline: 20, - baselineMax: 23, - baselineMin: 19 + baseline_conf_high: 23, + baseline_conf_low: 19 }, { date: '2020-03-01', indicator: 9, - cMax: 9, - cMin: 6, + indicator_conf_high: 9, + indicator_conf_low: 6, baseline: 23, - baselineMax: 25, - baselineMin: 19 + baseline_conf_high: 25, + baseline_conf_low: 19 }, { date: '2020-04-01', indicator: 17, - cMax: 20, - cMin: 16, + indicator_conf_high: 20, + indicator_conf_low: 16, baseline: 31, - baselineMax: 36, - baselineMin: 29 + baseline_conf_high: 36, + baseline_conf_low: 29 }, { date: '2020-05-01', indicator: 9, - cMax: 10, - cMin: 7, + indicator_conf_high: 10, + indicator_conf_low: 7, baseline: 23, - baselineMax: 26, - baselineMin: 20 + baseline_conf_high: 26, + baseline_conf_low: 20 } ] }; diff --git a/app/assets/scripts/components/spotlight/hub/index.js b/app/assets/scripts/components/spotlight/hub/index.js index 1b474cf8..7be7fc93 100644 --- a/app/assets/scripts/components/spotlight/hub/index.js +++ b/app/assets/scripts/components/spotlight/hub/index.js @@ -1,7 +1,5 @@ import React from 'react'; import T from 'prop-types'; -import styled from 'styled-components'; -import { NavLink } from 'react-router-dom'; import { connect } from 'react-redux'; import App from '../../common/app'; @@ -13,64 +11,16 @@ import { InpageTitle, InpageBody } from '../../../styles/inpage'; +import Prose from '../../../styles/type/prose'; +import Heading from '../../../styles/type/heading'; +import { + PageConstrainer, + EntriesList, + EntryNavLink +} from '../../../styles/hub-pages'; -import { glsp } from '../../../styles/utils/theme-values'; -import { themeVal } from '../../../styles/utils/general'; import { wrapApiResult } from '../../../redux/reduxeed'; -const InpageTrendsBody = styled(InpageBody)` - position: relative; - z-index: 9; - display: grid; - grid-template-columns: repeat(12, 1fr); - height: 100%; - padding: ${glsp(1, 3)}; -`; - -const PanelNavLink = styled(NavLink)` - position: relative; - display: block; - padding: ${glsp()}; - font-size: 1rem; - line-height: 1.25rem; - transition: all 0.16s ease 0s; - box-shadow: inset 0 0 0 1px ${themeVal('color.baseAlphaB')}; - border-radius: ${themeVal('shape.rounded')}; - - &, - &:visited { - color: inherit; - } - - &:hover { - background: ${themeVal('color.baseAlphaA')}; - box-shadow: inset 0 0 0 1px ${themeVal('color.baseAlphaC')}; - opacity: 1; - } - - &:after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: ${glsp(0.125)}; - content: ''; - background: ${themeVal('color.baseLight')}; - opacity: 0; - transition: all 0.16s ease 0s; - } - - &.active, - &.active:hover { - background: rgba(255, 255, 255, 0.04); - font-weight: bold; - - &:after { - opacity: 1; - } - } -`; - class SpotlightAreasHub extends React.Component { render () { const { spotlightList } = this.props; @@ -87,20 +37,49 @@ class SpotlightAreasHub extends React.Component { - -
      - {spotlightAreas && spotlightAreas.map((item) => ( -
    • - - {item.label} - -
    • - ))} -
    -
    + + + +

    + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Iure + molestias deserunt blanditiis veritatis, porro exercitationem + quaerat pariatur fugit nam iusto cum ullam animi? Velit + voluptatibus provident deserunt, corrupti natus porro. + Expedita repudiandae qui at ab eveniet nihil laborum eligendi + numquam nemo error, fuga, quasi natus debitis. Soluta labore + sed, rem autem alias accusamus dignissimos, nam aut suscipit + voluptas, harum nemo! +

    +

    + Porro aliquid sed veritatis cumque maiores adipisci ea et + perspiciatis officia deserunt perferendis assumenda mollitia + ab nihil quas similique aspernatur labore ipsa asperiores, eum + minima repudiandae, at fugiat! Totam, delectus! Ab aut + necessitatibus delectus pariatur eaque eveniet velit + consequuntur nam odio minus. Non est reiciendis, eveniet aut, + ut esse ratione libero temporibus inventore, enim vitae alias + necessitatibus error pariatur in. +

    +
    + + + Spotlight Areas + + + {spotlightAreas && + spotlightAreas.map((item) => ( +
  • + + {item.label} + +
  • + ))} +
    +
    +
    ); diff --git a/app/assets/scripts/components/spotlight/single/index.js b/app/assets/scripts/components/spotlight/single/index.js index 4bb7228c..10bdc56c 100644 --- a/app/assets/scripts/components/spotlight/single/index.js +++ b/app/assets/scripts/components/spotlight/single/index.js @@ -16,18 +16,17 @@ import { } from '../../../styles/inpage'; import MbMap from '../../common/mb-map-explore/mb-map'; import UhOh from '../../uhoh'; -import LineChart from '../../common/line-chart/chart'; import DataLayersBlock from '../../common/data-layers-block'; -import Panel, { PanelHeadline, PanelTitle } from '../../common/panel'; +import Panel, { PanelHeadline } from '../../common/panel'; import MapMessage from '../../common/map-message'; import Timeline from '../../common/timeline'; +import SecPanel from './sec-panel'; +import Heading from '../../../styles/type/heading'; import { themeVal } from '../../../styles/utils/general'; -import { glsp } from '../../../styles/utils/theme-values'; import { fetchSpotlightSingle as fetchSpotlightSingleAction } from '../../../redux/spotlight'; import { wrapApiResult, getFromState } from '../../../redux/reduxeed'; import { showGlobalLoading, hideGlobalLoading } from '../../common/global-loading'; -import { utcDate } from '../../../utils/utils'; import allMapLayers from '../../common/layers'; import { setLayerState, @@ -43,12 +42,12 @@ import { } from '../../../utils/map-explore-utils'; const layersBySpotlight = { - be: ['no2', 'car-count'], - du: ['no2'], - gh: ['no2'], - la: ['no2'], - sf: ['no2'], - tk: ['no2', 'nightlights'] + be: ['no2', 'nightlights-hd', 'nightlights-viirs', 'car-count'], + du: ['no2', 'nightlights-hd', 'nightlights-viirs'], + gh: ['no2', 'nightlights-hd', 'nightlights-viirs'], + la: ['no2', 'nightlights-hd', 'nightlights-viirs'], + sf: ['no2', 'nightlights-hd', 'nightlights-viirs'], + tk: ['no2', 'nightlights-hd', 'nightlights-viirs'] }; const ExploreCanvas = styled.div` @@ -74,14 +73,6 @@ const PrimePanel = styled(Panel)` width: 18rem; `; -const SecPanel = styled(Panel)` - width: 24rem; -`; - -const PanelBodyInner = styled.div` - padding: ${glsp()}; -`; - class SpotlightAreasSingle extends React.Component { constructor (props) { super(props); @@ -160,11 +151,18 @@ class SpotlightAreasSingle extends React.Component { } render () { - const { spotlight } = this.props; + const { spotlight, indicatorGroups } = this.props; - if (spotlight.hasError()) return ; + if (spotlight.hasError() || indicatorGroups.hasError()) return ; - const spotlightData = spotlight.getData(); + const { + label, + indicators + } = spotlight.getData(); + + const indicatorGroupsData = indicatorGroups.isReady() + ? indicatorGroups.getData() + : null; const layers = this.getLayersWithState(); const activeTimeseriesLayers = this.getActiveTimeseriesLayers(); @@ -196,7 +194,7 @@ class SpotlightAreasSingle extends React.Component { onPanelChange={this.resizeMap} headerContent={ -

    {spotlightData.label}

    + {label}
    } bodyContent={ @@ -228,42 +226,12 @@ class SpotlightAreasSingle extends React.Component { onSizeChange={this.resizeMap} /> + - Insights - - } - bodyContent={ - - {spotlightData.indicators.length ? spotlightData.indicators.map(ind => { - const xDomain = ind.domain.date.map(utcDate); - const yDomain = ind.domain.indicator; - - return ( - -

    {ind.name}

    - {ind.description &&

    {ind.description}

    } - -
    - ); - }) : ( -

    Detailed information for the area being viewed and/or interacted by the user.

    - )} -
    - } + indicators={indicators} + indicatorGroups={indicatorGroupsData} + selectedDate={activeTimeseriesLayers.length ? this.state.timelineDate : null} /> @@ -278,15 +246,61 @@ SpotlightAreasSingle.propTypes = { fetchSpotlightSingle: T.func, mapLayers: T.array, spotlight: T.object, + indicatorGroups: T.object, match: T.object }; function mapStateToProps (state, props) { const { spotlightId } = props.match.params; const layersToUse = layersBySpotlight[spotlightId] || []; + // Filter by the layers to include & + // Replace the {site} property on the layers + const spotlightMapLayers = allMapLayers + .filter(l => layersToUse.includes(l.id)) + .map(l => { + // This layer requires a special handling. + if (l.id === 'nightlights-viirs') { + const spotlightName = { + be: 'Beijing', + gh: 'EUPorts', + du: 'EUPorts', + la: 'LosAngeles', + sf: 'SanFrancisco', + tk: 'Tokyo' + }[spotlightId]; + + return { + ...l, + domain: l.domain.filter(d => { + if (spotlightName === 'Beijing') { + const dates = ['2020-03-18']; + return !dates.includes(d); + } else if (spotlightName === 'EUPorts') { + const dates = ['2020-05-05', '2020-05-07', '2020-05-11', '2020-05-13', '2020-05-16', '2020-05-18', '2020-05-19']; + return !dates.includes(d); + } + return true; + }), + source: { + ...l.source, + tiles: l.source.tiles.map(t => t.replace('{spotlightName}', spotlightName)) + } + }; + } else { + return { + ...l, + source: { + ...l.source, + tiles: l.source.tiles.map(t => t.replace('{spotlightId}', spotlightId)) + } + }; + } + }); + return { - mapLayers: allMapLayers.filter(l => layersToUse.includes(l.id)), - spotlight: wrapApiResult(getFromState(state, ['spotlight', 'single', spotlightId])) + mapLayers: spotlightMapLayers, + spotlight: wrapApiResult(getFromState(state, ['spotlight', 'single', spotlightId])), + indicatorGroups: wrapApiResult(state.indicators.groups) }; } diff --git a/app/assets/scripts/components/spotlight/single/sec-panel.js b/app/assets/scripts/components/spotlight/single/sec-panel.js new file mode 100644 index 00000000..bc8de780 --- /dev/null +++ b/app/assets/scripts/components/spotlight/single/sec-panel.js @@ -0,0 +1,176 @@ +import React from 'react'; +import T from 'prop-types'; +import styled from 'styled-components'; + +import LineChart from '../../common/line-chart/chart'; +import Panel, { PanelHeadline, PanelTitle } from '../../common/panel'; +import ShadowScrollbar from '../../common/shadow-scrollbar'; +import { + PanelBlock, + PanelBlockHeader, + PanelBlockTitle +} from '../../common/panel-block'; +import { Accordion, AccordionFold } from '../../common/accordion'; +import Heading from '../../../styles/type/heading'; + +import { glsp } from '../../../styles/utils/theme-values'; +import { utcDate } from '../../../utils/utils'; +import collecticon from '../../../styles/collecticons'; +import Prose from '../../../styles/type/prose'; + +const PanelSelf = styled(Panel)` + width: 30rem; +`; + +const BodyScroll = styled(ShadowScrollbar)` + flex: 1; + z-index: 1; +`; + +export const AccordionFoldTrigger = styled.a` + display: flex; + align-items: center; + margin: -${glsp(0.5)} -${glsp()}; + padding: ${glsp(0.5)} ${glsp()}; + + &, + &:visited { + color: inherit; + } + + &:after { + ${collecticon('chevron-down--small')} + margin-left: auto; + transition: transform 240ms ease-in-out; + transform: ${({ isExpanded }) => + isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'}; + } +`; + +const PanelBodyInner = styled.div` + padding: ${glsp()}; + + > *:not(:last-child) { + margin-bottom: ${glsp(1.5)}; + } + + figure { + margin-top: -1rem; + } +`; + +const Attribution = styled.p` + font-size: 0.874rem; + text-align: right; + font-style: italic; + padding-right: ${glsp(2)}; + margin-bottom: ${glsp()}; +`; + +export default function SecPanel (props) { + const { onPanelChange, indicators, indicatorGroups, selectedDate } = props; + + // Ensure that we only deal with groups that have data. + const groups = (indicatorGroups || []).filter(g => ( + g.indicators.some(indId => indicators.find(ind => ind.id === indId)) + )); + + return ( + + Insights + + } + bodyContent={ + + + {({ checkExpanded, setExpanded }) => ( + !!groups.length && groups.map((group, idx) => ( + setExpanded(idx, v)} + renderHeader={({ isFoldExpanded, setFoldExpanded }) => ( + + setFoldExpanded(!isFoldExpanded)} + > + {group.label} + + + )} + renderBody={() => ( + + {group.prose && ( + +

    {group.prose}

    +
    + )} + {group.indicators.map((indId) => { + const ind = indicators.find((o) => o.id === indId); + if (!ind) return null; + + const xDomain = ind.domain.date.map(utcDate); + const yDomain = ind.domain.indicator; + + return ( +
    + + {ind.name} + {ind.description &&

    {ind.description}

    } +
    + + {ind.attribution && ( +
    + + By: {ind.attribution} + +
    + )} +
    + {ind.notes &&

    {ind.notes}

    } +
    +
    + ); + })} +
    + )} + /> + )) + )} +
    +
    + } + /> + ); +} + +SecPanel.propTypes = { + onPanelChange: T.func, + indicators: T.array, + indicatorGroups: T.array, + selectedDate: T.object +}; diff --git a/app/assets/scripts/config/production.js b/app/assets/scripts/config/production.js index 9802f5f8..7c41d595 100644 --- a/app/assets/scripts/config/production.js +++ b/app/assets/scripts/config/production.js @@ -2,15 +2,14 @@ export default { environment: 'production', appTitle: 'COVID-19 Dashboard', appDescription: 'COVID-19 Dashboard', - appVersion: 'v0.01', gaTrackingCode: null, - mbToken: 'pk.eyJ1IjoiY292aWQtc3VwcG9ydCIsImEiOiJjazlhMTNweDIwMHd2M21venc1Nzdzdzh5In0.QZbkhaPpO9jRclw-dQWnDA', + mbToken: 'pk.eyJ1IjoiY292aWQtbmFzYSIsImEiOiJja2F6eHBobTUwMzVzMzFueGJuczF6ZzdhIn0.8va1fkyaWgM57_gZ2rBMMg', api: 'https://8ib71h0627.execute-api.us-east-1.amazonaws.com/v1', map: { center: [0, 0], zoom: 2, minZoom: 1, maxZoom: 20, - styleUrl: 'mapbox://styles/mapbox/light-v10' + styleUrl: 'mapbox://styles/covid-nasa/ckb01h6f10bn81iqg98ne0i2y' } }; diff --git a/app/assets/scripts/main.js b/app/assets/scripts/main.js index 5ff9e3a0..3df67876 100644 --- a/app/assets/scripts/main.js +++ b/app/assets/scripts/main.js @@ -13,6 +13,7 @@ import store from './utils/store'; import history from './utils/history'; import config from './config'; import { fetchSpotlightList } from './redux/spotlight'; +import { fetchIndicatorGroups } from './redux/indicators'; import GlobalStyles from './styles/global'; import ErrorBoundary from './fatal-error-boundary'; @@ -22,8 +23,10 @@ import SizeAwareElement from './components/common/size-aware-element'; // Views import Home from './components/home'; import GlobalExplore from './components/global'; +import SpotlightHub from './components/spotlight/hub'; import SpotlightSingle from './components/spotlight/single'; -import DatasetsSingle from './components/datasets/single'; +import IndicatorsHub from './components/indicators/hub'; +import IndicatorsSingle from './components/indicators/single'; import Sandbox from './components/sandbox'; import UhOh from './components/uhoh'; import About from './components/about'; @@ -31,6 +34,7 @@ import MobileMessage from './components/common/mobile-message'; // Load the spotlight areas list. store.dispatch(fetchSpotlightList()); +store.dispatch(fetchIndicatorGroups()); const { gaTrackingCode } = config; @@ -98,17 +102,27 @@ class Root extends React.Component { path='/global' component={GlobalExplore} /> + + diff --git a/app/assets/scripts/redux/index.js b/app/assets/scripts/redux/index.js index 3b17b484..b3adc525 100644 --- a/app/assets/scripts/redux/index.js +++ b/app/assets/scripts/redux/index.js @@ -5,12 +5,14 @@ import layerData from './layer-data'; import timeSeries from './time-series'; import cogTimeData from './cog-time-data'; import spotlight from './spotlight'; +import indicators from './indicators'; export const reducers = { layerData, timeSeries, cogTimeData, - spotlight + spotlight, + indicators }; export default combineReducers(reducers); diff --git a/app/assets/scripts/redux/indicators.js b/app/assets/scripts/redux/indicators.js new file mode 100644 index 00000000..8a18756e --- /dev/null +++ b/app/assets/scripts/redux/indicators.js @@ -0,0 +1,30 @@ +import { combineReducers } from 'redux'; + +import config from '../config'; +import { makeActions, makeFetchThunk, makeAPIReducer } from './reduxeed'; + +// ///////////////////////////////////////////////////////////////////////////// +// INDICATOR_GROUPS +// ///////////////////////////////////////////////////////////////////////////// + +const indicatorGroupsActions = makeActions('INDICATOR_GROUPS'); + +export function fetchIndicatorGroups () { + return makeFetchThunk({ + url: `${config.api}/indicator_groups`, + cache: true, + requestFn: indicatorGroupsActions.request, + receiveFn: indicatorGroupsActions.receive, + mutator: d => d.groups + }); +} + +const indicatorGroupsReducer = makeAPIReducer('INDICATOR_GROUPS'); + +// ///////////////////////////////////////////////////////////////////////////// +// Export +// ///////////////////////////////////////////////////////////////////////////// + +export default combineReducers({ + groups: indicatorGroupsReducer +}); diff --git a/app/assets/scripts/styles/fold.js b/app/assets/scripts/styles/fold.js index 859f3e5c..d8c4799f 100644 --- a/app/assets/scripts/styles/fold.js +++ b/app/assets/scripts/styles/fold.js @@ -4,6 +4,7 @@ import Constrainer from './constrainer'; import { glsp } from './utils/theme-values'; import Heading from './type/heading'; +import InpageHGroup from './inpage-hgroup'; export const Fold = styled.section` padding: ${glsp(3)} 0; @@ -16,7 +17,9 @@ export const Fold = styled.section` `; export const FoldDetails = styled.div` - /* Defined for reference use */ + ${InpageHGroup} { + margin-bottom: ${glsp(2)}; + } `; export const FoldTitle = styled(Heading)` diff --git a/app/assets/scripts/styles/global.js b/app/assets/scripts/styles/global.js index 1ae3e021..6970e671 100644 --- a/app/assets/scripts/styles/global.js +++ b/app/assets/scripts/styles/global.js @@ -158,7 +158,7 @@ const baseStyles = css` #app-container { position: relative; - overflow-x: hidden; + overflow: hidden; } `; diff --git a/app/assets/scripts/styles/hub-pages/index.js b/app/assets/scripts/styles/hub-pages/index.js new file mode 100644 index 00000000..2f711716 --- /dev/null +++ b/app/assets/scripts/styles/hub-pages/index.js @@ -0,0 +1,75 @@ +import styled from 'styled-components'; +import { NavLink } from 'react-router-dom'; +import Constrainer from '../constrainer'; +import Prose from '../type/prose'; + +import { glsp } from '../utils/theme-values'; +import { themeVal } from '../utils/general'; + +export const EntryNavLink = styled(NavLink)` + position: relative; + display: block; + padding: ${glsp()}; + font-size: 1rem; + line-height: 1.25rem; + transition: all 0.16s ease 0s; + box-shadow: inset 0 0 0 1px ${themeVal('color.baseAlphaB')}; + border-radius: ${themeVal('shape.rounded')}; + + &, + &:visited { + color: inherit; + } + + &:hover { + background: ${themeVal('color.baseAlphaA')}; + box-shadow: inset 0 0 0 1px ${themeVal('color.baseAlphaC')}; + opacity: 1; + } + + &:after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: ${glsp(0.125)}; + content: ''; + background: ${themeVal('color.baseLight')}; + opacity: 0; + transition: all 0.16s ease 0s; + } + + &.active, + &.active:hover { + background: rgba(255, 255, 255, 0.04); + font-weight: bold; + + &:after { + opacity: 1; + } + } +`; + +export const PageConstrainer = styled(Constrainer)` + padding-top: ${glsp(4)}; + padding-bottom: ${glsp(4)}; + + ${Prose} { + max-width: 50rem; + } + + > *:not(:last-child) { + margin-bottom: ${glsp(2)}; + } +`; + +export const EntriesList = styled.ul` + display: grid; + grid-gap: ${glsp(2)}; + grid-template-columns: repeat(12, 1fr); + + li { + text-align: center; + grid-column: auto / span 3; + } +`; diff --git a/app/assets/scripts/styles/inpage-hgroup.js b/app/assets/scripts/styles/inpage-hgroup.js index 1514f5b8..9b4f71b7 100644 --- a/app/assets/scripts/styles/inpage-hgroup.js +++ b/app/assets/scripts/styles/inpage-hgroup.js @@ -57,7 +57,7 @@ function InpageHGroupCmp ({ InpageHGroupCmp.propTypes = { className: T.string, - suptitle: T.string.isRequired, + suptitle: T.string, title: T.string.isRequired, dashColor: T.oneOfType([T.string, T.func]), size: T.string, diff --git a/app/assets/scripts/styles/media-image.js b/app/assets/scripts/styles/media-image.js index a99c857b..51289562 100644 --- a/app/assets/scripts/styles/media-image.js +++ b/app/assets/scripts/styles/media-image.js @@ -2,7 +2,8 @@ import React from 'react'; import styled from 'styled-components'; import { PropTypes as T } from 'prop-types'; -import { glsp } from './utils/theme-values'; +import { glsp, _rgba } from './utils/theme-values'; +import { themeVal } from './utils/general'; const MediaImageFigure = ({ className, @@ -50,3 +51,31 @@ const MediaImage = styled(MediaImageFigure)` `; export default MediaImage; + +export const MediaCompare = styled.figure` + /* Trying to style a bad structured plugin... */ + > div { + > div:nth-child(3) > div:nth-child(2) { + background-color: ${themeVal('color.primary')}; + width: 3rem; + height: 3rem; + } + + > div:nth-child(4) > div:nth-child(1), + > div:nth-child(5) > div:nth-child(1) { + border-radius: ${themeVal('shape.rounded')}; + background-color: ${_rgba(themeVal('color.baseDark'), 0.64)} !important; + } + } + + figcaption { + font-size: 0.875rem; + line-height: 1.5rem; + max-width: 30rem; + } + + /* stylelint-disable-next-line */ + > *:not(:last-child) { + margin-bottom: ${glsp()}; + } +`; diff --git a/app/assets/scripts/styles/type/prose.js b/app/assets/scripts/styles/type/prose.js index c76d25e7..00e6827f 100644 --- a/app/assets/scripts/styles/type/prose.js +++ b/app/assets/scripts/styles/type/prose.js @@ -11,8 +11,8 @@ const numColumnsFn = ({ numColumns }) => numColumns && css` `; const Prose = styled.div` - font-size: ${themeVal('type.base.size')}; /* 16px */ - line-height: ${themeVal('type.base.line')}; /* 24px */ + font-size: ${({ size }) => size === 'small' ? '0.875rem' : themeVal('type.base.size')}; /* 16px */ + line-height: ${({ size }) => size === 'small' ? '1.25rem' : themeVal('type.base.line')}; /* 16px */ ${numColumnsFn} @@ -39,7 +39,7 @@ const Prose = styled.div` } > * { - margin-bottom: ${multiply(themeVal('type.base.size'), themeVal('type.base.line'))}; /* same as line-height */ + margin-bottom: ${({ size }) => size === 'small' ? '1rem' : multiply(themeVal('type.base.size'), themeVal('type.base.line'))}; } > *:last-child { diff --git a/app/assets/scripts/utils/history.js b/app/assets/scripts/utils/history.js index 1fdd1f2d..fa2c5a82 100644 --- a/app/assets/scripts/utils/history.js +++ b/app/assets/scripts/utils/history.js @@ -3,5 +3,8 @@ // The only way to do this is create our own history and pass it to the router. // https://github.com/ReactTraining/react-router/blob/master/FAQ.md#how-do-i-access-the-history-object-outside-of-components import { createBrowserHistory } from 'history'; +import config from '../config'; -export default createBrowserHistory(); +export default createBrowserHistory({ + basename: config.baseUrl ? (new URL(config.baseUrl).pathname) : '/' +}); diff --git a/app/assets/scripts/utils/map-explore-utils.js b/app/assets/scripts/utils/map-explore-utils.js index a83870b5..c2d37f68 100644 --- a/app/assets/scripts/utils/map-explore-utils.js +++ b/app/assets/scripts/utils/map-explore-utils.js @@ -146,7 +146,7 @@ export function handlePanelAction (action, payload) { */ export function getUpdatedActiveLayersState (state, layer) { const { exclusiveWith, id } = layer; - const { activeLayers } = state; + const { activeLayers, layersState } = state; // Hide any layers that are not compatible with the current one. // This means that when this layer gets enabled some layers must be disabled. const exclusiveWithLayers = exclusiveWith || []; @@ -163,8 +163,23 @@ export function getUpdatedActiveLayersState (state, layer) { const diff = activeLayers.filter( (v) => !exclusiveWithLayers.includes(v) ); + + // Disable the comparison on any exclusive layer. + const newLayersState = exclusiveWithLayers.reduce((acc, id) => { + return get(layersState, [id, 'comparing']) + ? { + ...acc, + [id]: { + ...acc[id], + comparing: false + } + } + : acc; + }, state.layersState); + return { - activeLayers: [...diff, id] + activeLayers: [...diff, id], + layersState: newLayersState }; } @@ -220,7 +235,7 @@ export function toggleLayerRasterTimeseries (layer) { } : {}; return { - timelineDate: utcDate(layer.domain[1]), + timelineDate: utcDate(layer.domain[layer.domain.length - 1]), layersState: { ...state.layersState, [layer.id]: { diff --git a/app/assets/scripts/utils/utils.js b/app/assets/scripts/utils/utils.js index 338fc5fc..0ee8aee0 100644 --- a/app/assets/scripts/utils/utils.js +++ b/app/assets/scripts/utils/utils.js @@ -1,4 +1,5 @@ import React from 'react'; +import * as d3 from 'd3'; /** * Calculates the integer remainder of a division of a by n, handling negative @@ -118,3 +119,33 @@ export function filterComponentProps (Comp, filterProps = []) { return ; }); } + +const toDateAccessor = d => utcDate(d.date); +/** + * Returns the closed object to the given date. + * + * @param {array} data Array of data objects. Each object must have a date property + * @param {Date} date The date by which to bisect the array. + */ +export const bisectByDate = (data, date, accessor = toDateAccessor) => { + // Define bisector function. Is used to find where this date would fin in the + // data array + const bisect = d3.bisector(d => accessor(d).getTime()).left; + const mouseDate = date.getTime(); + // Returns the index to the current data item. + const i = bisect(data, mouseDate); + + if (i === 0) { + return data[i]; + } else if (i === data.length) { + return data[i - 1]; + } else { + const docR = data[i]; + const docL = data[i - 1]; + const deltaL = mouseDate - accessor(docL).getTime(); + const deltaR = accessor(docR).getTime() - mouseDate; + return deltaL > deltaR + ? docR + : docL; + } +}; diff --git a/package.json b/package.json index 96d99cee..727d7fdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "covid-dashboard", - "version": "0.3.0", + "version": "0.4.0", "description": "Frontend application for the Covid Dashboard", "repository": { "type": "git", @@ -122,6 +122,7 @@ "polished": "^3.4.1", "prop-types": "^15.7.2", "react": "^16.9.0", + "react-compare-image": "^2.0.4", "react-custom-scrollbars": "^4.2.1", "react-datepicker": "^2.14.1", "react-dom": "^16.9.0", diff --git a/yarn.lock b/yarn.lock index 282af0e7..619c1cdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2900,6 +2900,11 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= +css-element-queries@^1.0.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/css-element-queries/-/css-element-queries-1.2.3.tgz#e14940b1fcd4bf0da60ea4145d05742d7172e516" + integrity sha512-QK9uovYmKTsV2GXWQiMOByVNrLn2qz6m3P7vWpOR4IdD6I3iXoDw5qtgJEN3Xq7gIbdHVKvzHjdAtcl+4Arc4Q== + css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" @@ -8514,6 +8519,14 @@ raw-body@^2.3.2: iconv-lite "0.4.24" unpipe "1.0.0" +react-compare-image@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-compare-image/-/react-compare-image-2.0.4.tgz#d1c6d64cc61852404df538792fb1ee138de38919" + integrity sha512-kuYtu+qOF07XhY0/cJCqXgM26yqXRKtqZS9YLNi18Cd9tE27sqDe/LZCxoMTDiPA3gfq8uM82Q56nzkZMWBJhg== + dependencies: + css-element-queries "^1.0.2" + prop-types "^15.7.2" + react-custom-scrollbars@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db"