Add information modal about activity index

This commit is contained in:
Aurora Lahtela 2023-03-04 19:13:35 +02:00
parent 31d6fb1cb1
commit b3a8fee22d
11 changed files with 302 additions and 9 deletions

View File

@ -304,6 +304,19 @@ public enum HtmlLang implements Lang {
QUERY_SERVERS_TWO("html.query.label.servers.two", "using data of 2 servers"),
QUERY_SERVERS_MANY("html.query.label.servers.many", "using data of {number} servers"),
HELP_TEST_RESULT("html.label.help.testResult", "Test result"),
HELP_TEST_IT_OUT("html.label.help.testPrompt", "Test it out:"),
HELP_ACTIVITY_INDEX("html.label.help.activityIndexBasis", "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately."),
HELP_ACTIVITY_INDEX_THRESHOLD("html.label.help.threshold", "Threshold"),
HELP_ACTIVITY_INDEX_WEEK("html.label.help.activityIndexWeek", "Week {}"),
HELP_ACTIVITY_INDEX_THRESHOLD_UNIT("html.label.help.thresholdUnit", "hours / week"),
HELP_ACTIVITY_INDEX_PLAYTIME_UNIT("html.label.help.playtimeUnit", "hours"),
HELP_ACTIVITY_INDEX_EXAMPLE_1("html.label.help.activityIndexExample1", "If someone plays as much as threshold every week, they are given activity index ~3."),
HELP_ACTIVITY_INDEX_EXAMPLE_2("html.label.help.activityIndexExample2", "Very active is ~2x the threshold (y ≥ 3.75)."),
HELP_ACTIVITY_INDEX_EXAMPLE_3("html.label.help.activityIndexExample3", "The index approaches 5 indefinitely."),
HELP_ACTIVITY_INDEX_VISUALIZATION("html.label.help.activityIndexVisual", "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."),
WARNING_NO_GAME_SERVERS("html.description.noGameServers", "Some data requires Plan to be installed on game servers."),
WARNING_NO_GEOLOCATIONS("html.description.noGeolocations", "Geolocation gathering needs to be enabled in the config (Accept GeoLite2 EULA)."),
WARNING_NO_SPONGE_CHUNKS("html.description.noSpongeChunks", "Chunks unavailable on Sponge"),

View File

@ -5,18 +5,27 @@ import {ErrorViewCard} from "../../../../views/ErrorView";
import {Card} from "react-bootstrap";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faChartLine} from "@fortawesome/free-solid-svg-icons";
import React from "react";
import React, {useCallback} from "react";
import PlayerbaseGraph from "../../../graphs/PlayerbaseGraph";
import {CardLoader} from "../../../navigation/Loader";
import {faQuestionCircle} from "@fortawesome/free-regular-svg-icons";
import {useNavigation} from "../../../../hooks/navigationHook";
export const PlayerbaseDevelopmentCardWithData = ({data, title}) => {
const {t} = useTranslation();
const {setHelpModalTopic} = useNavigation();
const openHelp = useCallback(() => setHelpModalTopic('activity-index'), [setHelpModalTopic]);
return (
<Card>
<Card.Header>
<h6 className="col-black">
<h6 className="col-black" style={{width: "100%"}}>
<Fa className="col-amber"
icon={faChartLine}/> {t(title ? title : 'html.label.playerbaseDevelopment')}
<button className={"float-end"} onClick={openHelp}>
<Fa className={"col-blue"}
icon={faQuestionCircle}/>
</button>
</h6>
</Card.Header>
<PlayerbaseGraph data={data}/>

View File

@ -0,0 +1,59 @@
import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import React, {useEffect} from "react";
import NoDataDisplay from "highcharts/modules/no-data-to-display";
import Highcharts from "highcharts/highcharts";
import Accessibility from "highcharts/modules/accessibility";
const FunctionPlotGraph = ({
id,
series,
legendEnabled,
tall,
yPlotLines,
yPlotBands,
xPlotLines,
xPlotBands,
}) => {
const {t} = useTranslation()
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.chart(id, {
yAxis: {
plotLines: yPlotLines,
plotBands: yPlotBands
},
xAxis: {
softMin: -0.5,
plotLines: xPlotLines,
plotBands: xPlotBands,
},
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: legendEnabled,
},
series: series
});
}, [series, id, t, graphTheming, nightModeEnabled, legendEnabled,
yPlotLines, yPlotBands, xPlotLines, xPlotBands]);
const style = tall ? {height: "450px"} : undefined;
return (
<div className="chart-area" style={style} id={id}>
<span className="loader"/>
</div>
)
}
export default FunctionPlotGraph

