mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-15 05:41:51 +08:00
Add information modal about activity index
This commit is contained in:
parent
31d6fb1cb1
commit
b3a8fee22d
@ -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"),
|
||||
|
@ -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}/>
|
||||
|
@ -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
|
41
Plan/react/dashboard/src/components/modal/HelpModal.js
Normal file
41
Plan/react/dashboard/src/components/modal/HelpModal.js
Normal 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;
|
@ -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
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user