Skip to content

Commit e811a9c

Browse files
WV-3590 - Chart Window Additions (#6054)
* Added chart window changes * Added leading 0 in dates * UI tweaks * Adjusted UI for requested changes * Merge branch 'develop' into wv-3590-charting-minimap * Alignment fixes * Expand/collapse ui changes * Feedback changes * Remove mock data
1 parent c00db9d commit e811a9c

File tree

3 files changed

+414
-56
lines changed

3 files changed

+414
-56
lines changed

web/js/components/charting/chart-component.js

Lines changed: 222 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
1-
import React from 'react';
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { connect } from 'react-redux';
23
import {
34
LineChart, Line, XAxis, YAxis, Legend, Tooltip,
45
} from 'recharts';
56
import PropTypes from 'prop-types';
67
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
8+
import OlMap from 'ol/Map';
9+
import OlView from 'ol/View';
10+
import OlLayerGroup from 'ol/layer/Group';
11+
import OlLayerTile from 'ol/layer/Tile';
12+
import OlFeature from 'ol/Feature';
13+
import { fromExtent } from 'ol/geom/Polygon';
14+
import { Vector as OlVectorLayer } from 'ol/layer';
15+
import { Vector as OlVectorSource } from 'ol/source';
16+
import { getCenter } from 'ol/extent';
17+
import { inAndOut } from 'ol/easing';
18+
import {
19+
Fill as OlStyleFill,
20+
Stroke as OlStyleStroke,
21+
Style as OlStyle,
22+
} from 'ol/style';
23+
import util from '../../util/util';
724

825
function ChartComponent (props) {
926
const {
1027
liveData,
28+
mapView,
29+
createLayer,
30+
overviewMapLayerDef,
1131
} = props;
1232

33+
const [errorCollapsed, setErrorCollapsed] = useState(true);
34+
const mapInstanceRef = useRef(null);
35+
1336
const {
1437
data,
1538
unit,
@@ -19,8 +42,13 @@ function ChartComponent (props) {
1942
isTruncated,
2043
title,
2144
numPoints,
45+
coordinates,
46+
errors,
2247
} = liveData;
2348

49+
const errorDaysArr = errors?.error_days?.replaceAll(/('|\[|\])/gi, '').split(', ') || [];
50+
const format = util.getCoordinateFormat();
51+
2452
// Arbitrary array of colors to use
2553
const lineColors = ['#A3905D', '#82CA9D', 'orange', 'pink', 'green', 'red', 'yellow', 'aqua', 'maroon'];
2654
const formattedUnit = unit ? ` (${unit})` : '';
@@ -182,6 +210,75 @@ function ChartComponent (props) {
182210
);
183211
}
184212

213+
useEffect(() => {
214+
const boxFeature = new OlFeature({
215+
geometry: fromExtent(coordinates),
216+
});
217+
boxFeature.setStyle(new OlStyle({
218+
stroke: new OlStyleStroke({
219+
color: 'rgba(255, 255, 255, .6)',
220+
width: 1,
221+
}),
222+
fill: new OlStyleFill({
223+
color: 'rgba(255, 255, 255, .3)',
224+
}),
225+
}));
226+
const boxLayer = new OlVectorLayer({
227+
source: new OlVectorSource({
228+
features: [boxFeature],
229+
}),
230+
});
231+
232+
const createLayerWrapper = async () => {
233+
const backgroundLayerGroup = await createLayer(overviewMapLayerDef);
234+
backgroundLayerGroup.setVisible(true);
235+
236+
const layersList = [];
237+
backgroundLayerGroup.getLayers().getArray().forEach((layer) => {
238+
layersList.push(new OlLayerTile({
239+
source: layer.getSource(),
240+
}));
241+
});
242+
const copiedLayerGroup = new OlLayerGroup({
243+
layers: layersList,
244+
});
245+
246+
mapInstanceRef.current = new OlMap({
247+
view: new OlView({
248+
center: mapView.getCenter(),
249+
zoom: mapView.getZoom(),
250+
projection: mapView.getProjection(),
251+
}),
252+
layers: [copiedLayerGroup, boxLayer],
253+
target: 'charting-minimap-inner',
254+
interactions: [],
255+
});
256+
257+
const minimapView = mapInstanceRef.current.getView();
258+
minimapView.fit(boxFeature.getGeometry().getExtent(), { padding: [50, 50, 50, 50] });
259+
260+
mapInstanceRef.current.on('moveend', () => {
261+
const boxCenter = getCenter(boxFeature.getGeometry().getExtent());
262+
const minimapCenter = minimapView.getCenter();
263+
if (boxCenter[0] === minimapCenter[0] && boxCenter[1] === minimapCenter[1]) {
264+
return;
265+
}
266+
minimapView.animate({
267+
center: boxCenter,
268+
duration: 350,
269+
easing: inAndOut,
270+
});
271+
});
272+
};
273+
274+
createLayerWrapper();
275+
276+
return () => {
277+
mapInstanceRef.current.setTarget(null);
278+
mapInstanceRef.current = null;
279+
};
280+
}, [overviewMapLayerDef]);
281+
185282
return (
186283
<div className="charting-chart-container">
187284
<div className="charting-chart-inner">
@@ -217,56 +314,141 @@ function ChartComponent (props) {
217314
</LineChart>
218315
</div>
219316
<div className="charting-stat-text">
220-
<h3>
221-
<b>
222-
Average Statistics
223-
{formattedUnit}
224-
</b>
225-
</h3>
226-
<br />
227-
{getQuickStatistics(data)}
228-
</div>
229-
</div>
230-
<div className="charting-disclaimer">
231-
<strong className="charting-disclaimer-pre">Note: </strong>
232-
<span>Numerical analyses performed on imagery should only be used for initial basic exploratory purposes.</span>
233-
{isTruncated
234-
&& (
235-
<div className="charting-disclaimer-lower">
236-
<FontAwesomeIcon
237-
icon="exclamation-triangle"
238-
className="wv-alert-icon"
239-
size="1x"
240-
widthAuto
241-
/>
242-
<i className="charting-disclaimer-block">
243-
As part of this beta feature release, the number of data points plotted between
317+
<div id="charting-stats-container">
318+
<h3>
244319
<b>
245-
{` ${startDate} `}
320+
Average Statistics
321+
{formattedUnit}
246322
</b>
247-
and
248-
<b>
249-
{` ${endDate} `}
250-
</b>
251-
have been reduced from
252-
<b>
253-
{` ${numRangeDays} `}
254-
</b>
255-
to
323+
</h3>
324+
<br />
325+
{getQuickStatistics(data)}
326+
</div>
327+
<div id="charting-minimap-container">
328+
<div id="charting-minimap-inner" />
329+
</div>
330+
<div />
331+
<div id="charting-coordinates-container">
332+
<h3>
256333
<b>
257-
{` ${numPoints}`}
334+
Coordinates
258335
</b>
259-
.
260-
</i>
336+
</h3>
337+
<br />
338+
<div className="charting-coordinates-inner">
339+
<div />
340+
<div className="coordinate-center coordinate-subheader">Latitude</div>
341+
<div className="coordinate-center coordinate-subheader">Longitude</div>
342+
<div>Top Right:</div>
343+
<div className="coordinate-mono">{util.formatCoordinate([coordinates[2], coordinates[3]], format).split(', ')[0]}</div>
344+
<div className="coordinate-mono">{util.formatCoordinate([coordinates[2], coordinates[3]], format).split(', ')[1]}</div>
345+
<div>Bottom Left:</div>
346+
<div className="coordinate-mono">{util.formatCoordinate([coordinates[0], coordinates[1]], format).split(', ')[0]}</div>
347+
<div className="coordinate-mono">{util.formatCoordinate([coordinates[0], coordinates[1]], format).split(', ')[1]}</div>
348+
</div>
261349
</div>
262-
)}
350+
</div>
351+
<div className="charting-disclaimer">
352+
<strong className="charting-disclaimer-pre">Note: </strong>
353+
<span>Numerical analyses performed on imagery should only be used for initial basic exploratory purposes.</span>
354+
{isTruncated
355+
&& (
356+
<div className="charting-disclaimer-upper">
357+
<FontAwesomeIcon
358+
icon="exclamation-triangle"
359+
className="wv-alert-icon"
360+
size="1x"
361+
widthAuto
362+
/>
363+
<i className="charting-disclaimer-block">
364+
As part of this beta feature release, the number of data points plotted between
365+
<b>
366+
{` ${startDate} `}
367+
</b>
368+
and
369+
<b>
370+
{` ${endDate} `}
371+
</b>
372+
have been reduced from
373+
<b>
374+
{` ${numRangeDays} `}
375+
</b>
376+
to
377+
<b>
378+
{` ${numPoints}`}
379+
</b>
380+
.
381+
</i>
382+
</div>
383+
)}
384+
{errors && errors.error_count > 0
385+
&& (
386+
<div className="charting-disclaimer-lower">
387+
<FontAwesomeIcon
388+
icon="exclamation-triangle"
389+
className="wv-alert-icon"
390+
size="1x"
391+
widthAuto
392+
/>
393+
<i className="charting-disclaimer-block">
394+
{`${errors.error_count} `}
395+
dates requested have no data, so are not shown in the chart.
396+
</i>
397+
{!errorCollapsed
398+
&& (
399+
<div className="charting-disclaimer-dates">
400+
<i className="charting-disclaimer-block">
401+
{errorDaysArr.map((date, index) => (
402+
<>
403+
{date.split('T')[0]}
404+
{index < errorDaysArr.length - 1 && ', '}
405+
&nbsp;&nbsp;
406+
</>
407+
))}
408+
</i>
409+
</div>
410+
)}
411+
<div className="error-expand-button">
412+
<span className="error-expand-button-inner" onClick={() => setErrorCollapsed(!errorCollapsed)}>
413+
{errorCollapsed ? 'more' : 'less'}
414+
<FontAwesomeIcon
415+
className="layer-group-collapse"
416+
icon={!errorCollapsed ? 'caret-up' : 'caret-down'}
417+
widthAuto
418+
/>
419+
</span>
420+
</div>
421+
</div>
422+
)}
423+
</div>
263424
</div>
264425
</div>
265426
);
266427
}
267428

429+
const mapStateToProps = (state) => {
430+
const {
431+
map,
432+
layers,
433+
} = state;
434+
435+
const {
436+
ui,
437+
} = map;
438+
439+
const layerId = 'Coastlines_15m';
440+
441+
return {
442+
mapView: ui.selected.getView(),
443+
createLayer: ui.createLayer,
444+
overviewMapLayerDef: layers.layerConfig[layerId],
445+
};
446+
};
447+
268448
ChartComponent.propTypes = {
269449
liveData: PropTypes.object,
270450
};
271451

272-
export default ChartComponent;
452+
export default connect(
453+
mapStateToProps,
454+
)(ChartComponent);

web/js/components/sidebar/charting-mode-options.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ function ChartingModeOptions(props) {
8888

8989
const isMounted = useRef(false);
9090
const [isPostRender, setIsPostRender] = useState(false);
91+
const [doRenderChart, setDoRenderChart] = useState(false);
9192
const [mapViewChecked, setMapViewChecked] = useState(false);
9293
const [isWithinWings, setIsWithinWings] = useState(false);
9394
const [boundaries, setBoundaries] = useState({
@@ -411,17 +412,23 @@ function ChartingModeOptions(props) {
411412
if (timeSpanSelection === 'range') {
412413
const rechartsData = formatGIBSDataForRecharts(dataToRender);
413414
const numRangeDays = Math.floor((Date.parse(initialEndDate) - Date.parse(initialStartDate)) / 86400000);
415+
const startDateFormatted = `${initialStartDate.getFullYear()}-${`0${initialStartDate.getMonth() + 1}`.slice(-2)}-${`0${initialStartDate.getDate()}`.slice(-2)}`;
416+
const endDateFormatted = `${initialEndDate.getFullYear()}-${`0${initialEndDate.getMonth() + 1}`.slice(-2)}-${`0${initialEndDate.getDate()}`.slice(-2)}`;
414417
const numPoints = STEP_NUM - (data?.body?.errors?.error_count > 0 ? data.body.errors.error_count : 0);
415418
displayChart({
416419
title: dataToRender.title,
417420
subtitle: dataToRender.subtitle,
418421
unit: dataToRender.unit,
422+
errors: dataToRender.errors,
419423
data: rechartsData,
420424
startDate: primaryDate,
421425
endDate: secondaryDate,
426+
startDateFormatted,
427+
endDateFormatted,
422428
numRangeDays,
423429
isTruncated: numRangeDays > STEP_NUM,
424430
numPoints,
431+
coordinates: [...bottomLeftLatLong, ...topRightLatLong],
425432
});
426433
updateChartRequestStatus(false);
427434
} else {
@@ -434,13 +441,19 @@ function ChartingModeOptions(props) {
434441
}
435442
}
436443

444+
useEffect(() => {
445+
if (doRenderChart && isPostRender) {
446+
onRequestChartClick();
447+
}
448+
}, [doRenderChart, isPostRender]);
449+
437450
useEffect(() => {
438451
const isOpen = (modalId === 'CHARTING-CHART' || modalId === 'CHARTING-STATS-MODAL') && isModalOpen;
439452
if (isChartOpen && !isOpen && Object.keys(renderedPalettes).length > 0) {
440453
const layerInfo = getActiveChartingLayer();
441454
const paletteName = layerInfo.palette.id;
442455
if (renderedPalettes[paletteName]) {
443-
onRequestChartClick();
456+
setDoRenderChart(true);
444457
}
445458
}
446459
}, [isChartOpen, renderedPalettes]);
@@ -845,16 +858,29 @@ const mapDispatchToProps = (dispatch) => ({
845858
displayChart: (liveData) => {
846859
dispatch(
847860
openCustomContent('CHARTING-CHART', {
848-
headerText: `BETA | ${liveData.title} - ${liveData.subtitle}${liveData.unit ? ` (${liveData.unit})` : ''}`,
861+
headerText: (
862+
<>
863+
BETA |
864+
{` ${liveData.title} `}
865+
-
866+
{` ${liveData.subtitle}${liveData.unit ? ` (${liveData.unit})` : ''}`}
867+
<span className="charting-chart-subheader">
868+
from &nbsp;
869+
{liveData.startDateFormatted}
870+
&nbsp; to &nbsp;
871+
{liveData.endDateFormatted}
872+
</span>
873+
</>
874+
),
849875
backdrop: false,
850876
bodyComponent: ChartComponent,
851877
wrapClassName: 'unclickable-behind-modal',
852878
modalClassName: 'chart-dialog',
853879
isDraggable: true,
854880
dragHandle: '.modal-header',
855-
offsetLeft: 'calc(50% - 425px)',
881+
offsetLeft: 'calc(50% - 575px)',
856882
offsetTop: 50,
857-
width: 850,
883+
width: 1150,
858884
height: 420,
859885
stayOnscreen: true,
860886
type: 'selection', // This forces the user to specifically close the modal

0 commit comments

Comments
 (0)