View File

@ -0,0 +1,41 @@
import {Modal} from "react-bootstrap";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import React, {useCallback} from "react";
import {useNavigation} from "../../hooks/navigationHook";
import {useTranslation} from "react-i18next";
import ActivityIndexHelp from "./help/ActivityIndexHelp";
import {faQuestionCircle} from "@fortawesome/free-regular-svg-icons";
const HelpModal = () => {
const {t} = useTranslation();
const {helpModalTopic, setHelpModalTopic} = useNavigation();
const toggle = useCallback(() => setHelpModalTopic(undefined), [setHelpModalTopic]);
const helpTopics = {
"activity-index": {
title: t('html.label.activityIndex'),
body: <ActivityIndexHelp/>
}
}
const helpTopic = helpTopics[helpModalTopic];
return (
<Modal id="versionModal" aria-labelledby="versionModalLabel" show={Boolean(helpTopic)} onHide={toggle}
size="lg">
<Modal.Header>
<Modal.Title id="versionModalLabel">
<Fa icon={faQuestionCircle}/> {helpTopic?.title}
</Modal.Title>
<button aria-label="Close" className="btn-close" type="button" onClick={toggle}/>
</Modal.Header>
<Modal.Body>
{helpTopic?.body}
</Modal.Body>
<Modal.Footer>
<button className="btn bg-theme" onClick={toggle}>OK</button>
</Modal.Footer>
</Modal>
);
}
export default HelpModal;

View File

