diff --git a/README.md b/README.md
index fc598548..73c8d471 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,11 @@
-# Covid Dashboard
+# COVID-19 Dashboard
+As communities around the world have changed their behavior in response to the spread of COVID-19, NASA satellites have observed changes in the environment. This experimental dashboard reflects a rapid response to COVID-19 that is currently underway and will continue to evolve as more data becomes available.
+Visit the live site on: https://earthdata.nasa.gov/covid19/
+
+This dashboard is powered by an [open source API](https://github.com/NASA-IMPACT/covid-api/) that is developed in parallel. This API focuses on serving the Cloud Optimized GeoTIFF and time-series indicator data that people can interact with in the dashboard.
+
+![](https://user-images.githubusercontent.com/751330/85645349-7213ac00-b667-11ea-9ab0-52e2b16d416d.jpg)
## Installation and Usage
The steps below will walk you through setting up your own instance of the project.
diff --git a/app/assets/graphics/content/indicators/la-ship.png b/app/assets/graphics/content/indicators/la-ship.png
new file mode 100644
index 00000000..06b62531
Binary files /dev/null and b/app/assets/graphics/content/indicators/la-ship.png differ
diff --git a/app/assets/graphics/content/la-port.png b/app/assets/graphics/content/la-port.png
deleted file mode 100644
index b2a747d5..00000000
Binary files a/app/assets/graphics/content/la-port.png and /dev/null differ
diff --git a/app/assets/icons/collecticons/calendar.svg b/app/assets/icons/collecticons/calendar.svg
new file mode 100755
index 00000000..921539b1
--- /dev/null
+++ b/app/assets/icons/collecticons/calendar.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/assets/scripts/components/about/index.js b/app/assets/scripts/components/about/index.js
index c11f484a..3563474c 100644
--- a/app/assets/scripts/components/about/index.js
+++ b/app/assets/scripts/components/about/index.js
@@ -46,15 +46,15 @@ export default class About extends React.Component {
our yards more abundant.
- NASA’s continuous and sometimes near-real-time measurements of Earth allow for understanding both the systems
+ The National Aeronautics and Space Administration's (NASA) 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).
+ This dashboard features data collected and analyzed by 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
+ data we have been able to monitor some of those changes and this is allowing us to track and compare these
changes over time.
diff --git a/app/assets/scripts/components/common/date-picker.js b/app/assets/scripts/components/common/date-picker.js
index 2c9ad671..786e7983 100644
--- a/app/assets/scripts/components/common/date-picker.js
+++ b/app/assets/scripts/components/common/date-picker.js
@@ -1,11 +1,13 @@
import React from 'react';
import { PropTypes as T } from 'prop-types';
import styled from 'styled-components';
-import { format } from 'date-fns';
+import { format, isSameYear, isSameMonth } from 'date-fns';
import Dropdown from './dropdown';
import Button from '../../styles/button/button';
import DateRange from './date-range';
+import { FormHelper, FormHelperMessage } from '../../styles/form/helper';
+import { glsp } from '../../styles/utils/theme-values';
const DATE_FORMAT = 'MMM dd, yyyy';
@@ -24,6 +26,25 @@ const DropdownDatePicker = styled(Dropdown)`
}
`;
+const DateRangeSelector = styled(DateRange)`
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+`;
+
+const PickerFooter = styled.div`
+ display: flex;
+
+ & > * {
+ margin-right: ${glsp()};
+ }
+`;
+
+const FormHelperPicker = styled(FormHelper)`
+ margin-top: ${glsp()};
+ max-width: 17rem;
+`;
+
class DatePicker extends React.Component {
constructor (props) {
super(props);
@@ -44,88 +65,158 @@ class DatePicker extends React.Component {
// Committed props are tracked with a double leading underscore.
// See getDerivedStateFromProps below.
// https://reactjs.org/blog/2018/05/23/react-v-16-4.html#bugfix-for-getderivedstatefromprops
- __date: props.dateState,
- date: props.dateState
+ __range: props.dateState,
+ range: props.dateState
};
+ this.onClearClick = this.onClearClick.bind(this);
+ this.onApplyClick = this.onApplyClick.bind(this);
this.onDateChange = this.onDateChange.bind(this);
this.dropdownRef = React.createRef();
}
static getDerivedStateFromProps (props, state) {
- if (props.dateState !== state.__date) {
+ if (props.dateState !== state.__range) {
return {
- __date: props.dateState,
- date: props.dateState
+ __range: props.dateState,
+ range: props.dateState
};
}
return null;
}
+ onClearClick () {
+ this.setState({ range: { start: null, end: null } });
+ }
+
onDropdownClose () {
// There must always be a date set.
// If the date was cleared, reset to original.
- if (!this.state.date) {
+ if (!this.state.range.start && !this.state.range.end) {
this.setState(state => ({
- date: state.__date
+ range: state.__range
}));
}
}
- onDateChange (date) {
- this.setState({ date: date.start }, () => {
- this.props.onChange(this.state.date);
+ onApplyClick () {
+ // If applying only with start, consider that date start and end.
+ this.setState(state => {
+ const {
+ range: { start, end }
+ } = state;
+
+ if (!end) {
+ return {
+ range: { start, end: start }
+ };
+ }
+ }, () => {
+ this.props.onChange(this.state.range);
this.dropdownRef.current.close();
});
}
+ onDateChange (range) {
+ this.setState({ range });
+ }
+
getTriggerLabel () {
- const { dateState } = this.props;
- return dateState ? format(dateState, DATE_FORMAT) : 'Select date';
+ const {
+ dateState: { start, end }
+ } = this.props;
+
+ if (start) {
+ const startStr = format(start, DATE_FORMAT);
+ if (!end) {
+ return `${startStr} — end`;
+ }
+
+ const endStr = format(end, DATE_FORMAT);
+ if (startStr === endStr) {
+ return startStr;
+ }
+
+ if (isSameMonth(start, end) && isSameYear(start, end)) {
+ return `${format(start, 'MMM dd')}-${format(end, 'dd, yyyy')}`;
+ } else if (isSameYear(start, end)) {
+ return `${format(start, 'MMM dd')} — ${endStr}`;
+ } else {
+ return `${startStr} — ${endStr}`;
+ }
+ }
+
+ return 'From start — end';
}
render () {
- const { id, label, dateDomain } = this.props;
- const { date } = this.state;
+ const { id, label, dateDomain, validate } = this.props;
+ const { range } = this.state;
- const dateRangeVal = {
- start: date,
- end: date
- };
+ const validationError = validate(range);
+ const isValidRange = !validationError;
return (
!open && this.onDropdownClose()}
alignment='right'
- direction='up'
+ direction='down'
triggerElement={
-
+
{this.getTriggerLabel()}
}
>
- this.dropdownRef.current.reposition()}
/>
+
+
+ Clear
+
+
+ Apply
+
+
+ {!isValidRange && (
+
+ {validationError}
+
+ )}
);
}
}
DatePicker.propTypes = {
- dateState: T.object,
+ dateState: T.shape({
+ start: T.object,
+ end: T.object
+ }),
dateDomain: T.array,
id: T.string,
label: T.string,
- onChange: T.func
+ onChange: T.func,
+ validate: T.func
};
export default DatePicker;
diff --git a/app/assets/scripts/components/common/date-range.js b/app/assets/scripts/components/common/date-range.js
index 3b5018c7..7239f1da 100644
--- a/app/assets/scripts/components/common/date-range.js
+++ b/app/assets/scripts/components/common/date-range.js
@@ -58,7 +58,7 @@ class DateRange extends React.Component {
allowRange
} = this.props;
- if (allowRange) {
+ if (!allowRange) {
return onChange({
start: date,
end: date
@@ -97,7 +97,8 @@ class DateRange extends React.Component {
value: { start, end },
min,
max,
- allowRange
+ allowRange,
+ className
} = this.props;
const compProps = allowRange ? {
@@ -111,7 +112,7 @@ class DateRange extends React.Component {
};
return (
-
+
{this.state.pickerType === 'day' ? (
Previous Month}
@@ -119,7 +120,7 @@ class DateRange extends React.Component {
onChange={this.onDateSelect}
// selectsStart={!start || !end}
// selectsEnd={!start || !end}
- monthsShown={2}
+ monthsShown={1}
shouldCloseOnSelect={false}
// startDate={start}
// endDate={end || start}
@@ -148,6 +149,7 @@ class DateRange extends React.Component {
}
DateRange.propTypes = {
+ className: T.string,
value: T.shape({
start: T.object,
end: T.object
diff --git a/app/assets/scripts/components/common/dropdown.js b/app/assets/scripts/components/common/dropdown.js
index f84d144f..01ca5e5e 100644
--- a/app/assets/scripts/components/common/dropdown.js
+++ b/app/assets/scripts/components/common/dropdown.js
@@ -381,7 +381,14 @@ const DropContent = styled.div`
.tether-target-attached-top.tether-element-attached-bottom & {
${transitions.up.end}
-
+ &.drop-trans-appear,
+ &.drop-trans-enter {
+ ${transitions.up.start}
+ }
+ &.drop-trans-appear-active,
+ &.drop-trans-enter-active {
+ ${transitions.up.end}
+ }
&.drop-trans-exit {
${transitions.up.end}
}
@@ -393,7 +400,14 @@ const DropContent = styled.div`
.tether-target-attached-bottom.tether-element-attached-top & {
${transitions.down.end}
-
+ &.drop-trans-appear,
+ &.drop-trans-enter {
+ ${transitions.down.start}
+ }
+ &.drop-trans-appear-active,
+ &.drop-trans-enter-active {
+ ${transitions.down.end}
+ }
&.drop-trans-exit {
${transitions.down.end}
}
@@ -405,7 +419,14 @@ const DropContent = styled.div`
.tether-target-attached-right.tether-element-attached-left & {
${transitions.right.end}
-
+ &.drop-trans-appear,
+ &.drop-trans-enter {
+ ${transitions.right.start}
+ }
+ &.drop-trans-appear-active,
+ &.drop-trans-enter-active {
+ ${transitions.right.end}
+ }
&.drop-trans-exit {
${transitions.right.end}
}
@@ -417,7 +438,14 @@ const DropContent = styled.div`
.tether-target-attached-left.tether-element-attached-right & {
${transitions.left.end}
-
+ &.drop-trans-appear,
+ &.drop-trans-enter {
+ ${transitions.left.start}
+ }
+ &.drop-trans-appear-active,
+ &.drop-trans-enter-active {
+ ${transitions.left.end}
+ }
&.drop-trans-exit {
${transitions.left.end}
}
@@ -426,24 +454,6 @@ const DropContent = styled.div`
${transitions.left.start}
}
}
-
- &&.drop-trans-appear,
- &&.drop-trans-enter {
- ${({ direction }) => transitions[direction].start}
- }
-
- &&.drop-trans-enter-active,
- &&.drop-trans-appear-active {
- ${({ direction }) => transitions[direction].end}
- }
-
- &&.drop-trans-exit {
- ${({ direction }) => transitions[direction].end}
- }
-
- &&.drop-trans-exit-active {
- ${({ direction }) => transitions[direction].start}
- }
`;
class TransitionItem extends React.Component {
diff --git a/app/assets/scripts/components/common/layer.js b/app/assets/scripts/components/common/layer.js
index c163e09e..3d9146ca 100644
--- a/app/assets/scripts/components/common/layer.js
+++ b/app/assets/scripts/components/common/layer.js
@@ -20,6 +20,19 @@ const makeGradient = (stops) => {
const steps = stops.map((s, i) => `${s} ${i * d}%`);
return `linear-gradient(to right, ${steps.join(', ')})`;
};
+const renderTitle = input => {
+ const content = input.split('\u2082');
+
+ return (
+ <>
+ {
+ content.reduce((accum, el, ind) => (
+ ind < content.length - 1 ? [...accum, el, 2 ] : [...accum, el]
+ ), [])
+ }
+ >
+ );
+};
const printLegendVal = (val) => typeof val === 'number' ? formatThousands(val, { shorten: true }) : val;
@@ -45,6 +58,10 @@ const LayerTitle = styled.h1`
font-size: 1rem;
line-height: 1.25rem;
margin: 0;
+
+ sub {
+ bottom: 0;
+ }
`;
const LayerSubtitle = styled.p`
@@ -243,7 +260,7 @@ class Layer extends React.Component {
renderHeader={({ isFoldExpanded, setFoldExpanded }) => (
- {label}
+ {renderTitle(label)}
{typesSubtitles[type] || 'Layer'}
Color: {swatchName || 'Waikawa Gray'}
diff --git a/app/assets/scripts/components/common/layers/index.js b/app/assets/scripts/components/common/layers/index.js
index e9f79e5d..5d3bc03c 100644
--- a/app/assets/scripts/components/common/layers/index.js
+++ b/app/assets/scripts/components/common/layers/index.js
@@ -31,7 +31,7 @@ const layersBySpotlight = {
la: ['no2', 'co2', 'co2-diff', 'nightlights-hd', 'nightlights-viirs', 'detection-ship'],
sf: ['no2', 'co2', 'co2-diff', 'nightlights-hd', 'nightlights-viirs', 'detection-ship', 'water-chlorophyll', 'water-spm'],
tk: ['no2', 'co2', 'co2-diff', 'nightlights-hd', 'nightlights-viirs'],
- ny: ['no2', 'co2', 'co2-diff', 'detection-ship', 'water-chlorophyll', 'water-spm']
+ ny: ['no2', 'co2', 'co2-diff', 'nightlights-hd', 'nightlights-viirs', 'detection-ship', 'water-chlorophyll', 'water-spm']
};
const layerOverridesBySpotlight = {
@@ -88,6 +88,7 @@ const layerOverridesBySpotlight = {
}
},
ny: {
+ 'nightlights-viirs': handleNightlightsViirs,
'water-chlorophyll': (l, spotlightId) => {
return {
...l,
@@ -103,6 +104,13 @@ const layerOverridesBySpotlight = {
...l,
domain: ['2020-01-01', '2020-01-08', '2020-01-15', '2020-01-22', '2020-01-29', '2020-02-05', '2020-02-12', '2020-02-19', '2020-02-26', '2020-03-04', '2020-03-11', '2020-03-18', '2020-03-25', '2020-04-01', '2020-04-08', '2020-04-15', '2020-04-22', '2020-04-29', '2020-05-06', '2020-05-13', '2020-05-20', '2020-05-27', '2020-06-03']
};
+ },
+ 'nightlights-hd': (l, spotlightId) => {
+ return {
+ ...l,
+ // For NY, nightlights goes till June
+ domain: [l.domain[0], '2020-06-01']
+ };
}
}
};
@@ -156,6 +164,7 @@ function handleNightlightsViirs (l, spotlightId) {
gh: 'EUPorts',
du: 'EUPorts',
la: 'LosAngeles',
+ ny: 'NewYork',
sf: 'SanFrancisco',
tk: 'Tokyo'
}[spotlightId];
diff --git a/app/assets/scripts/components/common/layers/layer-co2-diff.js b/app/assets/scripts/components/common/layers/layer-co2-diff.js
index 3a6542d1..bbf79c05 100644
--- a/app/assets/scripts/components/common/layers/layer-co2-diff.js
+++ b/app/assets/scripts/components/common/layers/layer-co2-diff.js
@@ -4,7 +4,7 @@ import { indicatorGroupColors } from '../../../styles/theme/theme.js';
export default {
id: 'co2-diff',
- name: 'Carbon Dioxide - change',
+ name: 'CO\u2082 (Diff)',
type: 'raster-timeseries',
timeUnit: 'day',
domain: [
diff --git a/app/assets/scripts/components/common/layers/layer-co2.js b/app/assets/scripts/components/common/layers/layer-co2.js
index 24fdb645..7c6037dc 100644
--- a/app/assets/scripts/components/common/layers/layer-co2.js
+++ b/app/assets/scripts/components/common/layers/layer-co2.js
@@ -6,7 +6,7 @@ import { indicatorGroupColors } from '../../../styles/theme/theme.js';
export default {
id: 'co2',
- name: 'Carbon Dioxide - avg',
+ name: 'CO\u2082 (Avg)',
type: 'raster-timeseries',
timeUnit: 'day',
domain: [
diff --git a/app/assets/scripts/components/common/layers/layer-no2.js b/app/assets/scripts/components/common/layers/layer-no2.js
index ee410874..88bbe202 100644
--- a/app/assets/scripts/components/common/layers/layer-no2.js
+++ b/app/assets/scripts/components/common/layers/layer-no2.js
@@ -6,7 +6,7 @@ import { indicatorGroupColors } from '../../../styles/theme/theme.js';
export default {
id: 'no2',
- name: 'Nitrogen dioxide',
+ name: 'NO\u2082',
type: 'raster-timeseries',
domain: [
'2018-03-01',
@@ -18,9 +18,6 @@ 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%2C1.5e16&color_map=custom_no2&color_formula=gamma r {gamma}`
]
},
- paint: {
- 'raster-opacity': 0.75
- },
exclusiveWith: ['co2', 'co2-diff', 'gibs-population', 'car-count', 'nightlights-viirs', 'nightlights-hd', 'detection-ship', 'detection-multi', 'water-chlorophyll', 'water-spm'],
enabled: true,
compare: {
diff --git a/app/assets/scripts/components/common/layers/types.js b/app/assets/scripts/components/common/layers/types.js
index 74d4fae6..517dc4ed 100644
--- a/app/assets/scripts/components/common/layers/types.js
+++ b/app/assets/scripts/components/common/layers/types.js
@@ -49,6 +49,11 @@ const replaceRasterTiles = (theMap, sourceId, tiles) => {
};
const replaceVectorData = (theMap, sourceId, data) => {
+ const empty = {
+ type: 'FeatureCollection',
+ features: []
+ };
+ theMap.getSource(sourceId).setData(empty);
theMap.getSource(sourceId).setData(data);
};
@@ -201,7 +206,7 @@ export const layerTypes = {
) return;
// The source we're updating is not present.
- if (!mbMap.getSource(id)) return;
+ if (!mbMap.getSource(vecId) || !mbMap.getSource(rastId)) return;
const formatDate = format(utcDate(date), dateFormats[layerInfo.timeUnit]);
const vectorData = vector.data.replace('{date}', formatDate);
diff --git a/app/assets/scripts/components/common/mb-map-explore/mb-map.js b/app/assets/scripts/components/common/mb-map-explore/mb-map.js
index 35b96181..e5eb5f88 100644
--- a/app/assets/scripts/components/common/mb-map-explore/mb-map.js
+++ b/app/assets/scripts/components/common/mb-map-explore/mb-map.js
@@ -222,7 +222,7 @@ class MbMap extends React.Component {
// Define a common function to add markers
const addMarker = (spotlight, map) => {
- createMbMarker(map)
+ createMbMarker(map, { color: this.props.theme.color.primary })
.setLngLat(spotlight.center)
.addTo(map)
.onClick((coords) => {
diff --git a/app/assets/scripts/components/common/page-footer.js b/app/assets/scripts/components/common/page-footer.js
index 889da899..f7a7bd76 100644
--- a/app/assets/scripts/components/common/page-footer.js
+++ b/app/assets/scripts/components/common/page-footer.js
@@ -2,17 +2,12 @@ import React from 'react';
import styled from 'styled-components';
import { rgba } from 'polished';
-import config from '../../config';
-
-import { Link } from 'react-router-dom';
import { themeVal, stylizeFunction } from '../../styles/utils/general';
import { glsp } from '../../styles/utils/theme-values';
import media from '../../styles/utils/media-queries';
import { headingAlt } from '../../styles/type/heading';
import Button from '../../styles/button/button';
-const { appTitle } = config;
-
const _rgba = stylizeFunction(rgba);
const PageFoot = styled.footer`
@@ -37,27 +32,6 @@ const PageFootInner = styled.div`
height: 100%;
`;
-const PageFootTitle = styled(Link)`
- display: block;
-
- sup {
- display: block;
- font-size: 0.75rem;
- line-height: 1rem;
- font-weight: ${themeVal('type.base.extrabold')};
- text-transform: uppercase;
- top: 0;
- }
-
- strong {
- font-size: 1rem;
- line-height: 1.5rem;
- font-weight: ${themeVal('type.base.light')};
- letter-spacing: -0.025em;
- display: block;
- }
-`;
-
const PageCredits = styled.address`
display: flex;
flex-flow: column nowrap;
@@ -85,6 +59,20 @@ const Colophon = styled.p`
}
`;
+const CreditsLink = styled.a`
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: flex-end;
+
+ strong {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ font-weight: ${themeVal('type.base.regular')};
+ letter-spacing: -0.0125em;
+ display: block;
+ }
+`;
+
const InfoList = styled.dl`
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -99,16 +87,31 @@ const InfoList = styled.dl`
order: 2;
`}
+ ${media.mediumUp`
+ grid-template-columns: repeat(4, auto);
+ grid-auto-flow: auto;
+ grid-gap: ${glsp(0, 0.5)};
+ align-items: center;
+ `}
+
dt {
${headingAlt}
font-size: 0.75rem;
line-height: 1rem;
grid-row: 1;
+
+ ${media.mediumUp`
+ margin-left: ${glsp()};
+ `}
}
dd {
grid-row: 2;
+ ${media.mediumUp`
+ grid-row: 1;
+ `}
+
> * {
vertical-align: top;
margin-left: -0.125rem;
@@ -150,11 +153,10 @@ const PageFooter = props => {
-
- NASA — Earthdata
- {appTitle}
-
- 2020
+
+ NASA Earthdata
+ 2020
+
diff --git a/app/assets/scripts/components/development/index.js b/app/assets/scripts/components/development/index.js
index 572e68db..468e2292 100644
--- a/app/assets/scripts/components/development/index.js
+++ b/app/assets/scripts/components/development/index.js
@@ -71,8 +71,9 @@ export default class Development extends React.Component {
NASA started the development of the Dashboard in May 2020. This experimental site reflects a rapid response to COVID-19 currently underway and will continue to evolve as more data become available.
+ This dashboard is built by the NASA Earth Science Data Systems program with help from various science teams and data providers. We are grateful for the many third-party open source projects that we have used.
- We welcome your feedback to help improve the Dashboard.
+ We welcome your feedback to help improve the Dashboard. To do so you can use the feedback option on this website. For an overview of known issues, please consult the Github issue queue .
diff --git a/app/assets/scripts/components/global/index.js b/app/assets/scripts/components/global/index.js
index 3a4fb11b..90ae78df 100644
--- a/app/assets/scripts/components/global/index.js
+++ b/app/assets/scripts/components/global/index.js
@@ -116,6 +116,20 @@ const ExploreCarto = styled.section`
overflow: hidden;
`;
+const dateMax = (...args) =>
+ args.reduce((curr, d) => (curr.getTime() > d.getTime() ? curr : d));
+
+const cogLayers = {
+ no2: {
+ title: <>NO2 Concentration>,
+ unit: <>molecules/cm2 >
+ },
+ co2: {
+ title: <>CO2 Concentration>,
+ unit: 'ppm'
+ }
+};
+
class GlobalExplore extends React.Component {
constructor (props) {
super(props);
@@ -185,7 +199,25 @@ class GlobalExplore extends React.Component {
},
_urlActiveLayers: activeLayers,
panelPrime: false,
- panelSec: false
+ panelSec: false,
+ // Init dates for cog data according to a default.
+ cogDateRanges: Object.keys(cogLayers).reduce((acc, id) => {
+ const l = props.mapLayers.find(l => l.id === id);
+ const timeUnit = l.timeUnit || 'month';
+
+ const end = utcDate(l.domain[l.domain.length - 1]);
+ const domainStart = utcDate(l.domain[0]);
+ const start = timeUnit === 'month'
+ ? dateMax(sub(end, { months: 11 }), domainStart)
+ : dateMax(sub(end, { months: 2 }), domainStart);
+
+ return {
+ ...acc,
+ [id]: {
+ start, end
+ }
+ };
+ }, {})
};
}
@@ -204,20 +236,45 @@ class GlobalExplore extends React.Component {
async requestCogData () {
const {
- aoi: { feature }
+ aoi: { feature },
+ cogDateRanges
} = this.state;
- const activeLayers = this.getActiveTimeseriesLayers();
+ const activeLayers = this.getActiveTimeseriesLayers()
+ .filter(l => !!cogLayers[l.id]);
if (!feature || !activeLayers.length) return;
showGlobalLoading();
- // TODO: Change from hardcoded cog type and date
- const end = utcDate('2020-03-01');
+ await Promise.all(activeLayers.map(l => {
+ const cogDate = cogDateRanges[l.id];
+ return this.props.fetchCogTimeData(
+ l.id,
+ {
+ start: cogDate.start,
+ end: cogDate.end,
+ timeUnit: l.timeUnit || 'month'
+ },
+ feature
+ );
+ }));
+ hideGlobalLoading();
+ }
+
+ async requestSingleCogData (id) {
+ const {
+ aoi: { feature },
+ cogDateRanges
+ } = this.state;
+
+ showGlobalLoading();
+ const cogLayerSettings = cogLayers[id];
+ const cogDate = cogDateRanges[id];
await this.props.fetchCogTimeData(
- 'no2',
+ id,
{
- start: sub(end, { months: 11 }),
- end
+ start: cogDate.start,
+ end: cogDate.end,
+ dateFormat: cogLayerSettings.dateFormat
},
feature
);
@@ -276,6 +333,14 @@ class GlobalExplore extends React.Component {
}
);
break;
+ case 'cog.date-range':
+ this.setState(state => ({
+ cogDateRanges: {
+ ...state.cogDateRanges,
+ [payload.id]: payload.date
+ }
+ }), () => this.requestSingleCogData(payload.id));
+ break;
}
}
@@ -338,6 +403,8 @@ class GlobalExplore extends React.Component {
const { spotlightList } = this.props;
const layers = this.getLayersWithState();
const activeTimeseriesLayers = this.getActiveTimeseriesLayers();
+ const activeCogTimeseriesLayers = activeTimeseriesLayers
+ .filter(l => !!cogLayers[l.id]);
// Check if there's any layer that's comparing.
const comparingLayer = find(layers, 'comparing');
@@ -408,7 +475,10 @@ class GlobalExplore extends React.Component {
{
this.resizeMap();
this.onPanelChange('panelSec', revealed);
diff --git a/app/assets/scripts/components/global/sec-panel.js b/app/assets/scripts/components/global/sec-panel.js
index dce9e5fb..1681ccde 100644
--- a/app/assets/scripts/components/global/sec-panel.js
+++ b/app/assets/scripts/components/global/sec-panel.js
@@ -14,6 +14,9 @@ import ShadowScrollbar from '../common/shadow-scrollbar';
import { glsp } from '../../styles/utils/theme-values';
import { utcDate } from '../../utils/utils';
import media, { isLargeViewport } from '../../styles/utils/media-queries';
+import DatePicker from '../common/date-picker';
+import { differenceInMonths, differenceInDays } from 'date-fns';
+
import SummaryExpandable from '../common/summary-expandable';
const PanelSelf = styled(Panel)`
@@ -34,9 +37,24 @@ const InsightsBlock = styled.div`
flex: 1;
`;
+const InsightHeadline = styled.div`
+ display: flex;
+
+ > *:last-child {
+ margin-left: auto;
+ }
+`;
+
class ExpMapSecPanel extends React.Component {
renderContent () {
- const { cogTimeData, aoiFeature, layers } = this.props;
+ const {
+ cogTimeData,
+ aoiFeature,
+ layers,
+ cogLayersSettings,
+ cogDateRanges,
+ onAction
+ } = this.props;
if (!aoiFeature) {
return There is no area of interest defined.
;
@@ -46,31 +64,69 @@ class ExpMapSecPanel extends React.Component {
return There are no layers with time data enabled.
;
}
- // TODO: Do not use hardcoded values.
- const no2cogTimeData = cogTimeData.no2;
+ return layers.map(l => {
+ const cogData = cogTimeData[l.id];
+ const cogLayerDef = cogLayersSettings[l.id];
- if (!no2cogTimeData || !no2cogTimeData.isReady()) {
- return null;
- }
+ if (!cogData || !cogData.isReady()) {
+ return null;
+ }
- const data = no2cogTimeData.getData();
- const xDomain = [
- utcDate(data[0].date),
- utcDate(data[data.length - 1].date)
- ];
- const yDomain = d3.extent(data, d => d.value);
+ const data = cogData.getData();
+ const xDomain = [
+ utcDate(data[0].date),
+ utcDate(data[data.length - 1].date)
+ ];
+ const pickerDateDomain = [
+ utcDate(l.domain[0]),
+ utcDate(l.domain[l.domain.length - 1])
+ ];
+ const yDomain = d3.extent(data, d => d.value);
- return (
-
- NO2 Concentration
- molecules/cm2
-
-
- );
+ const dateRange = cogDateRanges[l.id] || {
+ start: null,
+ end: null
+ };
+
+ const timeUnit = l.timeUnit || 'month';
+
+ const validateDateRange = ({ start, end }) => {
+ if (timeUnit === 'month') {
+ const diff = differenceInMonths(end, start);
+ if (isNaN(diff) || diff < 3) {
+ return <>A date range for {l.name} must have 3 or more months>;
+ }
+ } else {
+ const diff = differenceInDays(end, start);
+ if (isNaN(diff) || diff < 7) {
+ return <>A date range for {l.name} must have 7 or more days>;
+ }
+ }
+ };
+
+ return (
+
+
+
+ {cogLayerDef.title}
+
+ onAction('cog.date-range', { id: l.id, date: selectedDate })}
+ />
+
+ {cogLayerDef.unit}
+
+
+
+ );
+ });
}
render () {
@@ -104,10 +160,13 @@ class ExpMapSecPanel extends React.Component {
}
ExpMapSecPanel.propTypes = {
+ onAction: T.func,
onPanelChange: T.func,
layers: T.array,
aoiFeature: T.object,
- cogTimeData: T.object
+ cogTimeData: T.object,
+ cogDateRanges: T.object,
+ cogLayersSettings: T.object
};
export default ExpMapSecPanel;
diff --git a/app/assets/scripts/components/home/index.js b/app/assets/scripts/components/home/index.js
index cb732736..e83de54e 100644
--- a/app/assets/scripts/components/home/index.js
+++ b/app/assets/scripts/components/home/index.js
@@ -175,9 +175,13 @@ const IntroStatsList = styled.dl`
dd {
font-family: ${themeVal('type.base.family')};
font-weight: ${themeVal('type.heading.weight')};
- font-size: 3rem;
+ font-size: 2rem;
line-height: 1;
grid-row: 2;
+
+ ${media.mediumUp`
+ font-size: 3rem;
+ `}
}
`;
@@ -489,7 +493,7 @@ class Home extends React.Component {
Areas
07
Indicators
- 04
+ 05
diff --git a/app/assets/scripts/components/indicators/indicator-co2.js b/app/assets/scripts/components/indicators/indicator-co2.js
index 5e3f74a4..c4085098 100644
--- a/app/assets/scripts/components/indicators/indicator-co2.js
+++ b/app/assets/scripts/components/indicators/indicator-co2.js
@@ -195,12 +195,6 @@ class CO2LongForm extends React.Component {
-
- Reductions in carbon dioxide (CO2 ) emissions due to
- COVID-19 shutdowns are expected to reduce the rate at which CO
- 2 accumulates in the atmosphere, but not its total
- atmospheric concentration.
-
Carbon dioxide (CO2 ) is a greenhouse gas primarily
emitted from the combustion of fossil fuels such as petroleum,
@@ -337,7 +331,7 @@ class CO2LongForm extends React.Component {
src={`${baseUrl}/assets/graphics/content/co2_april_1_20.png`}
alt='CO2 Diff April 1'
>
- CO2 difference on April 1, 2020.
+ The difference in carbon dioxide (CO2 ) levels from April 1, 2020 compared to previous years. Redder colors indicate increases in CO2 . Bluer colors indicate lower levels of CO2 . Image Credit: NASA
diff --git a/app/assets/scripts/components/indicators/indicator-no2.js b/app/assets/scripts/components/indicators/indicator-no2.js
index c737113d..1dc88a8f 100644
--- a/app/assets/scripts/components/indicators/indicator-no2.js
+++ b/app/assets/scripts/components/indicators/indicator-no2.js
@@ -253,7 +253,7 @@ class NO2LongForm extends React.Component {
/>
- 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.
+ Nitrogen dioxide has a relatively short lifespan 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.
diff --git a/app/assets/scripts/components/indicators/indicator-shipping.js b/app/assets/scripts/components/indicators/indicator-shipping.js
index 32053892..19e40d38 100644
--- a/app/assets/scripts/components/indicators/indicator-shipping.js
+++ b/app/assets/scripts/components/indicators/indicator-shipping.js
@@ -241,8 +241,8 @@ class ShippingLongForm extends React.Component {
Los Angeles has the busiest port in the United States, which
@@ -316,8 +316,7 @@ class ShippingLongForm extends React.Component {
Supply chains around the world dependent on cargo shipping have
been interrupted by travel restrictions and quarantines designed
- to stop the spread of the novel coronavirus. Image Credit: Open
- Source.
+ to stop the spread of the novel coronavirus. Image Credit: NOAA.
@@ -353,6 +352,16 @@ class ShippingLongForm extends React.Component {
A Satellite’s View of Ship Pollution
+
+
+ Commercial Smallsat Data Acquisition Program (CSDAP)
+
+
+
diff --git a/app/assets/scripts/components/indicators/indicator-water-quality.js b/app/assets/scripts/components/indicators/indicator-water-quality.js
index 93a86684..7a226fa6 100644
--- a/app/assets/scripts/components/indicators/indicator-water-quality.js
+++ b/app/assets/scripts/components/indicators/indicator-water-quality.js
@@ -206,7 +206,7 @@ class WQLongForm extends React.Component {
>
Chlorophyll-a is an indicator of algae growth. During
coronavirus-related shutdowns, changes in our activity may
- affect the amount of nutrients flowing in water bodies. This
+ affect the amount of nutrients flowing into water bodies. This
image shows the changes in chlorophyll-a for the San Francisco
Bay Area on April 3, 2020. Redder colors indicate higher levels
of chlorophyll-a and worse water quality. Bluer colors indicate
diff --git a/app/assets/scripts/components/sandbox/index.js b/app/assets/scripts/components/sandbox/index.js
index d79c59fe..fd38ed06 100644
--- a/app/assets/scripts/components/sandbox/index.js
+++ b/app/assets/scripts/components/sandbox/index.js
@@ -25,6 +25,7 @@ import BtnExample from './buttons';
import FormsExample from './forms';
import ColorsExample from './colors';
import LineChartExample from './line-chart';
+import ProseExample from './prose';
import config from '../../config';
@@ -59,6 +60,11 @@ const sandboxPages = [
name: 'Line chart',
url: '/line-chart',
cmp: LineChartExample
+ },
+ {
+ name: 'Prose',
+ url: '/prose',
+ cmp: ProseExample
}
];
diff --git a/app/assets/scripts/components/sandbox/prose.js b/app/assets/scripts/components/sandbox/prose.js
new file mode 100644
index 00000000..b06849d0
--- /dev/null
+++ b/app/assets/scripts/components/sandbox/prose.js
@@ -0,0 +1,10 @@
+import React from 'react';
+
+const ProseExample = () => (
+
+ Prose
+ Lorem ipsum dolor sit amet
+
+);
+
+export default ProseExample;
diff --git a/app/assets/scripts/redux/cog-time-data.js b/app/assets/scripts/redux/cog-time-data.js
index 09ba193c..96159610 100644
--- a/app/assets/scripts/redux/cog-time-data.js
+++ b/app/assets/scripts/redux/cog-time-data.js
@@ -1,4 +1,4 @@
-import { eachMonthOfInterval, format } from 'date-fns';
+import { eachMonthOfInterval, format, eachDayOfInterval } from 'date-fns';
import config from '../config';
import { makeActions, fetchJSON, makeAPIReducer } from './reduxeed';
@@ -14,25 +14,35 @@ export const invalidateCogTimeData = cogTimeDataActions.invalidate;
export function fetchCogTimeData (id, timeframe, area) {
return async function (dispatch) {
dispatch(cogTimeDataActions.request(id));
+ const { start, end, timeUnit } = timeframe;
+ const dateFormat = timeUnit === 'month'
+ ? 'yyyyMM'
+ : 'yyyy_MM_dd';
+
+ const interval = timeUnit === 'month'
+ ? eachMonthOfInterval({ start, end })
+ : eachDayOfInterval({ start, end });
- const { start, end } = timeframe;
- const months = eachMonthOfInterval({ start, end });
const url = `${config.api}/timelapse`;
- const requests = months.map(async date => {
- const reqDate = format(date, 'yyyyMM');
+ const requests = interval.map(async date => {
+ const reqDate = format(date, dateFormat);
try {
const { body } = await fetchJSON(url, {
method: 'POST',
body: JSON.stringify({
+ type: id,
month: reqDate,
geojson: area
})
});
+ const v = id === 'co2'
+ ? body.mean < 0 ? null : body.mean * 1000000
+ : body.mean < 0 ? null : body.mean;
return {
date: date.toISOString(),
- value: body.mean < 0 ? null : body.mean
+ value: v
};
} catch (e) {
return {
diff --git a/app/assets/scripts/styles/vendor/react-datepicker.js b/app/assets/scripts/styles/vendor/react-datepicker.js
index 4ee22c7e..79fd9092 100644
--- a/app/assets/scripts/styles/vendor/react-datepicker.js
+++ b/app/assets/scripts/styles/vendor/react-datepicker.js
@@ -1,19 +1,20 @@
import { css } from 'styled-components';
+import { rgba } from 'polished';
-import { themeVal } from '../utils/general';
+import { themeVal, stylizeFunction } from '../utils/general';
import collecticon from '../collecticons';
import { visuallyHidden } from '../helpers';
// Some dependencies include styles that must be included.
// This file overrides to be used the default react-datepicker styles.
+const _rgba = stylizeFunction(rgba);
+
export default () => css`
.react-datepicker {
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: ${themeVal('type.base.family')};
font-size: ${themeVal('type.base.size')};
- color: inherit;
- background: ${themeVal('color.surface')};
}
.react-datepicker__header {
@@ -34,7 +35,7 @@ export default () => css`
}
.react-datepicker__month {
- box-shadow: 0 -1px 0 0 ${themeVal('color.baseAlphaA')};
+ box-shadow: 0 -1px 0 0 ${themeVal('color.baseAlphaB')};
margin-top: 0;
padding-top: 0.25rem;
}
@@ -53,48 +54,35 @@ export default () => css`
.react-datepicker__day {
width: 2rem;
line-height: 2rem;
- outline: 0;
- text-align: center;
- transition: background-color 0.24s ease 0s;
&:hover {
- background: rgba(255, 255, 255, 0.04);
+ background: ${themeVal('color.baseAlphaB')};
}
}
.react-datepicker__day--in-range {
color: #FFF;
- background: rgba(255, 255, 255, 0.02);
+ background: ${_rgba(themeVal('color.primary'), 0.64)};
&.react-datepicker__day--range-start,
&.react-datepicker__day--range-end{
color: #FFF;
- background: rgba(255, 255, 255, 0.08);
+ background: ${themeVal('color.primary')};
}
&:hover {
- background: rgba(255, 255, 255, 0.12);
+ color: ${themeVal('color.base')};
+ background: ${themeVal('color.baseAlphaB')};
}
}
.react-datepicker__day--in-selecting-range {
color: #FFF;
- background: rgba(255, 255, 255, 0.02);
- transition: background-color 0.24s ease 0s;
+ background: ${_rgba(themeVal('color.primary'), 0.64)};
&.react-datepicker__day--selecting-range-end {
color: #FFF;
- background: rgba(255, 255, 255, 0.08);
- }
- }
-
- .react-datepicker__month-text,
- .react-datepicker__quarter-text {
- transition: background-color 0.24s ease 0s;
-
- &:hover {
- color: #FFF;
- background: rgba(255, 255, 255, 0.08);
+ background: ${themeVal('color.primary')};
}
}
@@ -104,35 +92,23 @@ export default () => css`
font-weight: inherit;
}
- .react-datepicker__day--disabled {
- opacity: 0.48;
- pointer-events: none;
- }
-
.react-datepicker__navigation {
overflow: visible;
text-indent: initial;
border: none;
top: 0;
line-height: 1.5rem;
- width: 1.5rem;
+ width: 1rem;
height: 1.5rem;
- outline: 0;
- border-radius: ${themeVal('shape.rounded')};
- transition: background-color 0.24s ease 0s;
- text-align: center;
span {
${visuallyHidden()}
}
+ }
- &:hover {
- background: rgba(255, 255, 255, 0.04);
- }
-
- &:active {
- background: rgba(255, 255, 255, 0.08);
- }
+ .react-datepicker__day--disabled {
+ opacity: 0.48;
+ pointer-events: none;
}
.react-datepicker__navigation--next::before {
@@ -149,16 +125,6 @@ export default () => css`
.react-datepicker__current-month {
cursor: pointer;
display: inline-block;
- padding: 0 0.25rem;
- transition: background-color 0.24s ease 0s;
-
- &:hover {
- background: rgba(255, 255, 255, 0.04);
- }
-
- &:active {
- background: rgba(255, 255, 255, 0.08);
- }
&::after {
${collecticon('swap-horizontal')}
diff --git a/package.json b/package.json
index 76825465..c1456a6f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "covid-dashboard",
- "version": "0.9.1",
+ "version": "1.0.0",
"description": "Frontend application for the Covid Dashboard",
"repository": {
"type": "git",