diff --git a/src/pages/dashboard/default.jsx b/src/pages/dashboard/default.jsx
index 74fbfca..480d93b 100644
--- a/src/pages/dashboard/default.jsx
+++ b/src/pages/dashboard/default.jsx
@@ -15,6 +15,7 @@ import ErrorDisplay from 'components/ErrorDisplay';
import CombinedChartCard from 'sections/dashboard/default/CombinedChartCard';
import UserTable from 'sections/dashboard/default/UserTable';
+import UserRetentionMetrics from 'sections/dashboard/default/UserRetentionMetrics';
import axios from 'axios';
import { useEffect, useState } from 'react';
@@ -713,6 +714,11 @@ export default function DashboardDefault() {
+
+ {/* row 4: User Retention Metrics */}
+
+
+
);
}
diff --git a/src/pages/extra-pages/documentation.jsx b/src/pages/extra-pages/documentation.jsx
index 8d1fb95..9ca0901 100644
--- a/src/pages/extra-pages/documentation.jsx
+++ b/src/pages/extra-pages/documentation.jsx
@@ -422,6 +422,41 @@ export default function Documentation() {
secondary="Interactive visualization showing user distribution across countries. Click on countries to see detailed statistics."
/>
+
+ User Retention Metrics:}
+ secondary={
+
+ Cohort analysis showing how well you retain users over time. This powerful feature helps you understand user loyalty
+ and engagement:
+
+ -
+ Monthly Cohorts - Groups users by their signup month
+
+ -
+ Cohort Table View - Shows retention percentages for each cohort over 7 periods (0-6 months).
+ Period 0 is the signup month (always 100%), and subsequent periods show how many users from that cohort are still
+ active.
+
+ -
+ Retention Curves View - Line chart visualizing retention trends over time for multiple cohorts
+
+ -
+ Color Coding - Green (80%+ retention) indicates excellent retention, yellow (40-80%) shows
+ moderate retention, and red (below 40%) highlights areas needing attention
+
+ -
+ Key Metrics - Average Day 1, Month 3, and Month 6 retention rates at a glance
+
+
+ How to Read It: If the January 2024 cohort shows 85% at +1, it means 85% of users who signed up in
+ January were still active one month later (February). Use this to identify which cohorts had better retention and
+ understand what factors contributed to their success. Look for patterns: Are recent cohorts retaining better than
+ older ones? Is there a consistent drop-off point (e.g., after Month 2)?
+
+ }
+ />
+
+
+ Peak Usage Hours Heatmap:}
+ secondary={
+
+ A weekly pattern visualization showing when users are most active throughout the week. The heatmap displays:
+
+ -
+ 7 rows - Days of the week (Sunday through Saturday)
+
+ -
+ 24 columns - Hours of the day (0-23, representing midnight to 11 PM)
+
+ -
+ Color intensity - Darker colors indicate higher publication activity
+
+
+ The heatmap aggregates ALL publications within your selected date range. For example, if you're viewing a full year of
+ data, all Mondays at 2 PM are counted together, all Tuesdays at 9 AM together, etc. This reveals typical usage
+ patterns: which day/hour combinations see the most activity. Hover over any cell to see the exact publication count
+ for that day/hour combination. Use this to identify peak traffic times for capacity planning, optimal maintenance
+ windows, and understanding user behavior across different time zones.
+
+ }
+ />
+
+
+ Platform Success Rate Chart:}
+ secondary={
+
+ A bar chart comparing the reliability of different platforms (Gmail, Twitter, Telegram, etc.). Shows:
+
+ -
+ Success Rate Percentage - Calculated as (successful publications / total publications) × 100
+
+ -
+ Platform Colors - Each platform displays in its brand color for easy identification
+
+ -
+ Sorted by Performance - Platforms are ordered from highest to lowest success rate
+
+
+ Use this chart to quickly identify which platforms are most reliable and which may need attention. A sudden drop in
+ success rate for a specific platform could indicate API changes, authentication issues, or service disruptions.
+
+ }
+ />
+
+
+ Platform Performance Summary:}
+ secondary={
+
+ Detailed performance cards showing comprehensive statistics for each platform:
+
+ -
+ Platform Name - With brand color indicator
+
+ -
+ Success Rate - Large percentage display (e.g., 98.5%)
+
+ -
+ Success Count - Number of successful publications out of total attempts (e.g., 1,234 / 1,250
+ successful)
+
+ -
+ Failed Count - Number of failed publications (displayed in red if failures exist)
+
+
+ These cards provide at-a-glance metrics for each platform's reliability. Use them to monitor platform health, identify
+ problematic platforms, and track improvements over time. The color-coded indicators make it easy to spot which
+ platforms need investigation.
+
+ }
+ />
+
Detailed Publications Table:}
diff --git a/src/pages/publication/publications.jsx b/src/pages/publication/publications.jsx
index 92867ad..7d7a204 100644
--- a/src/pages/publication/publications.jsx
+++ b/src/pages/publication/publications.jsx
@@ -27,6 +27,8 @@ import AnalyticEcommerce from 'components/cards/statistics/AnalyticEcommerce';
import PublicationChart from 'sections/publications/PublicationChart';
import PlatformDistributionChart from 'sections/publications/PlatformDistributionChart';
import PublicationMap from 'sections/publications/PublicationMap';
+import UsageHeatmap from 'sections/publications/UsageHeatmap';
+import PlatformSuccessRateChart, { PlatformSuccessRateSummary } from 'sections/publications/PlatformSuccessRateChart';
import axios from 'axios';
import { useEffect, useState } from 'react';
@@ -932,6 +934,29 @@ export default function Publications() {
+ {/* Usage Heatmap */}
+
+
+
+
+
+
+
+
+
+
+ {/* Platform Success Rate Summary */}
+
+
+
+ Platform Performance Summary
+
+
+ Detailed success metrics by platform
+
+
+
+
);
}
diff --git a/src/sections/dashboard/default/CombinedChartCard.jsx b/src/sections/dashboard/default/CombinedChartCard.jsx
index 0378a73..8ea188a 100644
--- a/src/sections/dashboard/default/CombinedChartCard.jsx
+++ b/src/sections/dashboard/default/CombinedChartCard.jsx
@@ -18,7 +18,7 @@ import UserBarChart from 'sections/dashboard/UserBarChart';
export default function CombinedChartCard({ filters }) {
const [view, setView] = useState('month');
- const [chartType, setChartType] = useState('bar');
+ const [chartType, setChartType] = useState('area');
const handleChartTypeChange = (event, newType) => {
if (newType !== null) {
diff --git a/src/sections/dashboard/default/UserRetentionMetrics.jsx b/src/sections/dashboard/default/UserRetentionMetrics.jsx
new file mode 100644
index 0000000..fb3f878
--- /dev/null
+++ b/src/sections/dashboard/default/UserRetentionMetrics.jsx
@@ -0,0 +1,365 @@
+import { useEffect, useState, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import {
+ Box,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ ToggleButtonGroup,
+ ToggleButton
+} from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import { LineChart } from '@mui/x-charts/LineChart';
+import axios from 'axios';
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
+import isoWeek from 'dayjs/plugin/isoWeek';
+
+// components
+import MainCard from 'components/MainCard';
+import Loader from 'components/Loader';
+
+dayjs.extend(utc);
+dayjs.extend(isoWeek);
+
+export default function UserRetentionMetrics({ filters }) {
+ const theme = useTheme();
+ const [loading, setLoading] = useState(true);
+ const [cohortData, setCohortData] = useState([]);
+ const [retentionCurves, setRetentionCurves] = useState([]);
+ const [view, setView] = useState('cohort');
+ const [periodType, setPeriodType] = useState('month');
+
+ const today = new Date().toISOString().split('T')[0];
+ const startDate = filters?.startDate || '2021-01-10';
+ const endDate = filters?.endDate || today;
+ const country = filters?.countryCode || '';
+
+ const calculateFuturePeriod = useCallback((basePeriod, periodsAhead) => {
+ const date = dayjs(basePeriod);
+ return date.add(periodsAhead, 'month').format('YYYY-MM-DD');
+ }, []);
+
+ const processCohortData = useCallback(
+ (signupData, retainedData) => {
+ const retainedMap = retainedData.reduce((acc, item) => {
+ acc[item.timeframe] = item.retained_users || 0;
+ return acc;
+ }, {});
+
+ const cohorts = signupData.map((signup, index) => {
+ const signupPeriod = signup.timeframe;
+ const signupCount = signup.signup_users || 0;
+
+ const retentionPeriods = [];
+
+ retentionPeriods.push({
+ period: 0,
+ users: signupCount,
+ percentage: 100
+ });
+
+ for (let i = 1; i <= 6; i++) {
+ const futurePeriod = calculateFuturePeriod(signupPeriod, i);
+ const retainedUsers = retainedMap[futurePeriod] || 0;
+ const retentionPercentage = signupCount > 0 ? ((retainedUsers / signupCount) * 100).toFixed(1) : 0;
+
+ retentionPeriods.push({
+ period: i,
+ users: retainedUsers,
+ percentage: parseFloat(retentionPercentage)
+ });
+ }
+
+ return {
+ cohort: signupPeriod,
+ signupCount,
+ retentionPeriods
+ };
+ });
+
+ return cohorts.slice(0, 12);
+ },
+ [calculateFuturePeriod]
+ );
+
+ const formatCohortName = useCallback((dateStr) => {
+ return dayjs(dateStr).format('MMM YYYY');
+ }, []);
+
+ const processRetentionCurves = useCallback(
+ (cohorts) => {
+ return cohorts.map((cohort) => ({
+ cohortName: formatCohortName(cohort.cohort),
+ data: cohort.retentionPeriods.map((period) => period.percentage)
+ }));
+ },
+ [formatCohortName]
+ );
+
+ useEffect(() => {
+ const fetchRetentionData = async () => {
+ setLoading(true);
+ try {
+ const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
+ const countryParam = country ? `&country_code=${country}` : '';
+
+ const signupResponse = await axios.get(
+ `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}`
+ );
+
+ const retainedResponse = await axios.get(
+ `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}`
+ );
+
+ const signupData = signupResponse?.data?.signup?.data ?? [];
+ const retainedData = retainedResponse?.data?.retained?.data ?? [];
+
+ const cohorts = processCohortData(signupData, retainedData);
+ setCohortData(cohorts);
+
+ const curves = processRetentionCurves(cohorts);
+ setRetentionCurves(curves);
+ } catch (err) {
+ console.error('Error fetching retention data:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchRetentionData();
+ }, [startDate, endDate, country, processCohortData, processRetentionCurves]);
+
+ const getCellColor = (percentage) => {
+ if (percentage >= 80) return theme.palette.success.dark;
+ if (percentage >= 60) return theme.palette.success.main;
+ if (percentage >= 40) return theme.palette.warning.main;
+ if (percentage >= 20) return theme.palette.warning.light;
+ return theme.palette.error.light;
+ };
+
+ const handleViewChange = (event, newView) => {
+ if (newView !== null) {
+ setView(newView);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ User Retention Analysis (Monthly Cohorts)
+
+
+ Track how well you retain users over time with monthly cohort analysis
+
+
+
+
+
+ Cohort Table
+ Retention Curves
+
+
+
+
+ {/* Key Metrics */}
+
+ {cohortData.length > 0 && (
+ <>
+
+
+ Average Day 1 Retention
+
+
+ {(cohortData.reduce((sum, cohort) => sum + (cohort.retentionPeriods[1]?.percentage || 0), 0) / cohortData.length).toFixed(
+ 1
+ )}
+ %
+
+
+
+
+
+ Average Month 3 Retention
+
+
+ {(cohortData.reduce((sum, cohort) => sum + (cohort.retentionPeriods[3]?.percentage || 0), 0) / cohortData.length).toFixed(
+ 1
+ )}
+ %
+
+
+
+
+
+ Average Month 6 Retention
+
+
+ {(cohortData.reduce((sum, cohort) => sum + (cohort.retentionPeriods[6]?.percentage || 0), 0) / cohortData.length).toFixed(
+ 1
+ )}
+ %
+
+
+ >
+ )}
+
+
+
+ {cohortData.length === 0 ? (
+
+ No retention data available for the selected period
+
+ ) : view === 'cohort' ? (
+ // Cohort Table View
+
+
+
+
+
+ Cohort
+
+ Users
+
+
+ Period 0
+
+ {[1, 2, 3, 4, 5, 6].map((period) => (
+
+ +{period}
+
+ ))}
+
+
+
+ {cohortData.map((cohort) => (
+
+ {formatCohortName(cohort.cohort)}
+ {cohort.signupCount.toLocaleString()}
+ {cohort.retentionPeriods.map((period, idx) => (
+
+ {period.percentage}%
+
+ ))}
+
+ ))}
+
+
+
+
+
+ * Period 0 = Signup month, +1 = Next month, +2 = Two months later, etc.
+
+
+
+ ) : (
+ // Retention Curves View
+
+ ({
+ data: curve.data,
+ label: curve.cohortName,
+ curve: 'monotoneX',
+ showMark: true
+ }))}
+ margin={{ top: 20, bottom: 60, left: 60, right: 20 }}
+ grid={{ horizontal: true }}
+ slotProps={{
+ legend: {
+ direction: 'row',
+ position: { vertical: 'bottom', horizontal: 'middle' },
+ padding: 0,
+ itemMarkWidth: 15,
+ itemMarkHeight: 2,
+ markGap: 5,
+ itemGap: 10,
+ labelStyle: {
+ fontSize: 11,
+ fill: theme.palette.text.primary
+ }
+ }
+ }}
+ />
+
+ )}
+
+ );
+}
+
+UserRetentionMetrics.propTypes = {
+ filters: PropTypes.object
+};
diff --git a/src/sections/publications/PlatformDistributionChart.jsx b/src/sections/publications/PlatformDistributionChart.jsx
index 2a8fddf..f3d06ec 100644
--- a/src/sections/publications/PlatformDistributionChart.jsx
+++ b/src/sections/publications/PlatformDistributionChart.jsx
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
// material-ui
@@ -27,15 +27,32 @@ export default function PlatformDistributionChart({ filters }) {
const [loading, setLoading] = useState(true);
const [highlightedItem, setHighlightedItem] = useState(null);
- const colors = [
- theme.palette.primary.main,
- theme.palette.success.main,
- theme.palette.warning.main,
- theme.palette.error.main,
- theme.palette.info.main,
- theme.palette.secondary.main,
- theme.palette.primary[700]
- ];
+ // Platform-specific brand colors
+ const getPlatformColor = useCallback(
+ (platformName) => {
+ const platform = platformName.toLowerCase();
+ switch (platform) {
+ case 'gmail':
+ return '#34A853'; // Gmail green
+ case 'twitter':
+ case 'x':
+ return '#242424ff'; // X/Twitter black
+ case 'telegram':
+ return '#0088CC'; // Telegram blue
+ case 'bluesky':
+ return '#1185FE'; // Bluesky blue
+ case 'mastodon':
+ return '#6364FF'; // Mastodon purple
+ case 'slack':
+ return '#4A154B'; // Slack purple
+ case 'email_bridge':
+ return '#FFA726'; // Orange for email bridge
+ default:
+ return theme.palette.primary.main; // Fallback to theme primary
+ }
+ },
+ [theme.palette.primary.main]
+ );
const handleLegendClick = (event, legendItem) => {
const clickedIndex = data.findIndex((item) => item.label === legendItem.label);
@@ -97,7 +114,8 @@ export default function PlatformDistributionChart({ filters }) {
const chartData = Object.entries(platformCounts).map(([platform, count], index) => ({
id: index,
value: count,
- label: platform
+ label: platform,
+ color: getPlatformColor(platform)
}));
setData(chartData);
@@ -109,7 +127,7 @@ export default function PlatformDistributionChart({ filters }) {
};
fetchData();
- }, [startDate, endDate, status, source, country]);
+ }, [startDate, endDate, status, source, country, getPlatformColor]);
return (
<>
@@ -159,7 +177,7 @@ export default function PlatformDistributionChart({ filters }) {
cy: 130
}
]}
- colors={colors}
+ colors={data.map((item) => item.color)}
height={370}
margin={{ top: 10, bottom: 80, left: 10, right: 10 }}
onItemClick={handleItemClick}
diff --git a/src/sections/publications/PlatformSuccessRateChart.jsx b/src/sections/publications/PlatformSuccessRateChart.jsx
new file mode 100644
index 0000000..975e7ab
--- /dev/null
+++ b/src/sections/publications/PlatformSuccessRateChart.jsx
@@ -0,0 +1,297 @@
+import PropTypes from 'prop-types';
+import { useState, useEffect, useCallback } from 'react';
+import axios from 'axios';
+
+// material-ui
+import { useTheme } from '@mui/material/styles';
+import Typography from '@mui/material/Typography';
+import Box from '@mui/material/Box';
+import { BarChart } from '@mui/x-charts/BarChart';
+
+// components
+import MainCard from 'components/MainCard';
+import Loader from 'components/Loader';
+
+const fetchPlatformData = async (startDate, endDate, source, country) => {
+ const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
+
+ const params = {
+ start_date: startDate,
+ end_date: endDate,
+ page: 1,
+ page_size: 100
+ };
+
+ if (source) params.source = source;
+ if (country) params.country_code = country;
+
+ const firstResponse = await axios.get(`${baseUrl}publications`, { params });
+ const totalPages = firstResponse?.data?.publications?.pagination?.total_pages || 1;
+
+ let allPublications = firstResponse?.data?.publications?.data ?? [];
+
+ if (totalPages > 1) {
+ const pagePromises = [];
+ for (let page = 2; page <= totalPages; page++) {
+ pagePromises.push(
+ axios.get(`${baseUrl}publications`, {
+ params: { ...params, page }
+ })
+ );
+ }
+
+ const responses = await Promise.all(pagePromises);
+ responses.forEach((response) => {
+ const pageData = response?.data?.publications?.data ?? [];
+ allPublications = [...allPublications, ...pageData];
+ });
+ }
+
+ const platformStats = allPublications.reduce((acc, item) => {
+ const platform = item.platform_name || 'Unknown';
+ const status = item.status?.toLowerCase();
+
+ if (!acc[platform]) {
+ acc[platform] = {
+ total: 0,
+ successful: 0,
+ failed: 0
+ };
+ }
+
+ acc[platform].total++;
+ if (status === 'published') {
+ acc[platform].successful++;
+ } else if (status === 'failed') {
+ acc[platform].failed++;
+ }
+
+ return acc;
+ }, {});
+
+ const chartData = Object.entries(platformStats)
+ .map(([platform, stats]) => ({
+ platform,
+ successRate: stats.total > 0 ? parseFloat(((stats.successful / stats.total) * 100).toFixed(2)) : 0,
+ total: stats.total,
+ successful: stats.successful,
+ failed: stats.failed
+ // color: getPlatformColor(platform)
+ }))
+ .sort((a, b) => b.successRate - a.successRate);
+
+ return chartData;
+};
+
+export function PlatformSuccessRateSummary({ filters }) {
+ const theme = useTheme();
+ const today = new Date();
+ const defaultEndDate = today.toISOString().split('T')[0];
+
+ const startDate = filters?.startDate || '2021-01-10';
+ const endDate = filters?.endDate || defaultEndDate;
+ const source = filters?.source || '';
+ const country = filters?.country || '';
+
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true);
+ try {
+ const chartData = await fetchPlatformData(startDate, endDate, source, country);
+ setData(chartData);
+ } catch (error) {
+ console.error('Error fetching platform success rate data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [startDate, endDate, source, country]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+ No data available
+
+ );
+ }
+
+ return (
+
+ {data.map((platform) => (
+
+
+ {/* */}
+
+ {platform.platform.toUpperCase()}
+
+
+
+ {platform.successRate}%
+
+
+ {platform.successful.toLocaleString()} / {platform.total.toLocaleString()} successful
+
+ {platform.failed > 0 && (
+
+ {platform.failed.toLocaleString()} failed
+
+ )}
+
+ ))}
+
+ );
+}
+
+PlatformSuccessRateSummary.propTypes = {
+ filters: PropTypes.shape({
+ startDate: PropTypes.string,
+ endDate: PropTypes.string,
+ source: PropTypes.string,
+ country: PropTypes.string
+ })
+};
+
+export default function PlatformSuccessRateChart({ filters }) {
+ const theme = useTheme();
+
+ const today = new Date();
+ const defaultEndDate = today.toISOString().split('T')[0];
+
+ const startDate = filters?.startDate || '2021-01-10';
+ const endDate = filters?.endDate || defaultEndDate;
+ const source = filters?.source || '';
+ const country = filters?.country || '';
+
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true);
+ try {
+ const chartData = await fetchPlatformData(startDate, endDate, source, country);
+ setData(chartData);
+ } catch (error) {
+ console.error('Error fetching platform success rate data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [startDate, endDate, source, country]);
+
+ return (
+
+ {loading ? (
+
+
+
+ ) : (
+
+
+ Platform Success Rates
+
+
+ Percentage of successful publications by platform (higher is better)
+
+
+ {data.length > 0 ? (
+ <>
+
+ `${value}%`,
+ color: theme.palette.success.main
+ }
+ ]}
+ margin={{ top: 20, bottom: 80, left: 60, right: 20 }}
+ grid={{ horizontal: true }}
+ sx={{
+ '& .MuiBarElement-root': {
+ transition: 'all 0.3s ease',
+ '&:hover': {
+ opacity: 0.8
+ }
+ }
+ }}
+ />
+
+ >
+ ) : (
+
+ No data available
+
+ )}
+
+ )}
+
+ );
+}
+
+PlatformSuccessRateChart.propTypes = {
+ filters: PropTypes.shape({
+ startDate: PropTypes.string,
+ endDate: PropTypes.string,
+ source: PropTypes.string,
+ country: PropTypes.string
+ })
+};
diff --git a/src/sections/publications/PublicationChart.jsx b/src/sections/publications/PublicationChart.jsx
index d73c6bd..506157a 100644
--- a/src/sections/publications/PublicationChart.jsx
+++ b/src/sections/publications/PublicationChart.jsx
@@ -41,7 +41,7 @@ export default function PublicationChart({ filters }) {
const pageSize = 10;
const [totalPages, setTotalPages] = useState(1);
- const primaryColor = theme.palette.success.main;
+ const primaryColor = theme.palette.primary.main;
const errorColor = theme.palette.error.main;
useEffect(() => {
@@ -182,7 +182,7 @@ export default function PublicationChart({ filters }) {
p: 1.5,
border: 1,
borderColor: 'divider',
- borderRadius: 1,
+ borderRadius: 0,
boxShadow: 2
}}
>
diff --git a/src/sections/publications/UsageHeatmap.jsx b/src/sections/publications/UsageHeatmap.jsx
new file mode 100644
index 0000000..aba335d
--- /dev/null
+++ b/src/sections/publications/UsageHeatmap.jsx
@@ -0,0 +1,294 @@
+import { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { Box, Typography } from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import axios from 'axios';
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+
+// components
+import MainCard from 'components/MainCard';
+import Loader from 'components/Loader';
+
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+export default function UsageHeatmap({ filters }) {
+ const theme = useTheme();
+ const [loading, setLoading] = useState(true);
+ const [heatmapData, setHeatmapData] = useState([]);
+ const [error, setError] = useState('');
+
+ const today = new Date().toISOString().split('T')[0];
+ const startDate = filters?.startDate || '2021-01-10';
+ const endDate = filters?.endDate || today;
+ const platform = filters?.platform || '';
+ const status = filters?.status || '';
+ const source = filters?.source || '';
+ const country = filters?.country || '';
+
+ const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+ const hoursOfDay = Array.from({ length: 24 }, (_, i) => i);
+
+ useEffect(() => {
+ const fetchHeatmapData = async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
+
+ const params = {
+ start_date: startDate,
+ end_date: endDate,
+ page: 1,
+ page_size: 100
+ };
+
+ if (platform) params.platform_name = platform;
+ if (status) params.status = status;
+ if (source) params.source = source;
+ if (country) params.country_code = country;
+
+ // Fetch all pages
+ const firstResponse = await axios.get(`${baseUrl}publications`, { params });
+ const totalPages = firstResponse?.data?.publications?.pagination?.total_pages || 1;
+
+ let allPublications = firstResponse?.data?.publications?.data ?? [];
+
+ if (totalPages > 1) {
+ const pagePromises = [];
+ for (let page = 2; page <= totalPages; page++) {
+ pagePromises.push(axios.get(`${baseUrl}publications`, { params: { ...params, page } }));
+ }
+
+ const responses = await Promise.all(pagePromises);
+ responses.forEach((response) => {
+ const pageData = response?.data?.publications?.data ?? [];
+ allPublications = [...allPublications, ...pageData];
+ });
+ }
+
+ const matrix = Array(7)
+ .fill(null)
+ .map(() => Array(24).fill(0));
+
+ allPublications.forEach((pub) => {
+ if (pub.date_created) {
+ const date = dayjs(pub.date_created);
+ const dayOfWeek = date.day(); // 0 = Sunday
+ const hour = date.hour();
+ matrix[dayOfWeek][hour]++;
+ }
+ });
+
+ const maxValue = Math.max(...matrix.flat());
+
+ const formattedData = [];
+ for (let day = 0; day < 7; day++) {
+ for (let hour = 0; hour < 24; hour++) {
+ formattedData.push({
+ day,
+ hour,
+ count: matrix[day][hour],
+ intensity: maxValue > 0 ? matrix[day][hour] / maxValue : 0
+ });
+ }
+ }
+
+ setHeatmapData(formattedData);
+ } catch (err) {
+ console.error('Error fetching heatmap data:', err);
+ setError('Failed to load usage heatmap data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchHeatmapData();
+ }, [startDate, endDate, platform, status, source, country]);
+
+ const getColor = (intensity) => {
+ const isDarkMode = theme.palette.mode === 'dark';
+
+ if (intensity === 0) {
+ return isDarkMode ? theme.palette.grey[800] : theme.palette.grey[100];
+ }
+
+ if (isDarkMode) {
+ if (intensity < 0.2) return theme.palette.primary.darker;
+ if (intensity < 0.4) return theme.palette.primary.dark;
+ if (intensity < 0.6) return theme.palette.primary.main;
+ if (intensity < 0.8) return theme.palette.primary.light;
+ return theme.palette.primary.lighter;
+ } else {
+ if (intensity < 0.2) return theme.palette.primary.lighter;
+ if (intensity < 0.4) return theme.palette.primary.light;
+ if (intensity < 0.6) return theme.palette.primary.main;
+ if (intensity < 0.8) return theme.palette.primary.dark;
+ return theme.palette.primary.darker;
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ {error}
+
+
+ );
+ }
+
+ return (
+
+
+ Peak Usage Hours
+
+
+ Publication activity by day of week and hour of day
+
+
+
+
+ {/* Hour labels */}
+
+ {hoursOfDay.map((hour) => (
+
+ {hour}
+
+ ))}
+
+
+ {/* Heatmap grid */}
+ {daysOfWeek.map((day, dayIndex) => (
+
+ {/* Day label */}
+
+ {day}
+
+
+ {/* Hour cells */}
+ {hoursOfDay.map((hour) => {
+ const dataPoint = heatmapData.find((d) => d.day === dayIndex && d.hour === hour);
+ const count = dataPoint?.count || 0;
+ const intensity = dataPoint?.intensity || 0;
+
+ return (
+
+
+ {count}
+
+
+ );
+ })}
+
+ ))}
+
+ {/* Legend */}
+
+
+ Less
+
+ {[0, 0.2, 0.4, 0.6, 0.8, 1].map((intensity, idx) => (
+
+ ))}
+
+ More
+
+
+
+
+
+ );
+}
+
+UsageHeatmap.propTypes = {
+ filters: PropTypes.object
+};
diff --git a/src/themes/palette.js b/src/themes/palette.js
index a4801de..db3c532 100644
--- a/src/themes/palette.js
+++ b/src/themes/palette.js
@@ -14,9 +14,9 @@ export default function Palette(mode, presetColor) {
// Custom blue color palette based on #336AFF for primary color
const customBlue = [
- '#EEF3FF', // 0 - lightest
- '#D6E4FF', // 1
- '#ADC8FF', // 2
+ '#d8e3fcff', // 0 - lightest
+ '#bfd4fdff', // 1
+ '#a2c1fdff', // 2
'#85ACFF', // 3 - light
'#5C8FFF', // 4
'#336AFF', // 5 - main