@ -0,0 +1,138 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {tooltip} from "../../../util/graphs";
import {withReducedSaturation} from "../../../util/colors";
import {useTheme} from "../../../hooks/themeHook";
import FunctionPlotGraph from "../../graphs/FunctionPlotGraph";
import {useTranslation} from "react-i18next";
import {Form, InputGroup} from "react-bootstrap";
const indexValue = x => {
return 5 - 5 / ((Math.PI * x / 2) + 1);
}
const inverseIndex = y => {
return -2 * y / (Math.PI * (y - 5));
}
const activityIndexPlot = () => {
const data = []
let x;
for (x = 0; x <= 3.5; x += 0.01) {
data.push([x, indexValue(x)]);
}
return data;
}
const ActivityIndexHelp = () => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
const yPlotLines = useMemo(() => [{
color: '#607D8B',
value: 0,
width: 1,
label: {text: t('html.label.inactive')}
}, {
color: 'rgb(76,175,80)',
value: 3.75,
width: 1.5,
label: {text: t('html.label.veryActive')}
}, {
color: 'rgb(139,195,74)',
value: 3,
width: 1.5,
label: {text: t('html.label.active')}
}, {
color: 'rgb(205,220,57)',
value: 2,
width: 1.5,
label: {text: t('html.label.regular')}
}, {
color: 'rgb(255,193,7)',
value: 1,
width: 1.5,
label: {text: t('html.label.irregular')}
}], [t]);
const xPlotLines = useMemo(() => [{
color: 'black',
value: 0,
width: 1
}], []);
const [threshold, setThreshold] = useState(2);
const [week1, setWeek1] = useState(0.5);
const [week2, setWeek2] = useState(0.75);
const [week3, setWeek3] = useState(0.9)
const [result, setResult] = useState(0);
const series = useMemo(() => {
const data = activityIndexPlot();
return [{
name: t('html.label.activityIndex') + ' y=5-5/(πx/2)+1',
data: data,
type: 'spline',
tooltip: tooltip.twoDecimals,
color: nightModeEnabled ? withReducedSaturation('#03A9F4') : '#03A9F4'
}, {
name: t('html.label.help.testResult'),
type: 'scatter',
data: [{x: inverseIndex(result), y: result, marker: {radius: 10}}],
pointPlacement: 0,
width: 5,
tooltip: tooltip.twoDecimals,
color: 'rgb(76,175,80)'
}]
}, [nightModeEnabled, t, result]);
useEffect(() => {
setResult((indexValue(week1 / threshold) + indexValue(week2 / threshold) + indexValue(week3 / threshold)) / 3);
}, [threshold, week1, week2, week3, setResult]);
const onThresholdSet = useCallback((event) => setThreshold(event.target.value), [setThreshold]);
const onWeek1Set = useCallback((event) => setWeek1(event.target.value), [setWeek1]);
const onWeek2Set = useCallback((event) => setWeek2(event.target.value), [setWeek2]);
const onWeek3Set = useCallback((event) => setWeek3(event.target.value), [setWeek3]);
return (
<>
<p>{t('html.label.help.activityIndexBasis')}</p>
<p>{t('html.label.help.activityIndexVisual')}</p>
<FunctionPlotGraph id={'activity-index-graph'}
series={series}
yPlotLines={yPlotLines} xPlotLines={xPlotLines}
legendEnabled/>
<ul>
<li>{t('html.label.help.activityIndexExample1')}</li>
<li>{t('html.label.help.activityIndexExample2')}</li>
<li>{t('html.label.help.activityIndexExample3')}</li>
</ul>
<hr/>
<p>{t('html.label.help.testPrompt')}</p>
<InputGroup className={'mb-2'}>
<InputGroup.Text>{t('html.label.help.threshold')}</InputGroup.Text>
<Form.Control value={threshold} onChange={onThresholdSet} isInvalid={isNaN(threshold)}/>
<InputGroup.Text>{t('html.label.help.thresholdUnit')}</InputGroup.Text>
</InputGroup>
<p>{t('html.label.playtime')}</p>
<InputGroup className={'mb-1'}>
<InputGroup.Text>{t('html.label.help.activityIndexWeek').replace('{}', '1')}</InputGroup.Text>
<Form.Control value={week1} onChange={onWeek1Set} isInvalid={isNaN(week1)}/>
<InputGroup.Text>{t('html.label.help.playtimeUnit')}</InputGroup.Text>
</InputGroup>
<InputGroup className={'mb-1'}>
<InputGroup.Text>{t('html.label.help.activityIndexWeek').replace('{}', '2')}</InputGroup.Text>
<Form.Control value={week2} onChange={onWeek2Set} isInvalid={isNaN(week2)}/>
<InputGroup.Text>{t('html.label.help.playtimeUnit')}</InputGroup.Text>
</InputGroup>
<InputGroup className={'mb-2'}>
<InputGroup.Text>{t('html.label.help.activityIndexWeek').replace('{}', '3')}</InputGroup.Text>
<Form.Control value={week3} onChange={onWeek3Set} isInvalid={isNaN(week3)}/>
<InputGroup.Text>{t('html.label.help.playtimeUnit')}</InputGroup.Text>
</InputGroup>
<p>{t('html.label.help.testResult')} {result.toFixed(2)}</p>
</>
)
};
export default ActivityIndexHelp

View File

