more stats

This commit is contained in:
HF 2022-09-26 13:01:52 +02:00
parent 36c6d87cd3
commit d12a82acdd
11 changed files with 598 additions and 164 deletions

View File

@ -7,10 +7,10 @@ import {
setBrightness,
getDateTimeString,
} from '../core/utils';
import { selectIsDarkMode } from '../store/selectors/gui';
import { parseParagraph } from '../core/MarkdownParser';
const selectStyle = (state) => state.gui.style.indexOf('dark') !== -1;
function ChatMessage({
name,
@ -20,9 +20,7 @@ function ChatMessage({
ts,
openCm,
}) {
const isDarkMode = useSelector(
selectStyle,
);
const isDarkMode = useSelector(selectIsDarkMode);
const refEmbed = useRef();
const isInfo = (name === 'info');

View File

@ -17,11 +17,24 @@ import {
Tooltip,
Legend,
LineController,
ArcElement,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import { Line, Pie } from 'react-chartjs-2';
import { selectIsDarkMode } from '../store/selectors/gui';
import { selectStats } from '../store/selectors/ranks';
import { colorFromText } from '../core/utils';
import {
getCHistChartOpts,
getCHistChartData,
getOnlineStatsOpts,
getOnlineStatsData,
getHistChartOpts,
getHistChartData,
getCPieOpts,
getCPieData,
getPDailyStatsOpts,
getPDailyStatsData,
} from '../core/chartSettings';
ChartJS.register(
CategoryScale,
@ -32,88 +45,10 @@ ChartJS.register(
Tooltip,
Legend,
LineController,
// for pie chart
ArcElement,
);
const options = {
responsive: true,
aspectRatio: 1.2,
color: '#e6e6e6',
scales: {
x: {
grid: {
drawBorder: false,
color: '#656565',
},
ticks: {
color: '#e6e6e6',
},
},
y: {
grid: {
drawBorder: false,
color: '#656565',
},
ticks: {
color: '#e6e6e6',
},
},
},
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
color: '#e6e6e6',
text: 'Top 10 Countries [pxls / day]',
},
},
};
const onlineStatsOptions = {
responsive: true,
color: '#e6e6e6',
scales: {
x: {
grid: {
drawBorder: false,
color: '#656565',
},
ticks: {
color: '#e6e6e6',
},
},
y: {
grid: {
drawBorder: false,
color: '#656565',
},
ticks: {
color: '#e6e6e6',
},
},
},
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
color: '#e6e6e6',
text: 'Players Online per full hour',
},
},
};
const Rankings = () => {
const [area, setArea] = useState('total');
const [
@ -124,82 +59,80 @@ const Rankings = () => {
onlineStats,
cHistStats,
histStats,
pDailyStats,
] = useSelector(selectStats, shallowEqual);
const isDarkMode = useSelector(selectIsDarkMode);
const cHistData = useMemo(() => {
if (area !== 'charts') {
return null;
}
const dataPerCountry = {};
const labels = [];
let ts = Date.now();
let c = cHistStats.length;
while (c) {
const dAmount = cHistStats.length - c;
c -= 1;
// x label
const date = new Date(ts);
labels.unshift(`${date.getUTCMonth() + 1} / ${date.getUTCDate()}`);
ts -= 1000 * 3600 * 24;
// y data per country
const dailyRanks = cHistStats[c];
for (let i = 0; i < dailyRanks.length; i += 1) {
const { cc, px } = dailyRanks[i];
if (!dataPerCountry[cc]) {
dataPerCountry[cc] = [];
}
const countryDat = dataPerCountry[cc];
while (countryDat.length < dAmount) {
countryDat.push(null);
}
countryDat.push(px);
}
}
console.log(dataPerCountry);
const countries = Object.keys(dataPerCountry);
const datasets = countries.map((cc) => {
const color = colorFromText(`${cc}${cc}${cc}${cc}${cc}`);
return {
label: cc,
data: dataPerCountry[cc],
borderColor: color,
backgroundColor: color,
};
});
return {
labels,
datasets,
};
return getCHistChartData(cHistStats);
}, [area, cHistStats]);
const cHistOpts = useMemo(() => {
if (area !== 'charts') {
return null;
}
return getCHistChartOpts(isDarkMode);
}, [area, isDarkMode]);
const onlineData = useMemo(() => {
if (area !== 'charts') {
return null;
}
const labels = [];
const data = [];
let ts = Date.now();
let c = onlineStats.length;
while (c) {
c -= 1;
const date = new Date(ts);
const hours = date.getHours();
const key = hours || `${date.getMonth() + 1} / ${date.getDate()}`;
labels.unshift(String(key));
ts -= 1000 * 3600;
data.push(onlineStats[c]);
}
return {
labels,
datasets: [{
label: 'Players',
data,
borderColor: '#3fadda',
backgroundColor: '#3fadda',
}],
};
return getOnlineStatsData(onlineStats);
}, [area, onlineStats]);
const onlineOpts = useMemo(() => {
if (area !== 'charts') {
return null;
}
return getOnlineStatsOpts(isDarkMode);
}, [area, isDarkMode]);
const histData = useMemo(() => {
if (area !== 'charts') {
return null;
}
return getHistChartData(histStats);
}, [area, histStats]);
const histOpts = useMemo(() => {
if (area !== 'charts') {
return null;
}
return getHistChartOpts(isDarkMode);
}, [area, isDarkMode]);
const pDailyData = useMemo(() => {
if (area !== 'charts') {
return null;
}
return getPDailyStatsData(pDailyStats);
}, [area, pDailyStats]);
const pDailyOpts = useMemo(() => {
if (area !== 'charts') {
return null;
}
return getPDailyStatsOpts(isDarkMode);
}, [area, isDarkMode]);
const cPieData = useMemo(() => {
if (area !== 'countries') {
return null;
}
return getCPieData(dailyCRanking);
}, [area, dailyCRanking]);
const cPieOpts = useMemo(() => {
if (area !== 'countries') {
return null;
}
return getCPieOpts(isDarkMode);
}, [area, isDarkMode]);
return (
<>
<div className="content">
@ -210,7 +143,7 @@ const Rankings = () => {
(area === 'total') ? 'modallink selected' : 'modallink'
}
onClick={() => setArea('total')}
>{t`Total`}</span>
> {t`Total`}</span>
<span className="hdivider" />
<span
role="button"
@ -219,7 +152,7 @@ const Rankings = () => {
(area === 'today') ? 'modallink selected' : 'modallink'
}
onClick={() => setArea('today')}
>{t`Today`}</span>
> {t`Today`}</span>
<span className="hdivider" />
<span
role="button"
@ -228,7 +161,7 @@ const Rankings = () => {
(area === 'yesterday') ? 'modallink selected' : 'modallink'
}
onClick={() => setArea('yesterday')}
>{t`Yesterday`}</span>
> {t`Yesterday`}</span>
<span className="hdivider" />
<span
role="button"
@ -237,7 +170,7 @@ const Rankings = () => {
(area === 'countries') ? 'modallink selected' : 'modallink'
}
onClick={() => setArea('countries')}
>{t`Countries Today`}</span>
> {t`Countries Today`}</span>
<span className="hdivider" />
<span
role="button"
@ -246,8 +179,15 @@ const Rankings = () => {
(area === 'charts') ? 'modallink selected' : 'modallink'
}
onClick={() => setArea('charts')}
>{t`Chart`}</span>
> {t`Charts`}</span>
</div>
<br />
{(area === 'countries') && (
<>
<Pie options={cPieOpts} data={cPieData} />
<br />
</>
)}
{(['total', 'today', 'yesterday', 'countries'].includes(area)) && (
<table style={{
display: 'inline',
@ -336,8 +276,10 @@ const Rankings = () => {
)}
{(area === 'charts') && (
<>
<Line options={options} data={cHistData} />
<Line options={onlineStatsOptions} data={onlineData} />
<Line options={cHistOpts} data={cHistData} />
<Line options={onlineOpts} data={onlineData} />
<Line options={pDailyOpts} data={pDailyData} />
<Line options={histOpts} data={histData} />
</>
)}
<p>

View File

@ -12,6 +12,10 @@ import {
getCountryDailyHistory,
getCountryRanks,
getTopDailyHistory,
storeHourlyPixelsPlaced,
getHourlyPixelStats,
getDailyPixelStats,
populateDailyTotal,
} from '../data/redis/ranks';
import socketEvents from '../socket/socketEvents';
import logger from './logger';
@ -22,13 +26,24 @@ import { DailyCron, HourlyCron } from '../utils/cron';
class Ranks {
constructor() {
this.ranks = {
// ranking today of users by pixels
dailyRanking: [],
// ranking of users by pixels
ranking: [],
// ranking today of countries by pixels
dailyCRanking: [],
// yesterdays ranking of users by pixels
prevTop: [],
// online user amount by hour
onlineStats: [],
// ranking of countries by day
cHistStats: [],
// ranking of users by day
histStats: [],
// pixels placed by hour
pHourlyStats: [],
// pixels placed by day
pDailyStats: [],
};
/*
* we go through socketEvents for sharding
@ -42,6 +57,7 @@ class Ranks {
}
async initialize() {
await populateDailyTotal();
try {
let someRanks = await Ranks.dailyUpdateRanking();
this.ranks = {
@ -95,10 +111,12 @@ class Ranks {
const cHistStats = await getCountryDailyHistory();
const histStats = await getTopDailyHistory();
histStats.users = await populateRanking(histStats.users);
const pHourlyStats = await getHourlyPixelStats();
const ret = {
onlineStats,
cHistStats,
histStats,
pHourlyStats,
};
if (socketEvents.amIImportant()) {
// only main shard sends to others
@ -111,8 +129,10 @@ class Ranks {
const prevTop = await populateRanking(
await getPrevTop(),
);
const pDailyStats = await getDailyPixelStats();
const ret = {
prevTop,
pDailyStats,
};
if (socketEvents.amIImportant()) {
// only main shard sends to others
@ -127,6 +147,7 @@ class Ranks {
}
const amount = socketEvents.onlineCounter.total;
await storeOnlinUserAmount(amount);
await storeHourlyPixelsPlaced();
await Ranks.hourlyUpdateRanking();
}

364
src/core/chartSettings.js Normal file
View File

@ -0,0 +1,364 @@
import { t } from 'ttag';
import { colorFromText } from './utils';
export function getCHistChartOpts(isDarkMode) {
const options = {
responsive: true,
aspectRatio: 1.2,
scales: {
x: {
grid: {
drawBorder: false,
},
},
y: {
grid: {
drawBorder: false,
},
},
},
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: t`Top 10 Countries [pxls / day]`,
},
},
};
if (isDarkMode) {
const sColor = '#e6e6e6';
const lColor = '#656565';
options.color = sColor;
options.scales.x.ticks = {
color: sColor,
};
options.scales.x.grid.color = lColor;
options.scales.y.ticks = {
color: sColor,
};
options.scales.y.grid.color = lColor;
options.plugins.title.color = sColor;
}
return options;
}
export function getCHistChartData(cHistStats) {
const dataPerCountry = {};
const labels = [];
let ts = Date.now();
let c = cHistStats.length;
while (c) {
const dAmount = cHistStats.length - c;
c -= 1;
// x label
const date = new Date(ts);
labels.unshift(`${date.getUTCMonth() + 1} / ${date.getUTCDate()}`);
ts -= 1000 * 3600 * 24;
// y data per country
const dailyRanks = cHistStats[c];
for (let i = 0; i < dailyRanks.length; i += 1) {
const { cc, px } = dailyRanks[i];
if (!dataPerCountry[cc]) {
dataPerCountry[cc] = [];
}
const countryDat = dataPerCountry[cc];
while (countryDat.length < dAmount) {
countryDat.push(null);
}
countryDat.push(px);
}
}
const countries = Object.keys(dataPerCountry);
const datasets = countries.map((cc) => {
const color = colorFromText(`${cc}${cc}${cc}${cc}${cc}`);
return {
label: cc,
data: dataPerCountry[cc],
borderColor: color,
backgroundColor: color,
};
});
return {
labels,
datasets,
};
}
export function getOnlineStatsOpts(isDarkMode) {
const options = {
responsive: true,
scales: {
x: {
grid: {
drawBorder: false,
},
},
y: {
grid: {
drawBorder: false,
},
},
},
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: t`Players Online per full hour`,
},
},
};
if (isDarkMode) {
const sColor = '#e6e6e6';
const lColor = '#656565';
options.color = sColor;
options.scales.x.ticks = {
color: sColor,
};
options.scales.x.grid.color = lColor;
options.scales.y.ticks = {
color: sColor,
};
options.scales.y.grid.color = lColor;
options.plugins.title.color = sColor;
}
return options;
}
export function getOnlineStatsData(onlineStats) {
const labels = [];
const data = [];
let ts = Date.now();
let c = onlineStats.length;
while (c) {
c -= 1;
const date = new Date(ts);
const hours = date.getHours();
const key = hours || `${date.getMonth() + 1} / ${date.getDate()}`;
labels.unshift(String(key));
ts -= 1000 * 3600;
data.push(onlineStats[c]);
}
return {
labels,
datasets: [{
label: 'Players',
data,
borderColor: '#3fadda',
backgroundColor: '#3fadda',
}],
};
}
export function getHistChartOpts(isDarkMode) {
const options = {
responsive: true,
aspectRatio: 1.4,
scales: {
x: {
grid: {
drawBorder: false,
},
},
y: {
grid: {
drawBorder: false,
},
},
},
interaction: {
mode: 'nearest',
axis: 'xy',
intersect: false,
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: t`Top 10 Players [pxls / day]`,
},
},
};
if (isDarkMode) {
const sColor = '#e6e6e6';
const lColor = '#656565';
options.color = sColor;
options.scales.x.ticks = {
color: sColor,
};
options.scales.x.grid.color = lColor;
options.scales.y.ticks = {
color: sColor,
};
options.scales.y.grid.color = lColor;
options.plugins.title.color = sColor;
}
return options;
}
export function getHistChartData(histStats) {
const { users, stats } = histStats;
const dataPerUser = {};
users.forEach((u) => { dataPerUser[u.id] = { name: u.name, data: [] }; });
const labels = [];
let ts = Date.now();
let c = stats.length;
// skipping todays dataset
while (c > 1) {
const dAmount = stats.length - c;
c -= 1;
// x label
ts -= 1000 * 3600 * 24;
const date = new Date(ts);
labels.unshift(`${date.getUTCMonth() + 1} / ${date.getUTCDate()}`);
// y data per user
const dailyRanks = stats[c];
for (let i = 0; i < dailyRanks.length; i += 1) {
const { id, px } = dailyRanks[i];
const userDat = dataPerUser[id].data;
while (userDat.length < dAmount) {
userDat.push(null);
}
userDat.push(px);
}
}
const userIds = Object.keys(dataPerUser);
const datasets = userIds.map((id) => {
const { name, data } = dataPerUser[id];
const color = colorFromText(name);
return {
label: name,
data,
borderColor: color,
backgroundColor: color,
};
});
return {
labels,
datasets,
};
}
export function getCPieOpts(isDarkMode) {
const options = {
responsive: true,
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: t`Countries by Pixels Today`,
},
},
};
if (isDarkMode) {
const sColor = '#e6e6e6';
options.plugins.title.color = sColor;
}
return options;
}
export function getCPieData(dailyCRanking) {
const labels = [];
const data = [];
const backgroundColor = [];
dailyCRanking.forEach((r) => {
const { cc, px } = r;
labels.push(cc);
data.push(px);
const color = colorFromText(`${cc}${cc}${cc}${cc}${cc}`);
backgroundColor.push(color);
});
return {
labels,
datasets: [{
label: '# of Pixels',
data,
backgroundColor,
borderWidth: 1,
}],
};
}
export function getPDailyStatsOpts(isDarkMode) {
const options = {
responsive: true,
scales: {
x: {
grid: {
drawBorder: false,
},
},
y: {
grid: {
drawBorder: false,
},
},
},
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: t`Total Pixels placed per day`,
},
},
};
if (isDarkMode) {
const sColor = '#e6e6e6';
const lColor = '#656565';
options.color = sColor;
options.scales.x.ticks = {
color: sColor,
};
options.scales.x.grid.color = lColor;
options.scales.y.ticks = {
color: sColor,
};
options.scales.y.grid.color = lColor;
options.plugins.title.color = sColor;
}
return options;
}
export function getPDailyStatsData(pDailyStats) {
const labels = [];
const data = [];
let ts = Date.now();
let c = pDailyStats.length;
while (c) {
c -= 1;
ts -= 1000 * 3600 * 24;
const date = new Date(ts);
labels.unshift(`${date.getUTCMonth() + 1} / ${date.getUTCDate()}`);
data.push(pDailyStats[c]);
}
return {
labels,
datasets: [{
label: 'Pixels',
data,
borderColor: '#3fadda',
backgroundColor: '#3fadda',
}],
};
}

View File

@ -11,6 +11,9 @@ const PREV_DAY_TOP_KEY = 'prankd';
const DAY_STATS_RANKS_KEY = 'ds';
const CDAY_STATS_RANKS_KEY = 'cds';
const ONLINE_CNTR_KEY = 'tonl';
const PREV_HOURLY_PLACED_KEY = 'tmph';
const HOURLY_PXL_CNTR_KEY = 'thpx';
const DAILY_PXL_CNTR_KEY = 'tdpx';
/*
* get pixelcount and ranking
@ -116,11 +119,11 @@ export async function getPrevTop() {
*/
export async function storeOnlinUserAmount(amount) {
await client.lPush(ONLINE_CNTR_KEY, String(amount));
await client.lTrim(ONLINE_CNTR_KEY, 0, 14 * 24);
await client.lTrim(ONLINE_CNTR_KEY, 0, 7 * 24);
}
/*
* get list of online counters
* get list of online counters per hour
*/
export async function getOnlineUserStats() {
let onlineStats = await client.lRange(ONLINE_CNTR_KEY, 0, -1);
@ -128,6 +131,63 @@ export async function getOnlineUserStats() {
return onlineStats;
}
/*
* calculate sum of scores of zset
* do NOT use it for large seets
*/
async function sumZSet(key) {
const ranks = await client.zRangeWithScores(key, 0, -1);
let total = 0;
ranks.forEach((r) => { total += Number(r.score); });
return total;
}
/*
* save hourly pixels placed by substracting
* the current daily total pixels set with the ones of an hour ago
*/
export async function storeHourlyPixelsPlaced() {
const tsNow = Date.now();
const prevData = await client.get(PREV_HOURLY_PLACED_KEY);
let prevTs;
let prevSum;
if (prevData) {
[prevTs, prevSum] = prevData.split(',').map((z) => Number(z));
}
let curSum = await sumZSet(DAILY_CRANKED_KEY);
await client.set(PREV_HOURLY_PLACED_KEY, `${tsNow},${curSum}`);
if (prevTs && prevTs > tsNow - 1000 * 3600 * 1.5) {
if (prevSum > curSum) {
// assume day change, add amount of yesterday
const dateKey = getDateKeyOfTs(tsNow - 1000 * 3600 * 24);
curSum += await sumZSet(`${CDAY_STATS_RANKS_KEY}:${dateKey}`);
}
const hourlyPixels = curSum - prevSum;
await client.lPush(HOURLY_PXL_CNTR_KEY, String(hourlyPixels));
await client.lTrim(HOURLY_PXL_CNTR_KEY, 0, 7 * 24);
}
}
/*
* get list of pixels placed per hour
*/
export async function getHourlyPixelStats() {
let pxlStats = await client.lRange(HOURLY_PXL_CNTR_KEY, 0, -1);
pxlStats = pxlStats.map((s) => Number(s));
return pxlStats;
}
/*
* get list of pixels placed per day
*/
export async function getDailyPixelStats() {
let pxlStats = await client.lRange(DAILY_PXL_CNTR_KEY, 0, -1);
pxlStats = pxlStats.map((s) => Number(s));
return pxlStats;
}
/*
* get top 10 of daily pixels over the past days
*/
@ -167,6 +227,20 @@ export async function getTopDailyHistory() {
};
}
/*
* for populating past daily totals
*/
export async function populateDailyTotal() {
await client.del(DAILY_PXL_CNTR_KEY);
for (let i = 14; i > 0; i -= 1) {
const ts = Date.now() - 1000 * 3600 * 24 * i;
const key = `${CDAY_STATS_RANKS_KEY}:${getDateKeyOfTs(ts)}`;
// eslint-disable-next-line no-await-in-loop
const sum = await sumZSet(key);
client.lPush(DAILY_PXL_CNTR_KEY, String(sum));
}
}
/*
* get top 10 countries over the past days
*/
@ -209,12 +283,24 @@ export async function resetDailyRanks() {
const dateKey = getDateKeyOfTs(
Date.now() - 1000 * 3600 * 24,
);
// daily user rank
await client.rename(
DAILY_RANKED_KEY,
`${DAY_STATS_RANKS_KEY}:${dateKey}`,
);
// daily country rank
await client.rename(
DAILY_CRANKED_KEY,
`${CDAY_STATS_RANKS_KEY}:${dateKey}`,
);
// daily pixel counter
const sum = await sumZSet(`${CDAY_STATS_RANKS_KEY}:${dateKey}`);
await client.lPush(DAILY_PXL_CNTR_KEY, String(sum));
await client.lTrim(DAILY_PXL_CNTR_KEY, 0, 28);
// purge old data
const purgeDateKey = getDateKeyOfTs(
Date.now() - 1000 * 3600 * 24 * 21,
);
await client.del(`${DAY_STATS_RANKS_KEY}:${purgeDateKey}`);
await client.del(`${CDAY_STATS_RANKS_KEY}:${purgeDateKey}`);
}

View File

@ -324,7 +324,7 @@ export function requestDeleteAccount(password) {
export function requestRankings() {
return makeAPIGETRequest(
'https://pixelplanet.fun/ranking',
'/ranking',
false,
);
}

View File

@ -279,6 +279,8 @@ export function receiveStats(
onlineStats,
cHistStats,
histStats,
pDailyStats,
pHourlyStats,
} = rankings;
return {
type: 'REC_STATS',
@ -289,6 +291,8 @@ export function receiveStats(
onlineStats,
cHistStats,
histStats,
pDailyStats,
pHourlyStats,
};
}

View File

@ -21,7 +21,9 @@ const initialState = {
prevTop: [],
onlineStats: [],
cHistStats: [],
histStats: [],
histStats: { users: [], stats: [] },
pDailyStats: [],
pHourlyStats: [],
};
export default function ranks(
@ -83,11 +85,10 @@ export default function ranks(
onlineStats,
cHistStats,
histStats,
pDailyStats,
pHourlyStats,
} = action;
const lastFetch = Date.now();
return {
...state,
lastFetch,
const newStats = {
totalRanking,
totalDailyRanking,
dailyCRanking,
@ -95,6 +96,14 @@ export default function ranks(
onlineStats,
cHistStats,
histStats,
pDailyStats,
pHourlyStats,
};
const lastFetch = Date.now();
return {
...state,
lastFetch,
...newStats,
};
}

View File

@ -0,0 +1,9 @@
/*
* selectors related to gui
*/
/* eslint-disable import/prefer-default-export */
export const selectIsDarkMode = (state) => (
state.gui.style.indexOf('dark') !== -1
);

View File

@ -12,4 +12,5 @@ export const selectStats = (state) => [
state.ranks.onlineStats,
state.ranks.cHistStats,
state.ranks.histStats,
state.ranks.pDailyStats,
];

View File

@ -15,7 +15,7 @@ import canvas from './reducers/canvas';
import chat from './reducers/chat';
import fetching from './reducers/fetching';
export const CURRENT_VERSION = 12;
export const CURRENT_VERSION = 14;
export const migrate = (state, version) => {
// eslint-disable-next-line no-underscore-dangle