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