@ -1,4 +1,4 @@
import {createContext, useCallback, useContext, useState} from "react";
import {createContext, useCallback, useContext, useMemo, useState} from "react";
const NavigationContext = createContext({});
@ -10,6 +10,7 @@ export const NavigationContextProvider = ({children}) => {
const [items, setItems] = useState([]);
const [sidebarExpanded, setSidebarExpanded] = useState(window.innerWidth > 1350);
const [helpModalTopic, setHelpModalTopic] = useState(undefined);
const setSidebarItems = useCallback((items) => {
const pathname = window.location.href;
@ -49,11 +50,19 @@ export const NavigationContextProvider = ({children}) => {
setSidebarExpanded(!sidebarExpanded);
}, [setSidebarExpanded, sidebarExpanded])
const sharedState = {
const sharedState = useMemo(() => {
return {
currentTab, setCurrentTab,
lastUpdate, updateRequested, updating, requestUpdate, finishUpdate,
sidebarExpanded, setSidebarExpanded, toggleSidebar, sidebarItems: items, setSidebarItems,
helpModalTopic, setHelpModalTopic
}
}, [
currentTab, setCurrentTab,
lastUpdate, updateRequested, updating, requestUpdate, finishUpdate,
sidebarExpanded, setSidebarExpanded, toggleSidebar, sidebarItems: items, setSidebarItems
}
sidebarExpanded, setSidebarExpanded, toggleSidebar, items, setSidebarItems,
helpModalTopic, setHelpModalTopic
]);
return (<NavigationContext.Provider value={sharedState}>
{children}
</NavigationContext.Provider>

View File

@ -27,6 +27,8 @@ import {ServerExtensionContextProvider, useServerExtensionContext} from "../../h
import {iconTypeToFontAwesomeClass} from "../../util/icons";
import {staticSite} from "../../service/backendConfiguration";
const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
const NetworkSidebar = () => {
const {t, i18n} = useTranslation();
const {sidebarItems, setSidebarItems} = useNavigation();
@ -138,6 +140,7 @@ const ServerPage = () => {
</main>
<aside>
<ColorSelectorModal/>
<React.Suspense fallback={""}><HelpModal/></React.Suspense>
</aside>
</div>
</div>

View File

@ -11,6 +11,7 @@ import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons";
import {useDataRequest} from "../../hooks/dataFetchHook";
import ErrorPage from "./ErrorPage";
const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
const PlayerPage = () => {
const {t, i18n} = useTranslation();
@ -59,6 +60,7 @@ const PlayerPage = () => {
</main>
<aside>
<ColorSelectorModal/>
<React.Suspense fallback={""}><HelpModal/></React.Suspense>
</aside>
</div>
</div>

View File

@ -10,6 +10,8 @@ import {useMetadata} from "../../hooks/metadataHook";
import ErrorPage from "./ErrorPage";
import {staticSite} from "../../service/backendConfiguration";
const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
const PlayersPage = () => {
const {t, i18n} = useTranslation();
const {isProxy, networkName, serverName} = useMetadata();
@ -42,6 +44,7 @@ const PlayersPage = () => {
</main>
<aside>
<ColorSelectorModal/>
<React.Suspense fallback={""}><HelpModal/></React.Suspense>
</aside>
</div>
</div>

View File

@ -30,6 +30,8 @@ import {ServerExtensionContextProvider, useServerExtensionContext} from "../../h
import {iconTypeToFontAwesomeClass} from "../../util/icons";
import {staticSite} from "../../service/backendConfiguration";
const HelpModal = React.lazy(() => import("../../components/modal/HelpModal"));
const ServerSidebar = () => {
const {t, i18n} = useTranslation();
const {sidebarItems, setSidebarItems} = useNavigation();
@ -159,6 +161,7 @@ const ServerPage = () => {
</main>
<aside>
<ColorSelectorModal/>
<React.Suspense fallback={""}><HelpModal/></React.Suspense>
</aside>
</div>
</div>

View File

@ -1,7 +1,13 @@
import React from "react";
import React, {useCallback} from "react";
import {Card, Col} from "react-bootstrap";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faAddressBook, faCalendar, faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
import {
faAddressBook,
faCalendar,
faCalendarCheck,
faClock,
faQuestionCircle
} from "@fortawesome/free-regular-svg-icons";
import {
faBookOpen,
faBraille,
@ -31,10 +37,13 @@ import {TableRow} from "../../components/table/TableRow";
import LoadIn from "../../components/animation/LoadIn";
import ExtendableCardBody from "../../components/layout/extension/ExtendableCardBody";
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {useNavigation} from "../../hooks/navigationHook";
const PlayerOverviewCard = ({player}) => {
const {t} = useTranslation();
const {getPlayerHeadImageUrl} = useMetadata();
const {setHelpModalTopic} = useNavigation();
const openHelp = useCallback(() => setHelpModalTopic('activity-index'), [setHelpModalTopic]);
const headImageUrl = getPlayerHeadImageUrl(player.info.name, player.info.uuid)
return (
@ -107,9 +116,13 @@ const PlayerOverviewCard = ({player}) => {
<Col lg={6}>
<Datapoint
icon={faUser} color="amber"
name={t('html.label.activityIndex')}
name={<>{t('html.label.activityIndex')} <span>
<button onClick={openHelp}><Fa className={"col-black"}
icon={faQuestionCircle}/>
</button></span></>}
value={player.info.activity_index} bold
valueLabel={player.info.activity_index_group}
title={t('html.label.activityIndex')}
/>
<Datapoint
icon={faServer} color="light-green"