diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
index b2ea2a83..896c733a 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
@@ -21,30 +21,28 @@ export function WebsiteChart({
const { pageviews, sessions, compare } = (data || {}) as any;
const chartData = useMemo(() => {
- if (data) {
- const result = {
- pageviews,
- sessions,
- };
+ if (!data) {
+ return { pageviews: [], sessions: [] };
+ }
- if (compare) {
- result.compare = {
- pageviews: result.pageviews.map(({ x }, i) => ({
+ return {
+ pageviews,
+ sessions,
+ ...(compare && {
+ compare: {
+ pageviews: pageviews.map(({ x }, i) => ({
x,
y: compare.pageviews[i]?.y,
d: compare.pageviews[i]?.x,
})),
- sessions: result.sessions.map(({ x }, i) => ({
+ sessions: sessions.map(({ x }, i) => ({
x,
y: compare.sessions[i]?.y,
d: compare.sessions[i]?.x,
})),
- };
- }
-
- return result;
- }
- return { pageviews: [], sessions: [] };
+ },
+ }),
+ };
}, [data, startDate, endDate, unit]);
return (
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
index 6c91ba6d..605ee385 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx
@@ -7,14 +7,18 @@ import { formatLongNumber, formatShortTime } from '@/lib/format';
export function WebsiteMetricsBar({
websiteId,
+ compareMode,
}: {
websiteId: string;
showChange?: boolean;
compareMode?: boolean;
}) {
- const { isAllTime } = useDateRange();
+ const { isAllTime, dateCompare } = useDateRange();
const { formatMessage, labels, getErrorMessage } = useMessages();
- const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
+ const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({
+ websiteId,
+ compare: compareMode ? dateCompare?.compare : undefined,
+ });
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
diff --git a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
index f587e112..5acc9e68 100644
--- a/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsitePage.tsx
@@ -1,7 +1,8 @@
'use client';
-import { Column } from '@umami/react-zen';
+import { Column, Row } from '@umami/react-zen';
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
import { Panel } from '@/components/common/Panel';
+import { UnitFilter } from '@/components/input/UnitFilter';
import { WebsiteChart } from './WebsiteChart';
import { WebsiteControls } from './WebsiteControls';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
@@ -13,6 +14,9 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
+
+
+
diff --git a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
index bca8d244..32d641b0 100644
--- a/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
+++ b/src/app/(main)/websites/[websiteId]/compare/ComparePage.tsx
@@ -10,7 +10,7 @@ export function ComparePage({ websiteId }: { websiteId: string }) {
return (
-
+
diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts
index b7177b5d..9d21f4f5 100644
--- a/src/app/api/websites/[websiteId]/stats/route.ts
+++ b/src/app/api/websites/[websiteId]/stats/route.ts
@@ -31,9 +31,11 @@ export async function GET(
const data = await getWebsiteStats(websiteId, filters);
- const compare = filters.compare ?? 'prev';
-
- const { startDate, endDate } = getCompareDate(compare, filters.startDate, filters.endDate);
+ const { startDate, endDate } = getCompareDate(
+ filters.compare ?? 'prev',
+ filters.startDate,
+ filters.endDate,
+ );
const comparison = await getWebsiteStats(websiteId, {
...filters,
diff --git a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
index b2e90199..1611c7f8 100644
--- a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
+++ b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
@@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery(
options?: ReactQueryOptions,
) {
const { get, useQuery } = useApi();
- const { startAt, endAt, unit, timezone } = useDateParameters();
+ const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery({
@@ -29,8 +29,6 @@ export function useWebsiteExpandedMetricsQuery(
websiteId,
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
},
@@ -39,8 +37,6 @@ export function useWebsiteExpandedMetricsQuery(
get(`/websites/${websiteId}/metrics/expanded`, {
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
}),
diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
index 67c5e4d4..cd064af6 100644
--- a/src/components/hooks/queries/useWebsiteMetricsQuery.ts
+++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
@@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
options?: ReactQueryOptions,
) {
const { get, useQuery } = useApi();
- const { startAt, endAt, unit, timezone } = useDateParameters();
+ const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery({
@@ -25,8 +25,6 @@ export function useWebsiteMetricsQuery(
websiteId,
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
},
@@ -35,8 +33,6 @@ export function useWebsiteMetricsQuery(
get(`/websites/${websiteId}/metrics`, {
startAt,
endAt,
- unit,
- timezone,
...filters,
...params,
}),
diff --git a/src/components/hooks/queries/useWebsiteStatsQuery.ts b/src/components/hooks/queries/useWebsiteStatsQuery.ts
index 69bae09f..48484a07 100644
--- a/src/components/hooks/queries/useWebsiteStatsQuery.ts
+++ b/src/components/hooks/queries/useWebsiteStatsQuery.ts
@@ -1,6 +1,5 @@
import type { UseQueryOptions } from '@tanstack/react-query';
import { useDateParameters } from '@/components/hooks/useDateParameters';
-import { useDateRange } from '@/components/hooks/useDateRange';
import { useApi } from '../useApi';
import { useFilterParameters } from '../useFilterParameters';
@@ -20,21 +19,16 @@ export interface WebsiteStatsData {
}
export function useWebsiteStatsQuery(
- websiteId: string,
+ { websiteId, compare }: { websiteId: string; compare?: string },
options?: UseQueryOptions,
) {
const { get, useQuery } = useApi();
- const { startAt, endAt, unit, timezone } = useDateParameters();
- const { compare } = useDateRange();
+ const { startAt, endAt } = useDateParameters();
const filters = useFilterParameters();
return useQuery({
- queryKey: [
- 'websites:stats',
- { websiteId, startAt, endAt, unit, timezone, compare, ...filters },
- ],
- queryFn: () =>
- get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, compare, ...filters }),
+ queryKey: ['websites:stats', { websiteId, compare, startAt, endAt, ...filters }],
+ queryFn: () => get(`/websites/${websiteId}/stats`, { compare, startAt, endAt, ...filters }),
enabled: !!websiteId,
...options,
});
diff --git a/src/components/hooks/queries/useWeeklyTrafficQuery.ts b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
index a76ebb3d..df729ffd 100644
--- a/src/components/hooks/queries/useWeeklyTrafficQuery.ts
+++ b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
@@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record {
return get(`/websites/${websiteId}/sessions/weekly`, {
startAt,
endAt,
- unit,
timezone,
...params,
...filters,
diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts
index 755f36ee..5090bd3d 100644
--- a/src/components/hooks/useDateRange.ts
+++ b/src/components/hooks/useDateRange.ts
@@ -7,13 +7,13 @@ import { getItem } from '@/lib/storage';
export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
const {
- query: { date = '', offset = 0, compare = 'prev' },
+ query: { date = '', unit = '', offset = 0, compare = 'prev' },
} = useNavigation();
const { locale } = useLocale();
-
const dateRange = useMemo(() => {
const dateRangeObject = parseDateRange(
date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
+ unit,
locale,
options.timezone,
);
@@ -21,12 +21,13 @@ export function useDateRange(options: { ignoreOffset?: boolean; timezone?: strin
return !options.ignoreOffset && offset
? getOffsetDateRange(dateRangeObject, +offset)
: dateRangeObject;
- }, [date, offset, options]);
+ }, [date, unit, offset, options]);
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
return {
date,
+ unit,
offset,
compare,
isAllTime: date.endsWith(`:all`),
diff --git a/src/components/input/UnitFilter.tsx b/src/components/input/UnitFilter.tsx
new file mode 100644
index 00000000..84a15f35
--- /dev/null
+++ b/src/components/input/UnitFilter.tsx
@@ -0,0 +1,71 @@
+import { ListItem, Row, Select } from '@umami/react-zen';
+import { useMessages, useNavigation } from '@/components/hooks';
+import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
+import { getItem } from '@/lib/storage';
+
+export function UnitFilter() {
+ const { formatMessage, labels } = useMessages();
+ const { router, query, updateParams } = useNavigation();
+
+ const DATE_RANGE_UNIT_CONFIG = {
+ '0week': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '7day': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '0month': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '30day': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'hour'],
+ },
+ '90day': {
+ defaultUnit: 'day',
+ availableUnits: ['day', 'month'],
+ },
+ '6month': {
+ defaultUnit: 'month',
+ availableUnits: ['month', 'day'],
+ },
+ };
+
+ const unitConfig =
+ DATE_RANGE_UNIT_CONFIG[query.date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE];
+
+ if (!unitConfig) {
+ return null;
+ }
+
+ const handleChange = (value: string) => {
+ router.push(updateParams({ unit: value }));
+ };
+
+ const options = unitConfig.availableUnits.map(unit => ({
+ id: unit,
+ label: formatMessage(labels[unit]),
+ }));
+
+ const selectedUnit = query.unit ?? unitConfig.defaultUnit;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx
index 18b4f13b..a76058ec 100644
--- a/src/components/input/WebsiteDateFilter.tsx
+++ b/src/components/input/WebsiteDateFilter.tsx
@@ -41,7 +41,7 @@ export function WebsiteDateFilter({
}),
);
} else {
- router.push(updateParams({ date, offset: undefined }));
+ router.push(updateParams({ date, offset: undefined, unit: undefined }));
}
};
diff --git a/src/components/messages.ts b/src/components/messages.ts
index 712495d8..3d7388cd 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -245,7 +245,10 @@ export const labels = defineMessages({
tag: { id: 'label.tag', defaultMessage: 'Tag' },
segment: { id: 'label.segment', defaultMessage: 'Segment' },
cohort: { id: 'label.cohort', defaultMessage: 'Cohort' },
+ minute: { id: 'label.minute', defaultMessage: 'Minute' },
+ hour: { id: 'label.hour', defaultMessage: 'Hour' },
day: { id: 'label.day', defaultMessage: 'Day' },
+ month: { id: 'label.month', defaultMessage: 'Month' },
date: { id: 'label.date', defaultMessage: 'Date' },
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
create: { id: 'label.create', defaultMessage: 'Create' },
diff --git a/src/lib/date.ts b/src/lib/date.ts
index 3c1fd1b7..a005bf8e 100644
--- a/src/lib/date.ts
+++ b/src/lib/date.ts
@@ -9,6 +9,7 @@ import {
differenceInCalendarMonths,
differenceInCalendarWeeks,
differenceInCalendarYears,
+ differenceInDays,
differenceInHours,
differenceInMinutes,
endOfDay,
@@ -136,7 +137,12 @@ export function parseDateValue(value: string) {
return { num: +num, unit };
}
-export function parseDateRange(value: string, locale = 'en-US', timezone?: string): DateRange {
+export function parseDateRange(
+ value: string,
+ unitValue?: string,
+ locale = 'en-US',
+ timezone?: string,
+): DateRange {
if (typeof value !== 'string') {
return null;
}
@@ -146,7 +152,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
const startDate = new Date(+startTime);
const endDate = new Date(+endTime);
- const unit = getMinimumUnit(startDate, endDate);
+ const unit = getMinimumUnit(startDate, endDate, true);
return {
startDate,
@@ -169,14 +175,14 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
endDate: endOfHour(now),
offset: 0,
num: num || 1,
- unit,
+ unit: unitValue || unit,
value,
};
case 'day':
return {
startDate: num ? subDays(startOfDay(now), num) : startOfDay(now),
endDate: endOfDay(now),
- unit: num ? 'day' : 'hour',
+ unit: unitValue ? unitValue : num ? 'day' : 'hour',
offset: 0,
num: num || 1,
value,
@@ -187,7 +193,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
? subWeeks(startOfWeek(now, { locale: dateLocale }), num)
: startOfWeek(now, { locale: dateLocale }),
endDate: endOfWeek(now, { locale: dateLocale }),
- unit: 'day',
+ unit: unitValue || 'day',
offset: 0,
num: num || 1,
value,
@@ -196,7 +202,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
return {
startDate: num ? subMonths(startOfMonth(now), num) : startOfMonth(now),
endDate: endOfMonth(now),
- unit: num ? 'month' : 'day',
+ unit: unitValue ? unitValue : num ? 'month' : 'day',
offset: 0,
num: num || 1,
value,
@@ -205,7 +211,7 @@ export function parseDateRange(value: string, locale = 'en-US', timezone?: strin
return {
startDate: num ? subYears(startOfYear(now), num) : startOfYear(now),
endDate: endOfYear(now),
- unit: 'month',
+ unit: unitValue || 'month',
offset: 0,
num: num || 1,
value,
@@ -273,10 +279,18 @@ export function getAllowedUnits(startDate: Date, endDate: Date) {
return index >= 0 ? units.splice(index) : [];
}
-export function getMinimumUnit(startDate: number | Date, endDate: number | Date) {
+export function getMinimumUnit(
+ startDate: number | Date,
+ endDate: number | Date,
+ isDateRange: boolean = false,
+) {
if (differenceInMinutes(endDate, startDate) <= 60) {
return 'minute';
- } else if (differenceInHours(endDate, startDate) <= 48) {
+ } else if (
+ isDateRange
+ ? differenceInHours(endDate, startDate) <= 48
+ : differenceInDays(endDate, startDate) <= 30
+ ) {
return 'hour';
} else if (differenceInCalendarMonths(endDate, startDate) <= 6) {
return 'day';