Implemented query page graph selector

The range selector on the graph was quite quirky
so functionality in LineGraph needed to be expanded a lot
This commit is contained in:
Aurora Lahtela 2022-10-16 10:47:18 +03:00
parent 428a0c5fde
commit 1c3cfbfe4b
6 changed files with 105 additions and 19 deletions

View File

@ -24,9 +24,11 @@ public class Cookie {
private final String value; private final String value;
public Cookie(String rawRepresentation) { public Cookie(String rawRepresentation) {
String[] split = StringUtils.split(rawRepresentation, "=", 2); this(StringUtils.split(rawRepresentation, "=", 2));
name = split[0]; }
value = split[1];
private Cookie(String[] splitRawRepresentation) {
this(splitRawRepresentation[0], splitRawRepresentation[1]);
} }
public Cookie(String name, String value) { public Cookie(String name, String value) {

View File

@ -1,4 +1,4 @@
import React, {useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {Card, Col, Row} from "react-bootstrap-v5"; import {Card, Col, Row} from "react-bootstrap-v5";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {useDataRequest} from "../../../hooks/dataFetchHook"; import {useDataRequest} from "../../../hooks/dataFetchHook";
@ -9,6 +9,24 @@ import DateInputField from "../../input/DateInputField";
import TimeInputField from "../../input/TimeInputField"; import TimeInputField from "../../input/TimeInputField";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faSearch} from "@fortawesome/free-solid-svg-icons"; import {faSearch} from "@fortawesome/free-solid-svg-icons";
import PlayersOnlineGraph from "../../graphs/PlayersOnlineGraph";
import Highcharts from "highcharts/highstock";
const parseTime = (dateString, timeString) => {
const d = dateString.match(
/^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/
);
const t = timeString.match(/^(0\d|\d{2}):?(0\d|\d{2})$/);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
const parsedDay = Number(d[1]);
const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December
const parsedYear = Number(d[3]);
let hour = Number(t[1]);
let minute = Number(t[2]);
const date = new Date(parsedYear, parsedMonth, parsedDay, hour, minute);
return date.getTime() - (date.getTimezoneOffset() * 60000);
};
const QueryOptionsCard = () => { const QueryOptionsCard = () => {
const {t} = useTranslation(); const {t} = useTranslation();
@ -17,12 +35,48 @@ const QueryOptionsCard = () => {
const [fromTime, setFromTime] = useState(undefined); const [fromTime, setFromTime] = useState(undefined);
const [toDate, setToDate] = useState(undefined); const [toDate, setToDate] = useState(undefined);
const [toTime, setToTime] = useState(undefined); const [toTime, setToTime] = useState(undefined);
const [invalidFields, setInvalidFields] = useState([]); const [invalidFields, setInvalidFields] = useState([]);
const setAsInvalid = id => setInvalidFields([...invalidFields, id]); const setAsInvalid = id => setInvalidFields([...invalidFields, id]);
const setAsValid = id => setInvalidFields(invalidFields.filter(invalid => id !== invalid)); const setAsValid = id => setInvalidFields(invalidFields.filter(invalid => id !== invalid));
const [extremes, setExtremes] = useState(undefined);
const updateExtremes = useCallback(() => {
if (invalidFields.length || !options) return;
const newMin = parseTime(
fromDate ? fromDate : options.view.afterDate,
fromTime ? fromTime : options.view.afterTime
);
const newMax = parseTime(
toDate ? toDate : options.view.beforeDate,
toTime ? toTime : options.view.beforeTime
);
setExtremes({
min: newMin,
max: newMax
});
}, [fromDate, fromTime, toDate, toTime, invalidFields]);
useEffect(updateExtremes, [invalidFields]);
const onSetExtremes = useCallback((event) => {
if (event && (event.trigger === "navigator" || event.trigger === 'rangeSelectorButton')) {
const afterDate = Highcharts.dateFormat('%d/%m/%Y', event.min);
const afterTime = Highcharts.dateFormat('%H:%M', event.min);
const beforeDate = Highcharts.dateFormat('%d/%m/%Y', event.max);
const beforeTime = Highcharts.dateFormat('%H:%M', event.max);
setFromDate(afterDate);
setFromTime(afterTime);
setToDate(beforeDate);
setToTime(beforeTime);
}
}, [setFromTime, setFromDate, setToTime, setToDate]);
const {data: options, loadingError} = useDataRequest(fetchFilters, []); const {data: options, loadingError} = useDataRequest(fetchFilters, []);
const [graphData, setGraphData] = useState(undefined);
useEffect(() => {
if (options) {
setGraphData({playersOnline: options.viewPoints, color: '#9E9E9E'})
}
}, [options, setGraphData]);
if (loadingError) return <ErrorViewCard error={loadingError}/> if (loadingError) return <ErrorViewCard error={loadingError}/>
if (!options) return (<Card> if (!options) return (<Card>
@ -31,7 +85,7 @@ const QueryOptionsCard = () => {
</Card.Body> </Card.Body>
</Card>) </Card>)
const view = options.view; const view = options?.view;
return ( return (
<Card> <Card>
@ -39,7 +93,7 @@ const QueryOptionsCard = () => {
<label>{t('html.query.label.view')}</label> <label>{t('html.query.label.view')}</label>
<Row className={"my-2 justify-content-start justify-content-md-center"}> <Row className={"my-2 justify-content-start justify-content-md-center"}>
<Col className={"my-2"}> <Col className={"my-2"}>
<label>{t('html.query.label.from') <label>{t('html.query.label.from') // TODO Remove locale hack when the old frontend is disabled
.replace('</label>', '') .replace('</label>', '')
.replace('>', '')}</label> .replace('>', '')}</label>
</Col> </Col>
@ -60,7 +114,7 @@ const QueryOptionsCard = () => {
/> />
</Col> </Col>
<Col md={1} className={"my-2 text-center"}> <Col md={1} className={"my-2 text-center"}>
<label>{t('html.query.label.to') <label>{t('html.query.label.to') // TODO Remove locale hack when the old frontend is disabled
.replace('</label>', '') .replace('</label>', '')
.replace('>', '')}</label> .replace('>', '')}</label>
</Col> </Col>
@ -81,6 +135,16 @@ const QueryOptionsCard = () => {
/> />
</Col> </Col>
</Row> </Row>
<Row>
<Col md={12}>
<PlayersOnlineGraph
data={graphData}
selectedRange={3}
extremes={extremes}
onSetExtremes={onSetExtremes}
/>
</Col>
</Row>
</Card.Body> </Card.Body>
<button id={"query-button"} className={"btn bg-plan m-2"} disabled={Boolean(invalidFields.length)}> <button id={"query-button"} className={"btn bg-plan m-2"} disabled={Boolean(invalidFields.length)}>
<FontAwesomeIcon icon={faSearch}/> {t('html.query.performQuery')} <FontAwesomeIcon icon={faSearch}/> {t('html.query.performQuery')}

View File

@ -1,29 +1,35 @@
import {useTheme} from "../../hooks/themeHook"; import {useTheme} from "../../hooks/themeHook";
import React, {useEffect} from "react"; import React, {useEffect, useState} from "react";
import {linegraphButtons} from "../../util/graphs"; import {linegraphButtons} from "../../util/graphs";
import Highcharts from "highcharts/highstock"; import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display" import NoDataDisplay from "highcharts/modules/no-data-to-display"
import Accessibility from "highcharts/modules/accessibility" import Accessibility from "highcharts/modules/accessibility"
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
const LineGraph = ({id, series, legendEnabled, tall, yAxis}) => { const LineGraph = ({id, series, legendEnabled, tall, yAxis, selectedRange, extremes, onSetExtremes}) => {
const {t} = useTranslation() const {t} = useTranslation()
const {graphTheming, nightModeEnabled} = useTheme(); const {graphTheming, nightModeEnabled} = useTheme();
const [graph, setGraph] = useState(undefined);
useEffect(() => { useEffect(() => {
NoDataDisplay(Highcharts); NoDataDisplay(Highcharts);
Accessibility(Highcharts); Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}}) Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming); Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, { setGraph(Highcharts.stockChart(id, {
rangeSelector: { rangeSelector: {
selected: 2, selected: selectedRange !== undefined ? selectedRange : 2,
buttons: linegraphButtons buttons: linegraphButtons
}, },
yAxis: yAxis || { yAxis: yAxis || {
softMax: 2, softMax: 2,
softMin: 0 softMin: 0
}, },
xAxis: {
events: {
afterSetExtremes: (event) => onSetExtremes(event)
}
},
title: {text: ''}, title: {text: ''},
plotOptions: { plotOptions: {
areaspline: { areaspline: {
@ -34,8 +40,13 @@ const LineGraph = ({id, series, legendEnabled, tall, yAxis}) => {
enabled: legendEnabled enabled: legendEnabled
}, },
series: series series: series
}) }));
}, [series, graphTheming, id, t, nightModeEnabled, legendEnabled, yAxis]) }, [series, graphTheming, id, t, nightModeEnabled, legendEnabled, yAxis, onSetExtremes, setGraph])
useEffect(() => {
if (graph && extremes) {
graph.xAxis[0].setExtremes(extremes.min, extremes.max);
}
}, [graph, extremes]);
const style = tall ? {height: "450px"} : undefined; const style = tall ? {height: "450px"} : undefined;

View File

@ -4,11 +4,12 @@ import {tooltip} from "../../util/graphs";
import LineGraph from "./LineGraph"; import LineGraph from "./LineGraph";
import {ChartLoader} from "../navigation/Loader"; import {ChartLoader} from "../navigation/Loader";
const PlayersOnlineGraph = ({data}) => { const PlayersOnlineGraph = ({data, selectedRange, extremes, onSetExtremes}) => {
const {t} = useTranslation(); const {t} = useTranslation();
const [series, setSeries] = useState([]); const [series, setSeries] = useState([]);
useEffect(() => { useEffect(() => {
if (!data) return;
const playersOnlineSeries = { const playersOnlineSeries = {
name: t('html.label.playersOnline'), name: t('html.label.playersOnline'),
type: 'areaspline', type: 'areaspline',
@ -23,7 +24,11 @@ const PlayersOnlineGraph = ({data}) => {
if (!data) return <ChartLoader/>; if (!data) return <ChartLoader/>;
return ( return (
<LineGraph id="players-online-graph" series={series}/> <LineGraph id="players-online-graph"
series={series}
selectedRange={selectedRange}
extremes={extremes}
onSetExtremes={onSetExtremes}/>
) )
} }

View File

@ -48,12 +48,14 @@ const DateInputField = ({id, setValue, value, placeholder, setAsInvalid, setAsVa
const invalid = !isValidDate(value); const invalid = !isValidDate(value);
setInvalid(invalid); setInvalid(invalid);
// Value has to change before invalidity events
// because all-valid fields triggers graph refresh with the current value
setValue(value);
if (invalid) { if (invalid) {
setAsInvalid(id); setAsInvalid(id);
} else { } else {
setAsValid(id); setAsValid(id);
} }
setValue(value);
} }
return ( return (

View File

@ -5,7 +5,7 @@ import {faClock} from "@fortawesome/free-regular-svg-icons";
const isValidTime = value => { const isValidTime = value => {
if (!value) return true; if (!value) return true;
const regex = /^[0-2][0-9]:[0-5][0-9]$/; const regex = /^[0-2]\d:[0-5]\d$/;
return regex.test(value); return regex.test(value);
}; };
@ -27,12 +27,14 @@ const TimeInputField = ({id, setValue, value, placeholder, setAsInvalid, setAsVa
const invalid = !isValidTime(value); const invalid = !isValidTime(value);
setInvalid(invalid); setInvalid(invalid);
// Value has to change before invalidity events
// because all-valid fields triggers graph refresh with the current value
setValue(value);
if (invalid) { if (invalid) {
setAsInvalid(id); setAsInvalid(id);
} else { } else {
setAsValid(id); setAsValid(id);
} }
setValue(value);
} }
return ( return (