mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-01-12 15:56:00 +08:00
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:
parent
428a0c5fde
commit
1c3cfbfe4b
@ -24,9 +24,11 @@ public class Cookie {
|
||||
private final String value;
|
||||
|
||||
public Cookie(String rawRepresentation) {
|
||||
String[] split = StringUtils.split(rawRepresentation, "=", 2);
|
||||
name = split[0];
|
||||
value = split[1];
|
||||
this(StringUtils.split(rawRepresentation, "=", 2));
|
||||
}
|
||||
|
||||
private Cookie(String[] splitRawRepresentation) {
|
||||
this(splitRawRepresentation[0], splitRawRepresentation[1]);
|
||||
}
|
||||
|
||||
public Cookie(String name, String value) {
|
||||
|
@ -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 {useTranslation} from "react-i18next";
|
||||
import {useDataRequest} from "../../../hooks/dataFetchHook";
|
||||
@ -9,6 +9,24 @@ import DateInputField from "../../input/DateInputField";
|
||||
import TimeInputField from "../../input/TimeInputField";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
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 {t} = useTranslation();
|
||||
@ -17,12 +35,48 @@ const QueryOptionsCard = () => {
|
||||
const [fromTime, setFromTime] = useState(undefined);
|
||||
const [toDate, setToDate] = useState(undefined);
|
||||
const [toTime, setToTime] = useState(undefined);
|
||||
|
||||
const [invalidFields, setInvalidFields] = useState([]);
|
||||
const setAsInvalid = id => setInvalidFields([...invalidFields, id]);
|
||||
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 [graphData, setGraphData] = useState(undefined);
|
||||
useEffect(() => {
|
||||
if (options) {
|
||||
setGraphData({playersOnline: options.viewPoints, color: '#9E9E9E'})
|
||||
}
|
||||
}, [options, setGraphData]);
|
||||
|
||||
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
||||
if (!options) return (<Card>
|
||||
@ -31,7 +85,7 @@ const QueryOptionsCard = () => {
|
||||
</Card.Body>
|
||||
</Card>)
|
||||
|
||||
const view = options.view;
|
||||
const view = options?.view;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -39,7 +93,7 @@ const QueryOptionsCard = () => {
|
||||
<label>{t('html.query.label.view')}</label>
|
||||
<Row className={"my-2 justify-content-start justify-content-md-center"}>
|
||||
<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>
|
||||
</Col>
|
||||
@ -60,7 +114,7 @@ const QueryOptionsCard = () => {
|
||||
/>
|
||||
</Col>
|
||||
<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>
|
||||
</Col>
|
||||
@ -81,6 +135,16 @@ const QueryOptionsCard = () => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<PlayersOnlineGraph
|
||||
data={graphData}
|
||||
selectedRange={3}
|
||||
extremes={extremes}
|
||||
onSetExtremes={onSetExtremes}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
<button id={"query-button"} className={"btn bg-plan m-2"} disabled={Boolean(invalidFields.length)}>
|
||||
<FontAwesomeIcon icon={faSearch}/> {t('html.query.performQuery')}
|
||||
|
@ -1,29 +1,35 @@
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import React, {useEffect} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {linegraphButtons} from "../../util/graphs";
|
||||
import Highcharts from "highcharts/highstock";
|
||||
import NoDataDisplay from "highcharts/modules/no-data-to-display"
|
||||
import Accessibility from "highcharts/modules/accessibility"
|
||||
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 {graphTheming, nightModeEnabled} = useTheme();
|
||||
const [graph, setGraph] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
NoDataDisplay(Highcharts);
|
||||
Accessibility(Highcharts);
|
||||
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.stockChart(id, {
|
||||
setGraph(Highcharts.stockChart(id, {
|
||||
rangeSelector: {
|
||||
selected: 2,
|
||||
selected: selectedRange !== undefined ? selectedRange : 2,
|
||||
buttons: linegraphButtons
|
||||
},
|
||||
yAxis: yAxis || {
|
||||
softMax: 2,
|
||||
softMin: 0
|
||||
},
|
||||
xAxis: {
|
||||
events: {
|
||||
afterSetExtremes: (event) => onSetExtremes(event)
|
||||
}
|
||||
},
|
||||
title: {text: ''},
|
||||
plotOptions: {
|
||||
areaspline: {
|
||||
@ -34,8 +40,13 @@ const LineGraph = ({id, series, legendEnabled, tall, yAxis}) => {
|
||||
enabled: legendEnabled
|
||||
},
|
||||
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;
|
||||
|
||||
|
@ -4,11 +4,12 @@ import {tooltip} from "../../util/graphs";
|
||||
import LineGraph from "./LineGraph";
|
||||
import {ChartLoader} from "../navigation/Loader";
|
||||
|
||||
const PlayersOnlineGraph = ({data}) => {
|
||||
const PlayersOnlineGraph = ({data, selectedRange, extremes, onSetExtremes}) => {
|
||||
const {t} = useTranslation();
|
||||
const [series, setSeries] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const playersOnlineSeries = {
|
||||
name: t('html.label.playersOnline'),
|
||||
type: 'areaspline',
|
||||
@ -23,7 +24,11 @@ const PlayersOnlineGraph = ({data}) => {
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return (
|
||||
<LineGraph id="players-online-graph" series={series}/>
|
||||
<LineGraph id="players-online-graph"
|
||||
series={series}
|
||||
selectedRange={selectedRange}
|
||||
extremes={extremes}
|
||||
onSetExtremes={onSetExtremes}/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -48,12 +48,14 @@ const DateInputField = ({id, setValue, value, placeholder, setAsInvalid, setAsVa
|
||||
const invalid = !isValidDate(value);
|
||||
setInvalid(invalid);
|
||||
|
||||
// Value has to change before invalidity events
|
||||
// because all-valid fields triggers graph refresh with the current value
|
||||
setValue(value);
|
||||
if (invalid) {
|
||||
setAsInvalid(id);
|
||||
} else {
|
||||
setAsValid(id);
|
||||
}
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -5,7 +5,7 @@ import {faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const isValidTime = value => {
|
||||
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);
|
||||
};
|
||||
|
||||
@ -27,12 +27,14 @@ const TimeInputField = ({id, setValue, value, placeholder, setAsInvalid, setAsVa
|
||||
const invalid = !isValidTime(value);
|
||||
setInvalid(invalid);
|
||||
|
||||
// Value has to change before invalidity events
|
||||
// because all-valid fields triggers graph refresh with the current value
|
||||
setValue(value);
|
||||
if (invalid) {
|
||||
setAsInvalid(id);
|
||||
} else {
|
||||
setAsValid(id);
|
||||
}
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
return (
|
||||
|
Loading…
Reference in New Issue
Block a user