diff --git a/src/api/gramjs/apiBuilders/statistics.ts b/src/api/gramjs/apiBuilders/statistics.ts index 598b965e..ac9571b9 100644 --- a/src/api/gramjs/apiBuilders/statistics.ts +++ b/src/api/gramjs/apiBuilders/statistics.ts @@ -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, + }; +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index f24b4897..f1224d84 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -77,4 +77,4 @@ export { setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction, } from './reactions'; -export { fetchStatistics, fetchStatisticsAsyncGraph } from './statistics'; +export { fetchChannelStatistics, fetchGroupStatistics, fetchStatisticsAsyncGraph } from './statistics'; diff --git a/src/api/gramjs/methods/statistics.ts b/src/api/gramjs/methods/statistics.ts index 8aa7af7b..21640c6b 100644 --- a/src/api/gramjs/methods/statistics.ts +++ b/src/api/gramjs/methods/statistics.ts @@ -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 { +export async function fetchChannelStatistics({ + chat, +}: { chat: ApiChat }): Promise { 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 { + 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({ diff --git a/src/api/types/statistics.ts b/src/api/types/statistics.ts index 641f7bbc..f0b7acf5 100644 --- a/src/api/types/statistics.ts +++ b/src/api/types/statistics.ts @@ -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; } +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; diff --git a/src/components/right/statistics/Statistics.scss b/src/components/right/statistics/Statistics.scss index 8201221c..dcc75f94 100644 --- a/src/components/right/statistics/Statistics.scss +++ b/src/components/right/statistics/Statistics.scss @@ -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; } diff --git a/src/components/right/statistics/Statistics.tsx b/src/components/right/statistics/Statistics.tsx index 4320e69d..ee8f6671 100644 --- a/src/components/right/statistics/Statistics.tsx +++ b/src/components/right/statistics/Statistics.tsx @@ -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 = ({ - 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 = ({ 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 = ({ } }, [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 = ({ 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)[graph]), + title: lang((graphTitles as Record)[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 ; @@ -138,21 +168,21 @@ const Statistics: FC = ({ return (
- + {!loadedCharts.current.length && }
- {GRAPHS.map((graph) => ( -
+ {graphs.map((graph) => ( +
))}
- {Boolean(statistics.recentTopMessages?.length) && ( -
-

{lang('ChannelStats.Recent.Header')}

+ {Boolean((statistics as ApiChannelStatistics).recentTopMessages?.length) && ( +
+

{lang('ChannelStats.Recent.Header')}

- {statistics.recentTopMessages.map((message) => ( + {(statistics as ApiChannelStatistics).recentTopMessages.map((message) => ( ))}
@@ -164,8 +194,10 @@ const Statistics: FC = ({ export default memo(withGlobal( (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)); diff --git a/src/components/right/statistics/StatisticsOverview.scss b/src/components/right/statistics/StatisticsOverview.scss index 6c708376..2614d366 100644 --- a/src/components/right/statistics/StatisticsOverview.scss +++ b/src/components/right/statistics/StatisticsOverview.scss @@ -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); diff --git a/src/components/right/statistics/StatisticsOverview.tsx b/src/components/right/statistics/StatisticsOverview.tsx index 743a06a4..f236659b 100644 --- a/src/components/right/statistics/StatisticsOverview.tsx +++ b/src/components/right/statistics/StatisticsOverview.tsx @@ -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 = ({ 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 = ({ isGroup, statistics }) => { const lang = useLang(); const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => { + if (!change) { + return undefined; + } + const isChangeNegative = Number(change) < 0; return ( - - {isChangeNegative ? change : `+${change}`} + + {isChangeNegative ? `-${formatIntegerCompact(Math.abs(change))}` : `+${formatIntegerCompact(change)}`} {percentage && ( <> {' '} @@ -30,40 +65,48 @@ const StatisticsOverview: FC = ({ statistics }) => { ); }; - const { - followers, viewsPerPost, sharesPerPost, enabledNotifications, - } = statistics; + const { period } = (statistics as ApiGroupStatistics); return (
-

{lang('ChannelStats.Overview')}

+
+
{lang('ChannelStats.Overview')}
- - - - - + {period && ( +
+ {formatFullDate(lang, period.minDate * 1000)} — {formatFullDate(lang, period.maxDate * 1000)} +
+ )} + - - - - +
- {followers.current} {renderOverviewItemValue(followers)} -

{lang('ChannelStats.Overview.Followers')}

-
- {enabledNotifications.percentage}% -

{lang('ChannelStats.Overview.EnabledNotifications')}

-
- {viewsPerPost.current} - {' '} - {renderOverviewItemValue(viewsPerPost)} -

{lang('ChannelStats.Overview.ViewsPerPost')}

-
- {sharesPerPost.current} - {' '} - {renderOverviewItemValue(sharesPerPost)} -

{lang('ChannelStats.Overview.SharesPerPost')}

-
+ {(isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => ( + + {row.map((cell: OverviewCell) => { + const field = (statistics as any)[cell.name]; + + if (cell.isPercentage) { + return ( + + ); + } + + return ( + + ); + })} + + ))}
+ {field.percentage}% +

{lang(cell.title)}

+
+ + {formatIntegerCompact(field.current)} + + {' '} + {renderOverviewItemValue(field)} +

{lang(cell.title)}

+
); diff --git a/src/components/right/statistics/StatisticsRecentMessage.scss b/src/components/right/statistics/StatisticsRecentMessage.scss index 143aba45..543b2d36 100644 --- a/src/components/right/statistics/StatisticsRecentMessage.scss +++ b/src/components/right/statistics/StatisticsRecentMessage.scss @@ -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; } diff --git a/src/components/right/statistics/StatisticsRecentMessage.tsx b/src/components/right/statistics/StatisticsRecentMessage.tsx index 7cdf52aa..5a1a863c 100644 --- a/src/components/right/statistics/StatisticsRecentMessage.tsx +++ b/src/components/right/statistics/StatisticsRecentMessage.tsx @@ -30,20 +30,20 @@ const StatisticsRecentMessage: FC = ({ message }) => { return (

-

-
+
+
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
-
+
{lang('ChannelStats.ViewsCount', message.views)}
-
-
+
+
{formatDateTimeToString(message.date * 1000, lang.code)}
-
+
{message.forwards ? lang('ChannelStats.SharesCount', message.forwards) : 'No shares'}
@@ -58,7 +58,7 @@ function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRo return ( - + {getMessageVideo(message) && } {renderMessageSummary(lang, message, true)} diff --git a/src/global/actions/api/statistics.ts b/src/global/actions/api/statistics.ts index bff47ca2..8c703eca 100644 --- a/src/global/actions/api/statistics.ts +++ b/src/global/actions/api/statistics.ts @@ -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] })); } diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index da8bfd9c..4372cf01 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -33,6 +33,7 @@ addActionHandler('openChat', (global, actions, payload) => { global = { ...global, + isStatisticsShown: false, messages: { ...global.messages, contentToBeScheduled: undefined, diff --git a/src/global/reducers/statistics.ts b/src/global/reducers/statistics.ts index 2fd2a73a..df81319d 100644 --- a/src/global/reducers/statistics.ts +++ b/src/global/reducers/statistics.ts @@ -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, diff --git a/src/global/types.ts b/src/global/types.ts index a18abccb..fdf46dee 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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; + byChatId: Record; }; newContact?: { diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index b66f1fdb..3cf7ae3b 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1170,4 +1170,5 @@ langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector; folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats; -stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;`; \ No newline at end of file +stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph; +stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;`; \ No newline at end of file diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index e9d4605d..aeef75df 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -211,5 +211,6 @@ "messages.setDefaultReaction", "help.getAppConfig", "stats.getBroadcastStats", + "stats.getMegagroupStats", "stats.loadAsyncGraph" ] diff --git a/src/lib/lovely-chart/LovelyChart.js b/src/lib/lovely-chart/LovelyChart.js index 70774470..7e054501 100644 --- a/src/lib/lovely-chart/LovelyChart.js +++ b/src/lib/lovely-chart/LovelyChart.js @@ -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 }); } diff --git a/src/lib/lovely-chart/StateManager.js b/src/lib/lovely-chart/StateManager.js index 25701782..819c7f6a 100644 --- a/src/lib/lovely-chart/StateManager.js +++ b/src/lib/lovely-chart/StateManager.js @@ -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, diff --git a/src/lib/lovely-chart/Tooltip.js b/src/lib/lovely-chart/Tooltip.js index 0df59877..4cd85d8b 100644 --- a/src/lib/lovely-chart/Tooltip.js +++ b/src/lib/lovely-chart/Tooltip.js @@ -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)); } diff --git a/src/lib/lovely-chart/data.js b/src/lib/lovely-chart/data.js index c735b114..b57d258c 100644 --- a/src/lib/lovely-chart/data.js +++ b/src/lib/lovely-chart/data.js @@ -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, diff --git a/src/lib/lovely-chart/drawDatasets.js b/src/lib/lovely-chart/drawDatasets.js index 891572a9..762bba25 100644 --- a/src/lib/lovely-chart/drawDatasets.js +++ b/src/lib/lovely-chart/drawDatasets.js @@ -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(); diff --git a/src/lib/lovely-chart/styles/_header.scss b/src/lib/lovely-chart/styles/_header.scss index a1f21efe..56e64e3b 100644 --- a/src/lib/lovely-chart/styles/_header.scss +++ b/src/lib/lovely-chart/styles/_header.scss @@ -17,7 +17,7 @@ &-title { font-size: 16px; float: left; - margin-right: 1em; + margin-right: 2em; text-transform: lowercase; &:first-letter {