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