Management: Group Statistics (#1774)

This commit is contained in:
Alexander Zinchuk 2022-03-19 21:20:04 +01:00
parent 6d063c2132
commit 05a59608fc
22 changed files with 302 additions and 117 deletions

View File

@ -1,12 +1,14 @@
import { Api as GramJs } from '../../../lib/gramjs';
import {
ApiStatistics,
ApiChannelStatistics,
ApiGroupStatistics,
StatisticsGraph,
StatisticsOverviewItem,
StatisticsOverviewPercentage,
StatisticsOverviewPeriod,
} from '../../types';
export function buildStatistics(stats: GramJs.stats.BroadcastStats): ApiStatistics {
export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics {
return {
// Graphs
growthGraph: buildGraph(stats.growthGraph),
@ -31,6 +33,27 @@ export function buildStatistics(stats: GramJs.stats.BroadcastStats): ApiStatisti
};
}
export function buildGroupStatistics(stats: GramJs.stats.MegagroupStats): ApiGroupStatistics {
return {
// Graphs
growthGraph: buildGraph(stats.growthGraph),
membersGraph: buildGraph(stats.membersGraph),
topHoursGraph: buildGraph(stats.topHoursGraph),
// Async graphs
languagesGraph: (stats.languagesGraph as GramJs.StatsGraphAsync).token,
messagesGraph: (stats.messagesGraph as GramJs.StatsGraphAsync).token,
actionsGraph: (stats.actionsGraph as GramJs.StatsGraphAsync).token,
// Statistics overview
period: getOverviewPeriod(stats.period),
members: buildStatisticsOverview(stats.members),
viewers: buildStatisticsOverview(stats.viewers),
messages: buildStatisticsOverview(stats.messages),
posters: buildStatisticsOverview(stats.posters),
};
}
export function buildGraph(result: GramJs.TypeStatsGraph, isPercentage?: boolean): StatisticsGraph {
if ((result as GramJs.StatsGraphError).error) {
throw new Error((result as GramJs.StatsGraphError).error);
@ -41,7 +64,7 @@ export function buildGraph(result: GramJs.TypeStatsGraph, isPercentage?: boolean
const hasSecondYAxis = data.y_scaled;
return {
type: getGraphType(data.types.y0, isPercentage),
type: isPercentage ? 'area' : data.types.y0,
zoomToken: (result as GramJs.StatsGraph).zoomToken,
labelFormatter: data.xTickFormatter,
tooltipFormatter: data.xTooltipFormatter,
@ -63,15 +86,6 @@ export function buildGraph(result: GramJs.TypeStatsGraph, isPercentage?: boolean
};
}
function getGraphType(apiType: string, isPercentage?: boolean): string {
switch (apiType) {
case 'step':
return 'bar';
default:
return isPercentage ? 'area' : apiType;
}
}
function extractColor(color: string): string {
return color.substring(color.indexOf('#'));
}
@ -113,3 +127,10 @@ function buildStatisticsPercentage(data: GramJs.StatsPercentValue): StatisticsOv
percentage: ((data.part / data.total) * 100).toFixed(2),
};
}
function getOverviewPeriod(data: GramJs.StatsDateRangeDays): StatisticsOverviewPeriod {
return {
maxDate: data.maxDate,
minDate: data.minDate,
};
}

View File

@ -77,4 +77,4 @@ export {
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
} from './reactions';
export { fetchStatistics, fetchStatisticsAsyncGraph } from './statistics';
export { fetchChannelStatistics, fetchGroupStatistics, fetchStatisticsAsyncGraph } from './statistics';

View File

@ -1,13 +1,17 @@
import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import { ApiChat, ApiStatistics, StatisticsGraph } from '../../types';
import {
ApiChat, ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph,
} from '../../types';
import { invokeRequest } from './client';
import { buildInputEntity } from '../gramjsBuilders';
import { buildStatistics, buildGraph } from '../apiBuilders/statistics';
import { buildChannelStatistics, buildGroupStatistics, buildGraph } from '../apiBuilders/statistics';
export async function fetchStatistics({ chat }: { chat: ApiChat }): Promise<ApiStatistics | undefined> {
export async function fetchChannelStatistics({
chat,
}: { chat: ApiChat }): Promise<ApiChannelStatistics | undefined> {
const result = await invokeRequest(new GramJs.stats.GetBroadcastStats({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId);
@ -16,7 +20,21 @@ export async function fetchStatistics({ chat }: { chat: ApiChat }): Promise<ApiS
return undefined;
}
return buildStatistics(result);
return buildChannelStatistics(result);
}
export async function fetchGroupStatistics({
chat,
}: { chat: ApiChat }): Promise<ApiGroupStatistics | undefined> {
const result = await invokeRequest(new GramJs.stats.GetMegagroupStats({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId);
if (!result) {
return undefined;
}
return buildGroupStatistics(result);
}
export async function fetchStatisticsAsyncGraph({

View File

@ -1,6 +1,6 @@
import { ApiMessage } from './messages';
export interface ApiStatistics {
export interface ApiChannelStatistics {
growthGraph: StatisticsGraph;
followersGraph: StatisticsGraph;
muteGraph: StatisticsGraph;
@ -16,6 +16,20 @@ export interface ApiStatistics {
recentTopMessages: Array<StatisticsRecentMessage | StatisticsRecentMessage & ApiMessage>;
}
export interface ApiGroupStatistics {
growthGraph: StatisticsGraph;
membersGraph: StatisticsGraph;
topHoursGraph: StatisticsGraph;
languagesGraph: StatisticsGraph | string;
messagesGraph: StatisticsGraph | string;
actionsGraph: StatisticsGraph | string;
period: StatisticsOverviewPeriod;
members: StatisticsOverviewItem;
viewers: StatisticsOverviewItem;
messages: StatisticsOverviewItem;
posters: StatisticsOverviewItem;
}
export interface StatisticsGraph {
type: string;
zoomToken?: string;
@ -49,6 +63,11 @@ export interface StatisticsOverviewPercentage {
percentage: string;
}
export interface StatisticsOverviewPeriod {
maxDate: number;
minDate: number;
}
export interface StatisticsRecentMessage {
msgId: number;
forwards: number;

View File

@ -3,8 +3,9 @@
overflow-x: hidden;
overflow-y: hidden;
&--messages {
&__messages {
padding: 1rem 0.75rem;
border-top: 1px solid var(--color-borders);
&-title {
padding-left: 0.25rem;
@ -23,13 +24,18 @@
overflow-y: scroll !important;
}
.chat-container {
&__graph {
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-borders);
opacity: 1;
transition: opacity 0.3s ease;
&:last-of-type {
margin-bottom: 0;
border-bottom: none;
}
&.hidden {
opacity: 0;
}

View File

@ -1,11 +1,14 @@
import React, {
FC, memo, useState, useEffect, useRef,
FC, memo, useState, useEffect, useRef, useMemo,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import { callApi } from '../../../api/gramjs';
import {
ApiMessage, ApiStatistics, StatisticsRecentMessage as StatisticsRecentMessageType, StatisticsGraph,
ApiMessage,
ApiChannelStatistics,
ApiGroupStatistics,
StatisticsRecentMessage as StatisticsRecentMessageType,
} from '../../../api/types';
import { selectChat, selectStatistics } from '../../../global/selectors';
@ -31,7 +34,7 @@ async function ensureLovelyChart() {
return lovelyChartPromise;
}
const GRAPHS_TITLES = {
const CHANNEL_GRAPHS_TITLES = {
growthGraph: 'ChannelStats.Graph.Growth',
followersGraph: 'ChannelStats.Graph.Followers',
muteGraph: 'ChannelStats.Graph.Notifications',
@ -41,7 +44,17 @@ const GRAPHS_TITLES = {
languagesGraph: 'ChannelStats.Graph.Language',
interactionsGraph: 'ChannelStats.Graph.Interactions',
};
const GRAPHS = Object.keys(GRAPHS_TITLES) as (keyof ApiStatistics)[];
const CHANNEL_GRAPHS = Object.keys(CHANNEL_GRAPHS_TITLES) as (keyof ApiChannelStatistics)[];
const GROUP_GRAPHS_TITLES = {
growthGraph: 'Stats.GroupGrowthTitle',
membersGraph: 'Stats.GroupMembersTitle',
languagesGraph: 'Stats.GroupLanguagesTitle',
messagesGraph: 'Stats.GroupMessagesTitle',
actionsGraph: 'Stats.GroupActionsTitle',
topHoursGraph: 'Stats.GroupTopHoursTitle',
};
const GROUP_GRAPHS = Object.keys(GROUP_GRAPHS_TITLES) as (keyof ApiGroupStatistics)[];
export type OwnProps = {
chatId: string;
@ -49,12 +62,17 @@ export type OwnProps = {
};
export type StateProps = {
statistics: ApiStatistics;
statistics: ApiChannelStatistics | ApiGroupStatistics;
dcId?: number;
isGroup: boolean;
};
const Statistics: FC<OwnProps & StateProps> = ({
chatId, isActive, statistics, dcId,
chatId,
isActive,
statistics,
dcId,
isGroup,
}) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
@ -65,8 +83,8 @@ const Statistics: FC<OwnProps & StateProps> = ({
const { loadStatistics, loadStatisticsAsyncGraph } = getActions();
useEffect(() => {
loadStatistics({ chatId });
}, [chatId, loadStatistics]);
loadStatistics({ chatId, isGroup });
}, [chatId, loadStatistics, isGroup]);
useEffect(() => {
if (!isActive) {
@ -74,25 +92,35 @@ const Statistics: FC<OwnProps & StateProps> = ({
}
}, [isActive]);
const graphs = useMemo(() => {
return isGroup ? GROUP_GRAPHS : CHANNEL_GRAPHS;
}, [isGroup]);
const graphTitles = useMemo(() => {
return isGroup ? GROUP_GRAPHS_TITLES : CHANNEL_GRAPHS_TITLES;
}, [isGroup]);
// Load async graphs
useEffect(() => {
if (!statistics) {
return;
}
GRAPHS.forEach((graph) => {
const isAsync = typeof statistics?.[graph] === 'string';
graphs.forEach((name) => {
const graph = statistics[name as keyof typeof statistics];
const isAsync = typeof graph === 'string';
if (isAsync) {
loadStatisticsAsyncGraph({
name: graph,
name,
chatId,
token: statistics[graph],
token: graph,
// Hardcode percentage for languages graph, since API does not return `percentage` flag
isPercentage: graph === 'languagesGraph',
isPercentage: name === 'languagesGraph',
});
}
});
}, [chatId, statistics, loadStatisticsAsyncGraph]);
}, [graphs, chatId, statistics, loadStatisticsAsyncGraph]);
useEffect(() => {
(async () => {
@ -107,30 +135,32 @@ const Statistics: FC<OwnProps & StateProps> = ({
return;
}
GRAPHS.forEach((graph, index: number) => {
const isAsync = typeof statistics?.[graph] === 'string';
if (isAsync || loadedCharts.current.includes(graph)) {
graphs.forEach((name, index: number) => {
const graph = statistics[name as keyof typeof statistics];
const isAsync = typeof graph === 'string';
if (isAsync || loadedCharts.current.includes(name)) {
return;
}
const { zoomToken } = (statistics[graph] as StatisticsGraph);
const { zoomToken } = graph;
LovelyChart.create(
containerRef.current!.children[index],
{
title: lang((GRAPHS_TITLES as Record<string, string>)[graph]),
title: lang((graphTitles as Record<string, string>)[name]),
...zoomToken && {
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
zoomOutLabel: lang('Graph.ZoomOut'),
},
...(statistics[graph] as StatisticsGraph),
...graph,
},
);
loadedCharts.current.push(graph);
loadedCharts.current.push(name);
});
})();
}, [isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId]);
}, [graphs, graphTitles, isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId]);
if (!isReady || !statistics) {
return <Loading />;
@ -138,21 +168,21 @@ const Statistics: FC<OwnProps & StateProps> = ({
return (
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
<StatisticsOverview statistics={statistics} />
<StatisticsOverview statistics={statistics} isGroup={isGroup} />
{!loadedCharts.current.length && <Loading />}
<div ref={containerRef}>
{GRAPHS.map((graph) => (
<div className={buildClassName('chat-container', !loadedCharts.current.includes(graph) && 'hidden')} />
{graphs.map((graph) => (
<div className={buildClassName('Statistics__graph', !loadedCharts.current.includes(graph) && 'hidden')} />
))}
</div>
{Boolean(statistics.recentTopMessages?.length) && (
<div className="Statistics--messages">
<h2 className="Statistics--messages-title">{lang('ChannelStats.Recent.Header')}</h2>
{Boolean((statistics as ApiChannelStatistics).recentTopMessages?.length) && (
<div className="Statistics__messages">
<h2 className="Statistics__messages-title">{lang('ChannelStats.Recent.Header')}</h2>
{statistics.recentTopMessages.map((message) => (
{(statistics as ApiChannelStatistics).recentTopMessages.map((message) => (
<StatisticsRecentMessage message={message as ApiMessage & StatisticsRecentMessageType} />
))}
</div>
@ -164,8 +194,10 @@ const Statistics: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const statistics = selectStatistics(global, chatId);
const dcId = selectChat(global, chatId)?.fullInfo?.statisticsDcId;
const chat = selectChat(global, chatId);
const dcId = chat?.fullInfo?.statisticsDcId;
const isGroup = chat?.type === 'chatTypeSuperGroup';
return { statistics, dcId };
return { statistics, dcId, isGroup };
},
)(Statistics));

View File

@ -3,7 +3,16 @@
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-borders);
&--title {
&__header {
margin-bottom: 0.5rem;
font-weight: 500;
display: flex;
align-items: center;
justify-content: space-between;
}
&__title {
margin-right: 2em;
padding-left: 0.25rem;
font-size: 16px;
color: var(--text-color);
@ -15,7 +24,12 @@
}
}
&--table {
&__caption {
font-size: 0.75rem;
text-align: right;
}
&__table {
width: 100%;
&-heading {
@ -29,7 +43,7 @@
}
}
&--value {
&__value {
font-size: 0.6875rem;
color: var(--color-text-green);

View File

@ -1,25 +1,60 @@
import React, { FC, memo } from '../../../lib/teact/teact';
import { ApiStatistics, StatisticsOverviewItem } from '../../../api/types';
import { ApiChannelStatistics, ApiGroupStatistics, StatisticsOverviewItem } from '../../../api/types';
import { formatIntegerCompact } from '../../../util/textFormat';
import { formatFullDate } from '../../../util/dateFormat';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import './StatisticsOverview.scss';
export type OwnProps = {
statistics: ApiStatistics;
type OverviewCell = {
name: string;
title: string;
isPercentage?: boolean;
};
const StatisticsOverview: FC<OwnProps> = ({ statistics }) => {
const CHANNEL_OVERVIEW: OverviewCell[][] = [
[
{ name: 'followers', title: 'ChannelStats.Overview.Followers' },
{ name: 'enabledNotifications', title: 'ChannelStats.Overview.EnabledNotifications', isPercentage: true },
],
[
{ name: 'viewsPerPost', title: 'ChannelStats.Overview.ViewsPerPost' },
{ name: 'sharesPerPost', title: 'ChannelStats.Overview.SharesPerPost' },
],
];
const GROUP_OVERVIEW: OverviewCell[][] = [
[
{ name: 'members', title: 'Stats.GroupMembers' },
{ name: 'messages', title: 'Stats.GroupMessages' },
],
[
{ name: 'viewers', title: 'Stats.GroupViewers' },
{ name: 'posters', title: 'Stats.GroupPosters' },
],
];
export type OwnProps = {
isGroup?: boolean;
statistics: ApiChannelStatistics | ApiGroupStatistics;
};
const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
const lang = useLang();
const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => {
if (!change) {
return undefined;
}
const isChangeNegative = Number(change) < 0;
return (
<span className={buildClassName('StatisticsOverview--value', isChangeNegative && 'negative')}>
{isChangeNegative ? change : `+${change}`}
<span className={buildClassName('StatisticsOverview__value', isChangeNegative && 'negative')}>
{isChangeNegative ? `-${formatIntegerCompact(Math.abs(change))}` : `+${formatIntegerCompact(change)}`}
{percentage && (
<>
{' '}
@ -30,40 +65,48 @@ const StatisticsOverview: FC<OwnProps> = ({ statistics }) => {
);
};
const {
followers, viewsPerPost, sharesPerPost, enabledNotifications,
} = statistics;
const { period } = (statistics as ApiGroupStatistics);
return (
<div className="StatisticsOverview">
<h2 className="StatisticsOverview--title">{lang('ChannelStats.Overview')}</h2>
<div className="StatisticsOverview__header">
<div className="StatisticsOverview__title">{lang('ChannelStats.Overview')}</div>
<table className="StatisticsOverview--table">
<tr>
<td>
<b className="StatisticsOverview--table-value">{followers.current}</b> {renderOverviewItemValue(followers)}
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.Followers')}</h3>
</td>
<td>
<b className="StatisticsOverview--table-value">{enabledNotifications.percentage}%</b>
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.EnabledNotifications')}</h3>
</td>
</tr>
{period && (
<div className="StatisticsOverview__caption">
{formatFullDate(lang, period.minDate * 1000)} {formatFullDate(lang, period.maxDate * 1000)}
</div>
)}
</div>
<tr>
<td>
<b className="StatisticsOverview--table-value">{viewsPerPost.current}</b>
{' '}
{renderOverviewItemValue(viewsPerPost)}
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.ViewsPerPost')}</h3>
</td>
<td>
<b className="StatisticsOverview--table-value">{sharesPerPost.current}</b>
{' '}
{renderOverviewItemValue(sharesPerPost)}
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.SharesPerPost')}</h3>
</td>
</tr>
<table className="StatisticsOverview__table">
{(isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => (
<tr>
{row.map((cell: OverviewCell) => {
const field = (statistics as any)[cell.name];
if (cell.isPercentage) {
return (
<td>
<b className="StatisticsOverview__table-value">{field.percentage}%</b>
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
</td>
);
}
return (
<td>
<b className="StatisticsOverview__table-value">
{formatIntegerCompact(field.current)}
</b>
{' '}
{renderOverviewItemValue(field)}
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
</td>
);
})}
</tr>
))}
</table>
</div>
);

View File

@ -2,14 +2,14 @@
position: relative;
padding-left: 3rem;
&--summary {
&__summary {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 0.75rem;
.media-preview--image {
.media-preview__image {
width: 2.5rem;
height: 2.5rem;
position: absolute;
@ -35,24 +35,24 @@
}
}
&--title {
&__title {
display: flex;
align-items: center;
line-height: 1.25rem;
}
&--info {
&__info {
display: flex;
align-items: center;
width: 100%;
color: var(--color-text-meta);
}
&--meta {
&__meta {
font-size: 0.75rem;
}
&--date {
&__date {
flex: 1;
font-size: 0.8125rem;
}

View File

@ -30,20 +30,20 @@ const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
return (
<p className="StatisticsRecentMessage">
<div className="StatisticsRecentMessage--title">
<div className="StatisticsRecentMessage--summary">
<div className="StatisticsRecentMessage__title">
<div className="StatisticsRecentMessage__summary">
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
</div>
<div className="StatisticsRecentMessage--meta">
<div className="StatisticsRecentMessage__meta">
{lang('ChannelStats.ViewsCount', message.views)}
</div>
</div>
<div className="StatisticsRecentMessage--info">
<div className="StatisticsRecentMessage--date">
<div className="StatisticsRecentMessage__info">
<div className="StatisticsRecentMessage__date">
{formatDateTimeToString(message.date * 1000, lang.code)}
</div>
<div className="StatisticsRecentMessage--meta">
<div className="StatisticsRecentMessage__meta">
{message.forwards ? lang('ChannelStats.SharesCount', message.forwards) : 'No shares'}
</div>
</div>
@ -58,7 +58,7 @@ function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRo
return (
<span className="media-preview">
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
<img src={blobUrl} alt="" className={buildClassName('media-preview__image', isRoundVideo && 'round')} />
{getMessageVideo(message) && <i className="icon-play" />}
{renderMessageSummary(lang, message, true)}
</span>

View File

@ -1,27 +1,28 @@
import { addActionHandler, getGlobal } from '../../index';
import { ApiChannelStatistics } from '../../../api/types';
import { callApi } from '../../../api/gramjs';
import { updateStatistics, updateStatisticsGraph } from '../../reducers';
import { selectChatMessages, selectChat } from '../../selectors';
addActionHandler('loadStatistics', async (global, actions, payload) => {
const { chatId } = payload;
const { chatId, isGroup } = payload;
const chat = selectChat(global, chatId);
if (!chat?.fullInfo) {
return undefined;
}
const result = await callApi('fetchStatistics', { chat });
const result = await callApi(isGroup ? 'fetchGroupStatistics' : 'fetchChannelStatistics', { chat });
if (!result) {
return undefined;
}
global = getGlobal();
if (result?.recentTopMessages.length) {
if ((result as ApiChannelStatistics).recentTopMessages?.length) {
const messages = selectChatMessages(global, chatId);
result.recentTopMessages = result.recentTopMessages
(result as ApiChannelStatistics).recentTopMessages = (result as ApiChannelStatistics).recentTopMessages
.map((message) => ({ ...message, ...messages[message.msgId] }));
}

View File

@ -33,6 +33,7 @@ addActionHandler('openChat', (global, actions, payload) => {
global = {
...global,
isStatisticsShown: false,
messages: {
...global.messages,
contentToBeScheduled: undefined,

View File

@ -1,8 +1,8 @@
import { GlobalState } from '../types';
import { ApiStatistics, StatisticsGraph } from '../../api/types';
import { ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph } from '../../api/types';
export function updateStatistics(
global: GlobalState, chatId: string, statistics: ApiStatistics,
global: GlobalState, chatId: string, statistics: ApiChannelStatistics | ApiGroupStatistics,
): GlobalState {
return {
...global,

View File

@ -26,7 +26,8 @@ import {
ApiAvailableReaction,
ApiAppConfig,
ApiSponsoredMessage,
ApiStatistics,
ApiChannelStatistics,
ApiGroupStatistics,
ApiPaymentFormNativeParams, ApiUpdate,
} from '../api/types';
import {
@ -508,7 +509,7 @@ export type GlobalState = {
serviceNotifications: ServiceNotification[];
statistics: {
byChatId: Record<string, ApiStatistics>;
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
};
newContact?: {

View File

@ -1170,4 +1170,5 @@ langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<strin
langpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;`;
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;`;

View File

@ -211,5 +211,6 @@
"messages.setDefaultReaction",
"help.getAppConfig",
"stats.getBroadcastStats",
"stats.getMegagroupStats",
"stats.loadAsyncGraph"
]

View File

@ -152,7 +152,7 @@ function create(container, originalData) {
}
function _onFocus(focusOn) {
if (_data.isBars || _data.isPie) {
if (_data.isBars || _data.isPie || _data.isSteps) {
// TODO animate
_stateManager.update({ focusOn });
}

View File

@ -143,7 +143,6 @@ function calculateYRanges(data, filter, labelFromIndex, labelToIndex, prevState)
const yRanges = calculateYRangesForGroup(data, labelFromIndex, labelToIndex, prevState, filteredDatasets);
if (secondaryYAxisDataset) {
const group = filter[secondaryYAxisDataset.key] ? [secondaryYAxisDataset] : [];
const {
yMinViewport: yMinViewportSecond,
yMaxViewport: yMaxViewportSecond,

View File

@ -408,7 +408,7 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
}
});
if (data.isBars && data.isStacked) {
if ((data.isBars || data.isSteps) && data.isStacked) {
_renderTotal(dataSetContainer, formatInteger(totalValue));
}

View File

@ -48,6 +48,7 @@ export function analyzeData(data) {
onZoom,
isLines: data.type === 'line',
isBars: data.type === 'bar',
isSteps: data.type === 'step',
isAreas: data.type === 'area',
isPie: data.type === 'pie',
yMin: totalYMin,

View File

@ -58,14 +58,14 @@ export function drawDatasets(
drawDataset(datasetType, context, datasetPoints, datasetProjection, options);
});
if (state.focusOn && data.isBars) {
if (state.focusOn && (data.isBars || data.isSteps)) {
const [x0] = toPixels(projection, 0, 0);
const [x1] = toPixels(projection, 1, 0);
drawBarsMask(context, projection, {
focusOn: state.focusOn,
color: getCssColor(colors, 'mask'),
lineWidth: x1 - x0,
lineWidth: data.isSteps ? x1 - x0 + lineWidth : x1 - x0,
});
}
}
@ -76,6 +76,8 @@ function drawDataset(type, ...args) {
return drawDatasetLine(...args);
case 'bar':
return drawDatasetBars(...args);
case 'step':
return drawDatasetSteps(...args);
case 'area':
return drawDatasetArea(...args);
case 'pie':
@ -138,6 +140,31 @@ function drawDatasetBars(context, points, projection, options) {
context.restore();
}
function drawDatasetSteps(context, points, projection, options) {
context.beginPath();
let pixels = [];
for (let j = 0, l = points.length; j < l; j++) {
const { labelIndex, stackValue } = points[j];
pixels.push(
toPixels(projection, labelIndex - PLOT_BARS_WIDTH_SHIFT, stackValue),
toPixels(projection, labelIndex + PLOT_BARS_WIDTH_SHIFT, stackValue),
);
}
pixels.forEach(([x, y]) => {
context.lineTo(x, y);
});
context.save();
context.strokeStyle = options.color;
context.lineWidth = options.lineWidth;
context.globalAlpha = options.opacity;
context.stroke();
context.restore();
}
function drawBarsMask(context, projection, options) {
const [xCenter, yCenter] = projection.getCenter();
const [width, height] = projection.getSize();

View File

@ -17,7 +17,7 @@
&-title {
font-size: 16px;
float: left;
margin-right: 1em;
margin-right: 2em;
text-transform: lowercase;
&:first-letter {