From b3a8fee22d99c66704802f1605a18e8a50a578b6 Mon Sep 17 00:00:00 2001 From: Aurora Lahtela <24460436+AuroraLS3@users.noreply.github.com> Date: Sat, 4 Mar 2023 19:13:35 +0200 Subject: [PATCH] Add information modal about activity index --- .../plan/settings/locale/lang/HtmlLang.java | 13 ++ .../graphs/PlayerbaseDevelopmentCard.js | 13 +- .../components/graphs/FunctionPlotGraph.js | 59 ++++++++ .../src/components/modal/HelpModal.js | 41 ++++++ .../modal/help/ActivityIndexHelp.js | 138 ++++++++++++++++++ .../dashboard/src/hooks/navigationHook.js | 17 ++- .../dashboard/src/views/layout/NetworkPage.js | 3 + .../dashboard/src/views/layout/PlayerPage.js | 2 + .../dashboard/src/views/layout/PlayersPage.js | 3 + .../dashboard/src/views/layout/ServerPage.js | 3 + .../src/views/player/PlayerOverview.js | 19 ++- 11 files changed, 302 insertions(+), 9 deletions(-) create mode 100644 Plan/react/dashboard/src/components/graphs/FunctionPlotGraph.js create mode 100644 Plan/react/dashboard/src/components/modal/HelpModal.js create mode 100644 Plan/react/dashboard/src/components/modal/help/ActivityIndexHelp.js diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index 1dbde7865..aa337bef8 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -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"), diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js index 1d47fd95f..33557be3b 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/PlayerbaseDevelopmentCard.js @@ -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 ( -
+
{t(title ? title : 'html.label.playerbaseDevelopment')} +
diff --git a/Plan/react/dashboard/src/components/graphs/FunctionPlotGraph.js b/Plan/react/dashboard/src/components/graphs/FunctionPlotGraph.js new file mode 100644 index 000000000..c45777b2a --- /dev/null +++ b/Plan/react/dashboard/src/components/graphs/FunctionPlotGraph.js @@ -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 ( +
+ +
+ ) +} + +export default FunctionPlotGraph \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/modal/HelpModal.js b/Plan/react/dashboard/src/components/modal/HelpModal.js new file mode 100644 index 000000000..58c18c64f --- /dev/null +++ b/Plan/react/dashboard/src/components/modal/HelpModal.js @@ -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: + } + } + + const helpTopic = helpTopics[helpModalTopic]; + return ( + + + + {helpTopic?.title} + + + + + ); +} + +export default HelpModal; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/modal/help/ActivityIndexHelp.js b/Plan/react/dashboard/src/components/modal/help/ActivityIndexHelp.js new file mode 100644 index 000000000..bba37c16e --- /dev/null +++ b/Plan/react/dashboard/src/components/modal/help/ActivityIndexHelp.js @@ -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 ( + <> +

{t('html.label.help.activityIndexBasis')}

+

{t('html.label.help.activityIndexVisual')}

+ +
    +
  • {t('html.label.help.activityIndexExample1')}
  • +
  • {t('html.label.help.activityIndexExample2')}
  • +
  • {t('html.label.help.activityIndexExample3')}
  • +
+
+

{t('html.label.help.testPrompt')}

+ + {t('html.label.help.threshold')} + + {t('html.label.help.thresholdUnit')} + +

{t('html.label.playtime')}

+ + {t('html.label.help.activityIndexWeek').replace('{}', '1')} + + {t('html.label.help.playtimeUnit')} + + + {t('html.label.help.activityIndexWeek').replace('{}', '2')} + + {t('html.label.help.playtimeUnit')} + + + {t('html.label.help.activityIndexWeek').replace('{}', '3')} + + {t('html.label.help.playtimeUnit')} + +

{t('html.label.help.testResult')} {result.toFixed(2)}

+ + ) +}; + +export default ActivityIndexHelp \ No newline at end of file diff --git a/Plan/react/dashboard/src/hooks/navigationHook.js b/Plan/react/dashboard/src/hooks/navigationHook.js index 67e94ea4f..6de25478b 100644 --- a/Plan/react/dashboard/src/hooks/navigationHook.js +++ b/Plan/react/dashboard/src/hooks/navigationHook.js @@ -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 ( {children} diff --git a/Plan/react/dashboard/src/views/layout/NetworkPage.js b/Plan/react/dashboard/src/views/layout/NetworkPage.js index fba5d7e19..a74841708 100644 --- a/Plan/react/dashboard/src/views/layout/NetworkPage.js +++ b/Plan/react/dashboard/src/views/layout/NetworkPage.js @@ -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 = () => { diff --git a/Plan/react/dashboard/src/views/layout/PlayerPage.js b/Plan/react/dashboard/src/views/layout/PlayerPage.js index fd3612f0f..6cd640b6b 100644 --- a/Plan/react/dashboard/src/views/layout/PlayerPage.js +++ b/Plan/react/dashboard/src/views/layout/PlayerPage.js @@ -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 = () => { diff --git a/Plan/react/dashboard/src/views/layout/PlayersPage.js b/Plan/react/dashboard/src/views/layout/PlayersPage.js index 66ac254e1..1c3a41dbf 100644 --- a/Plan/react/dashboard/src/views/layout/PlayersPage.js +++ b/Plan/react/dashboard/src/views/layout/PlayersPage.js @@ -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 = () => { diff --git a/Plan/react/dashboard/src/views/layout/ServerPage.js b/Plan/react/dashboard/src/views/layout/ServerPage.js index a7a1a12f3..62bd52d32 100644 --- a/Plan/react/dashboard/src/views/layout/ServerPage.js +++ b/Plan/react/dashboard/src/views/layout/ServerPage.js @@ -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 = () => { diff --git a/Plan/react/dashboard/src/views/player/PlayerOverview.js b/Plan/react/dashboard/src/views/player/PlayerOverview.js index ee96d459f..aca9be40b 100644 --- a/Plan/react/dashboard/src/views/player/PlayerOverview.js +++ b/Plan/react/dashboard/src/views/player/PlayerOverview.js @@ -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}) => { {t('html.label.activityIndex')} + } value={player.info.activity_index} bold valueLabel={player.info.activity_index_group} + title={t('html.label.activityIndex')} />