diff --git a/config/widget_positions.json.overlay2 b/config/widget_positions.json.overlay2 index e8309dfa1..f73b3d519 100644 --- a/config/widget_positions.json.overlay2 +++ b/config/widget_positions.json.overlay2 @@ -11,6 +11,12 @@ "sizeX": 1488, "sizeY": 984 }, + "statistics": { + "positionX": 16, + "positionY": 662, + "sizeX": 1488, + "sizeY": 338 + }, "ticker": { "positionX": 16, "positionY": 1016, diff --git a/src/frontend/overlay/src/components/atoms/ContestLabels.jsx b/src/frontend/overlay/src/components/atoms/ContestLabels.jsx index 5f244ac94..3dac58431 100644 --- a/src/frontend/overlay/src/components/atoms/ContestLabels.jsx +++ b/src/frontend/overlay/src/components/atoms/ContestLabels.jsx @@ -29,7 +29,7 @@ export const IOITaskResult = PropTypes.shape({ const VerdictLabel = styled(ShrinkingBox)` background-color: ${({ color }) => color}; font-size: 14px; - font-weight: 700; + font-weight: ${c.GLOBAL_DEFAULT_FONT_WEIGHT_BOLD}; display: flex; justify-content: center; align-items: center; diff --git a/src/frontend/overlay/src/components/molecules/statistics/StackedBars.tsx b/src/frontend/overlay/src/components/molecules/statistics/StackedBars.tsx new file mode 100644 index 000000000..cd26c1b23 --- /dev/null +++ b/src/frontend/overlay/src/components/molecules/statistics/StackedBars.tsx @@ -0,0 +1,85 @@ +import {Legend, StackedBarsData} from "./types"; +import styled from "styled-components"; +import c from "../../../config"; +import { ProblemLabel } from "../../atoms/ProblemLabel"; + +const BarsWrapper = styled.div` + width: 100%; + height: 100%; + display: grid; + gap: ${c.STATISTICS_BAR_GAP}; + grid-auto-flow: column; + grid-template-rows: repeat(${({rowsCount}) => rowsCount}, 1fr); +` + +const BarWrapper = styled.div` + width: 100%; + height: ${c.STATISTICS_BAR_HEIGHT}; + line-height: ${c.STATISTICS_BAR_HEIGHT}; + display: grid; + gap: 0; + grid-template-columns: ${c.STATISTICS_BAR_HEIGHT} auto; +` + +const BarName = styled(ProblemLabel)` + width: ${c.STATISTICS_BAR_HEIGHT}; + background-color: ${({color}) => color}; + font-size: ${c.GLOBAL_DEFAULT_FONT_SIZE}; + font-family: ${c.GLOBAL_DEFAULT_FONT_FAMILY}; + text-align: center; +` +//todo: set font widget + +const BarValues = styled.div` + width: 100%; + display: flex; + justify-content: flex-start; + background-color: ${c.QUEUE_ROW_BACKGROUND}; + border-radius: 0 ${c.GLOBAL_BORDER_RADIUS} ${c.GLOBAL_BORDER_RADIUS} 0; + overflow: hidden; +` + +const BarValue = styled.div.attrs(({ value, caption }) => ({ + style: { + width: `calc(max(${value * 100}%, ${value === 0 ? 0 : ((caption?.length ?? -1) + 1)}ch))`, + } +}))` + height: ${c.STATISTICS_BAR_HEIGHT}; + line-height: ${c.STATISTICS_BAR_HEIGHT}; + transition: width linear ${c.STATISTICS_CELL_MORPH_TIME}ms; + overflow: hidden; + box-sizing: border-box; + + font-size: ${c.GLOBAL_DEFAULT_FONT_SIZE}; + font-family: ${c.GLOBAL_DEFAULT_FONT_FAMILY}; + + background-color: ${({color}) => color}; + color: ${c.STATISTICS_TITLE_COLOR}; + text-align: center; +` + +interface StackedBarsProps { + data: StackedBarsData; + legend: Legend; +} +export const StackedBars = ({data}: StackedBarsProps) => { + const rowsCount = data.length + return ( + + {data.map((b) => { + return ( + + + + {b.values.map(v => ( + + {v.caption} + + ))} + + + ) + })} + + ); +} diff --git a/src/frontend/overlay/src/components/molecules/statistics/StatisticsLegend.tsx b/src/frontend/overlay/src/components/molecules/statistics/StatisticsLegend.tsx new file mode 100644 index 000000000..8d47c1850 --- /dev/null +++ b/src/frontend/overlay/src/components/molecules/statistics/StatisticsLegend.tsx @@ -0,0 +1,53 @@ +import {Legend} from "./types"; +import styled from "styled-components"; +import c from "../../../config"; + + +const LegendsWrapper = styled.div` + width: 100%; + height: 100%; + display: grid; + gap: ${c.STATISTICS_BAR_GAP}; + //grid-template-columns: auto; + grid-auto-flow: column; + justify-content: end; + align-content: center; +` + +const LegendCardWrapper = styled.div` + width: 100%; + background-color: ${({color}) => color}; + border-radius: ${c.GLOBAL_BORDER_RADIUS}; +` + +const LegendWrapper = styled.div` + line-height: ${c.STATISTICS_BAR_HEIGHT}; + font-size: ${c.GLOBAL_DEFAULT_FONT_SIZE}; + font-family: ${c.GLOBAL_DEFAULT_FONT_FAMILY}; + text-align: center; + margin: 8px 16px; +` + +type LegendCardProps = { color: string; caption: string }; + +export const LegendCard = ({ color, caption }: LegendCardProps) => { + return ( + + + {caption} + + + ); +} + +type StatisticsLegendsProps = { legend: Legend }; + +export const StatisticsLegend = ({legend}: StatisticsLegendsProps) => { + return ( + + {legend?.map((l) => ( + + ))} + + ); +} diff --git a/src/frontend/overlay/src/components/molecules/statistics/types.ts b/src/frontend/overlay/src/components/molecules/statistics/types.ts new file mode 100644 index 000000000..ec7155d92 --- /dev/null +++ b/src/frontend/overlay/src/components/molecules/statistics/types.ts @@ -0,0 +1,25 @@ +export interface BarValue { + readonly color: string; + readonly caption: string; + readonly value: number; +} + +export interface BarData { + readonly name: string; + readonly color: string; + readonly values: BarValue[]; +} + +export interface LegendDescription { + readonly caption: string; + readonly color: string; +} + +export type Legend = LegendDescription[]; + +export type StackedBarsData = BarData[]; + +export interface StatisticsData { + readonly data: StackedBarsData; // | other statistics data + readonly legend: Legend; +} diff --git a/src/frontend/overlay/src/components/organisms/widgets/Advertisement.jsx b/src/frontend/overlay/src/components/organisms/widgets/Advertisement.jsx index 8add0f2e9..59bb74612 100644 --- a/src/frontend/overlay/src/components/organisms/widgets/Advertisement.jsx +++ b/src/frontend/overlay/src/components/organisms/widgets/Advertisement.jsx @@ -1,5 +1,6 @@ import React from "react"; import styled from "styled-components"; +import c from "../../../config"; const AdvertisementContainer = styled.div` width: 100%; @@ -15,7 +16,7 @@ const AdvertisementWrap = styled.div` background-color: white; border-radius: 12px; font-size: 24pt; - font-weight: 700; + font-weight: ${c.GLOBAL_DEFAULT_FONT_WEIGHT_BOLD}; font-family: Urbanist, Passageway, serif; color: black; `; diff --git a/src/frontend/overlay/src/components/organisms/widgets/Queue.jsx b/src/frontend/overlay/src/components/organisms/widgets/Queue.jsx index f662e179e..7f28aa838 100644 --- a/src/frontend/overlay/src/components/organisms/widgets/Queue.jsx +++ b/src/frontend/overlay/src/components/organisms/widgets/Queue.jsx @@ -69,7 +69,7 @@ const fadeOut = () => keyframes` opacity: 100%; } to { - opacity: 0%; + opacity: 0; } `; @@ -189,7 +189,7 @@ const StyledQueueRow = styled.div` gap: 5px; color: white; font-size: 18px; - background: rgba(0, 0, 0, 0.08); + background: ${c.QUEUE_ROW_BACKGROUND}; `; const QueueScoreLabel = styled(ShrinkingBox)` @@ -232,7 +232,7 @@ const QueueWrap = styled.div` position: relative; background-color: ${c.QUEUE_BACKGROUND_COLOR}; background-repeat: no-repeat; - border-radius: 16px; + border-radius: ${c.GLOBAL_BORDER_RADIUS}; padding: 8px; box-sizing: border-box; display: flex; @@ -249,7 +249,7 @@ const RowsContainer = styled.div` const QueueHeader = styled.div` font-size: 32px; - font-weight: 700; + font-weight: ${c.GLOBAL_DEFAULT_FONT_WEIGHT_BOLD}; line-height: 44px; color: white; width: 100%; diff --git a/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.jsx b/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.jsx index 279b0baba..ab4ccc8be 100644 --- a/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.jsx +++ b/src/frontend/overlay/src/components/organisms/widgets/Scoreboard.jsx @@ -31,7 +31,7 @@ const ScoreboardHeader = styled.div` flex-direction: row; font-size: ${c.SCOREBOARD_CAPTION_FONT_SIZE}; font-style: normal; - font-weight: 700; + font-weight: ${c.GLOBAL_DEFAULT_FONT_WEIGHT_BOLD}; padding-top: 0.3em; `; @@ -205,7 +205,7 @@ const ScoreboardTableHeaderWrap = styled(ScoreboardTableRowWrap)` font-size: ${c.SCOREBOARD_TABLE_HEADER_FONT_SIZE}px; font-style: normal; - font-weight: 700; + font-weight: ${c.GLOBAL_DEFAULT_FONT_WEIGHT_BOLD}; line-height: ${c.SCOREBOARD_TABLE_HEADER_HEIGHT}; `; diff --git a/src/frontend/overlay/src/components/organisms/widgets/Statistics.jsx b/src/frontend/overlay/src/components/organisms/widgets/Statistics.jsx deleted file mode 100644 index a8f73190b..000000000 --- a/src/frontend/overlay/src/components/organisms/widgets/Statistics.jsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { Fragment } from "react"; -import styled from "styled-components"; -import { useSelector } from "react-redux"; -import c from "../../../config"; -import { getTeamTaskColor } from "../../../utils/statusInfo"; -import { Cell } from "../../atoms/Cell"; -import { ProblemCell } from "../../atoms/ContestCells"; - -const AllDiv = styled.div` - width: 100%; - height: 100%; - position: relative; -`; - -const StatisticsWrap = styled.div` - width: 100%; - position: absolute; - bottom: 0; - display: flex; - flex-direction: column; - opacity: ${c.STATISTICS_OPACITY}; - background: ${c.STATISTICS_BG_COLOR}; -`; - -const Title = styled.div` - background: ${c.VERDICT_NOK}; - color: ${c.STATISTICS_TITLE_COLOR}; - font-size: ${c.STATISTICS_TITLE_FONT_SIZE}; - text-align: center; - font-family: ${c.CELL_FONT_FAMILY} -`; - -const Table = styled.div` - height: 100%; - display: grid; - /* stylelint-disable-next-line */ - grid-template-columns: auto 1fr; -`; - - -const SubmissionStats = styled.div` - grid-column: 2; - overflow: hidden; - text-align: end; - display: flex; - flex-wrap: wrap; - align-content: center; - height: 100%; - width: 100%; - font-size: ${c.STATISTICS_STATS_VALUE_FONT_SIZE}; - font-family: ${c.STATISTICS_STATS_VALUE_FONT_FAMILY}; - color: ${c.STATISTICS_STATS_VALUE_COLOR}; -`; - -const StatEntry = styled(Cell).attrs(({ targetWidth }) => ({ - style: { - width: targetWidth, - } -}))` - background: ${props => props.color}; - transition: width linear ${c.STATISTICS_CELL_MORPH_TIME}ms; - height: 100%; - overflow: hidden; - float: left; - box-sizing: border-box; - text-align: center; - font-family: ${c.CELL_FONT_FAMILY}; - &:before { - content: ''; - display: inline-block; - } -`; - - -const StatisticsProblemCell = styled(ProblemCell)` - padding: 0 10px; - box-sizing: border-box; -`; - -const getFormattedWidth = (count) => (val, fl) => { - if (fl) { - return `calc(max(${val / count * 100}%, ${val === 0 ? 0 : (val + "").length + 1}ch))`; - } else { - return `${val / count * 100}%`; - } -}; - -export const Statistics = () => { - const statistics = useSelector(state => state.statistics.statistics); - const resultType = useSelector(state => state.contestInfo?.info?.resultType); - const count = useSelector(state => state.contestInfo?.info?.teams?.length); - const tasks = useSelector(state => state.contestInfo?.info?.problems); - const contestData = useSelector((state) => state.contestInfo?.info); - - const calculator = getFormattedWidth(count); - return - - Statistics - - {tasks && statistics?.map(({ result, success, pending, wrong }, index) => { - return - - {resultType === "ICPC" && - - - {success} - - - {pending} - - - {wrong} - - - } - {resultType !== "ICPC" && - - {result.map(({ count, score }, i) => { - return - ; - })} - - } - - ; - })} -
-
-
; -}; -export default Statistics; diff --git a/src/frontend/overlay/src/components/organisms/widgets/Statistics.tsx b/src/frontend/overlay/src/components/organisms/widgets/Statistics.tsx new file mode 100644 index 000000000..5c8962549 --- /dev/null +++ b/src/frontend/overlay/src/components/organisms/widgets/Statistics.tsx @@ -0,0 +1,60 @@ +import styled from "styled-components"; +import {useSelector} from "react-redux"; +import c from "../../../config"; +import {StackedBars} from "../../molecules/statistics/StackedBars"; +import {StatisticsLegend} from "../../molecules/statistics/StatisticsLegend"; +import {stackedBarsData} from "../../../statistics/barData"; + +const StatisticsWrap = styled.div` + width: 100%; + height: 100%; + position: relative; + background-color: ${c.CONTEST_COLOR}; + background-repeat: no-repeat; + border-radius: ${c.GLOBAL_BORDER_RADIUS}; + padding: 8px 16px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 8px; +`; + +const Header = styled.div` + font-size: 32px; + line-height: 44px; + color: white; + width: 100%; + gap: 16px; + display: flex; +`; +const Title = styled.div` + font-weight: ${c.GLOBAL_DEFAULT_FONT_WEIGHT_BOLD}; +`; + +const Caption = styled.div``; + + +export const Statistics = ({ }) => { + // @ts-ignore-start + const resultType = useSelector(state => state.contestInfo?.info?.resultType); + // @ts-ignore + const statistics = useSelector(state => state.statistics.statistics); + // @ts-ignore + const count = useSelector(state => state.contestInfo?.info?.teams?.length); + // @ts-ignore + const tasks = useSelector(state => state.contestInfo?.info?.problems); + const data = stackedBarsData(resultType, tasks, statistics, count); + + return ( + +
+ {c.STATISTICS_TITLE} + {c.STATISTICS_CAPTION} + +
+ + +
+ ) +}; +export default Statistics; diff --git a/src/frontend/overlay/src/config.jsx b/src/frontend/overlay/src/config.jsx index 93f749edc..a31152b4b 100644 --- a/src/frontend/overlay/src/config.jsx +++ b/src/frontend/overlay/src/config.jsx @@ -10,6 +10,8 @@ const visualConfig = await fetch(VISUAL_CONFIG_URL) let config = {}; config.CONTEST_COLOR = "#4C83C3"; +config.CONTEST_CAPTION = "46th"; + config.BASE_URL_WS = (import.meta.env.VITE_WEBSOCKET_URL ?? WS_PROTO + window.location.hostname + ":" + WS_PORT + "/api/overlay"); // Non Styling configs @@ -17,8 +19,10 @@ config.WEBSOCKET_RECONNECT_TIME = 5000; // ms // Strings config.QUEUE_TITLE = "Queue"; -config.QUEUE_CAPTION = "46th"; -config.SCOREBOARD_CAPTION = "46th"; +config.QUEUE_CAPTION = config.CONTEST_CAPTION; +config.SCOREBOARD_CAPTION = config.CONTEST_CAPTION; +config.STATISTICS_TITLE = "Statistics"; +config.STATISTICS_CAPTION = config.CONTEST_CAPTION; // Behaviour config.TICKER_SCOREBOARD_REPEATS = 1; @@ -44,8 +48,9 @@ config.STATISTICS_CELL_MORPH_TIME = 200; //ms config.CELL_FLASH_PERIOD = 500; //ms // Styles > Global -config.GLOBAL_DEFAULT_FONT_FAMILY = "Helvetica; serif"; // css-property +config.GLOBAL_DEFAULT_FONT_FAMILY = "Helvetica, serif"; // css-property config.GLOBAL_DEFAULT_FONT_SIZE = "18px"; // css-property +config.GLOBAL_DEFAULT_FONT_WEIGHT_BOLD = 700; // css-property config.GLOBAL_DEFAULT_FONT = config.GLOBAL_DEFAULT_FONT_SIZE + " " + config.GLOBAL_DEFAULT_FONT_FAMILY; // css property MUST HAVE FONT SIZE config.GLOBAL_BACKGROUND_COLOR = "#242425"; config.GLOBAL_TEXT_COLOR = "#FFF"; @@ -82,7 +87,7 @@ config.SCOREBOARD_TABLE_GAP = 3; //px config.SCOREBOARD_TABLE_ROW_GAP = 1; // px - +config.QUEUE_ROW_BACKGROUND = "rgba(0, 0, 0, 0.08)"; config.QUEUE_ROW_HEIGHT = 41; // px config.QUEUE_ROW_HEIGHT2 = 25; // px config.QUEUE_FTS_PADDING = config.QUEUE_ROW_HEIGHT / 2; // px @@ -104,7 +109,10 @@ config.STATISTICS_TITLE_COLOR = "#FFFFFF"; config.STATISTICS_STATS_VALUE_FONT_SIZE = "24pt"; config.STATISTICS_STATS_VALUE_FONT_FAMILY = config.GLOBAL_DEFAULT_FONT_FAMILY; config.STATISTICS_STATS_VALUE_COLOR = "#FFFFFF"; - +config.STATISTICS_BAR_HEIGHT_PX = 24; +config.STATISTICS_BAR_HEIGHT = `${config.STATISTICS_BAR_HEIGHT_PX}px`; +config.STATISTICS_BAR_GAP_PX = 16; +config.STATISTICS_BAR_GAP = `${config.STATISTICS_BAR_GAP_PX}px`; config.CELL_FONT_FAMILY = config.GLOBAL_DEFAULT_FONT_FAMILY; config.CELL_FONT_SIZE = "18px"; @@ -131,7 +139,7 @@ config.TICKER_SMALL_BACKGROUND = config.VERDICT_NOK; config.TICKER_BACKGROUND = config.CELL_BG_COLOR; config.TICKER_OPACITY = 0.95; config.TICKER_FONT_COLOR = "#FFFFFF"; -config.TICKER_FONT_FAMILY = "Helvetica; serif"; +config.TICKER_FONT_FAMILY = config.GLOBAL_DEFAULT_FONT_FAMILY; config.TICKER_TEXT_FONT_SIZE = "32px"; // css property config.TICKER_TEXT_MARGIN_LEFT = "16px"; // css property config.TICKER_CLOCK_FONT_SIZE = "32px"; // css property diff --git a/src/frontend/overlay/src/statistics/barData.tsx b/src/frontend/overlay/src/statistics/barData.tsx new file mode 100644 index 000000000..9b452b5cf --- /dev/null +++ b/src/frontend/overlay/src/statistics/barData.tsx @@ -0,0 +1,71 @@ +import {StatisticsData} from "../components/molecules/statistics/types"; +import {getTeamTaskColor} from "../utils/statusInfo" +import c from "../config" + +export const stackedBarsData = (resultType: string, tasks: any[], statistics: any[], count: number): StatisticsData => { + if (!tasks || !statistics || !count) { + return { + legend: [], + data: [], + }; + } + + const legend = []; + const bars = []; + if (resultType === "ICPC") { + legend.push({ + caption: "solved", + color: c.VERDICT_OK2, + }); + legend.push({ + caption: "pending", + color: c.VERDICT_UNKNOWN2, + }); + legend.push({ + caption: "incorrect", + color: c.VERDICT_NOK2, + }); + bars.push(...statistics?.map(({result, success, pending, wrong}, index) => ({ + name: tasks[index].letter, + color: tasks[index].color, + values: [ + { + color: c.VERDICT_OK2, + caption: success ? success.toString() : "", + value: count ? success / count : 0.0, + }, + { + color: c.VERDICT_UNKNOWN2, + caption: pending ? pending.toString() : "", + value: count ? pending / count : 0.0, + }, + { + color: c.VERDICT_NOK2, + caption: wrong ? wrong.toString() : "", + value: count ? wrong / count : 0.0, + }, + ] + }))); + } else if (resultType === "IOI") { + legend.push({ + caption: "max score", + color: c.VERDICT_OK2, + }); + legend.push({ + caption: "min score", + color: c.VERDICT_NOK2, + }); + bars.push(...statistics?.map(({result, success, pending, wrong}, index) => ({ + name: tasks[index].letter, + color: tasks[index].color, + values: result.map(({count: rCount, score}) => ({ + color: getTeamTaskColor(score, tasks[index]?.minScore, tasks[index]?.maxScore), + value: count ? rCount / count : 0.0, + })), + }))); + } + return { + legend, + data: bars + }; +} \ No newline at end of file