diff --git a/db/clickhouse/migrations/12_add_heatmap.sql b/db/clickhouse/migrations/12_add_heatmap.sql index de6439236..1c983ab0d 100644 --- a/db/clickhouse/migrations/12_add_heatmap.sql +++ b/db/clickhouse/migrations/12_add_heatmap.sql @@ -12,6 +12,7 @@ CREATE TABLE umami.heatmap_event y Nullable(Int32), viewport_w Nullable(Int32), viewport_h Nullable(Int32), + page_h Nullable(Int32), scroll_pct Nullable(UInt8), created_at DateTime('UTC') ) diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index f587825b8..0ffdbd9bf 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -359,6 +359,7 @@ CREATE TABLE umami.heatmap_event y Nullable(Int32), viewport_w Nullable(Int32), viewport_h Nullable(Int32), + page_h Nullable(Int32), scroll_pct Nullable(UInt8), created_at DateTime('UTC') ) diff --git a/prisma/migrations/21_add_heatmap_page_h/migration.sql b/prisma/migrations/21_add_heatmap_page_h/migration.sql new file mode 100644 index 000000000..e985e33bc --- /dev/null +++ b/prisma/migrations/21_add_heatmap_page_h/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "heatmap_event" ADD COLUMN "page_h" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9cc0d946d..ced346e6d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -413,6 +413,7 @@ model HeatmapEvent { y Int? @db.Integer viewportW Int? @map("viewport_w") @db.Integer viewportH Int? @map("viewport_h") @db.Integer + pageH Int? @map("page_h") @db.Integer scrollPct Int? @map("scroll_pct") @db.Integer createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 38a7460d3..a24a9e316 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -139,6 +139,9 @@ "greater-than-equals": "Greater than or equals", "grouped": "Grouped", "growth": "Growth", + "heatmap": "Heatmap", + "heatmap-description": "Visualize where visitors click and how far they scroll on your pages.", + "heatmaps": "Heatmaps", "hostname": "Hostname", "hour": "Hour", "includes": "Includes", diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css index edea449db..0895311fb 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css @@ -40,6 +40,34 @@ background: var(--interactive-bg-hover); } +.toggleButtonSelected, +.toggleButtonSelected:hover { + background: var(--surface-inverted); + color: var(--surface-base); + border-color: var(--surface-inverted); +} + +.scrollBand { + position: absolute; + left: 0; + right: 0; + pointer-events: auto; + display: flex; + align-items: flex-start; + justify-content: flex-end; + padding: 2px 8px; +} + +.scrollBandLabel { + font-size: 11px; + font-weight: 600; + color: rgba(0, 0, 0, 0.7); + background: rgba(255, 255, 255, 0.85); + border-radius: 4px; + padding: 1px 6px; + white-space: nowrap; +} + .canvas { position: relative; overflow: hidden; diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx index 5e71c29cf..bbf5e784e 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from 'react'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useResultQuery, useWebsite } from '@/components/hooks'; import { formatLongNumber } from '@/lib/format'; -import type { HeatmapPoint, HeatmapResult } from '@/queries/sql'; +import type { HeatmapMode, HeatmapPoint, HeatmapResult } from '@/queries/sql'; import styles from './Heatmap.module.css'; const RENDER_WIDTH = 1024; @@ -19,25 +19,38 @@ interface HeatmapProps { websiteId: string; urlPath: string; onUrlPathChange: (urlPath: string) => void; + mode: HeatmapMode; + onModeChange: (mode: HeatmapMode) => void; } -export function Heatmap({ websiteId, urlPath, onUrlPathChange }: HeatmapProps) { +export function Heatmap({ websiteId, urlPath, onUrlPathChange, mode, onModeChange }: HeatmapProps) { const website = useWebsite(); const { data, error, isLoading } = useResultQuery('heatmap', { websiteId, urlPath: urlPath || undefined, + mode, }); const pages = data?.pages ?? []; const points = data?.points ?? []; + const scroll = data?.scroll; return ( - + + {urlPath ? ( - + mode === 'scroll' ? ( + + ) : ( + + ) ) : ( )} @@ -47,14 +60,43 @@ export function Heatmap({ websiteId, urlPath, onUrlPathChange }: HeatmapProps) { ); } +function ModeToggle({ + mode, + onChange, +}: { + mode: HeatmapMode; + onChange: (mode: HeatmapMode) => void; +}) { + return ( + + + + + ); +} + function PageList({ pages, selected, onSelect, + mode, }: { pages: HeatmapResult['pages']; selected: string; onSelect: (urlPath: string) => void; + mode: HeatmapMode; }) { return ( @@ -69,7 +111,7 @@ function PageList({ > {p.urlPath} - {formatLongNumber(p.count)} + {formatLongNumber(mode === 'scroll' ? p.sessions : p.count)} ))} @@ -144,10 +186,7 @@ function HeatmapView({ )} -
+
{showPage && iframeSrc && (