Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bd04d96
Update report percentages on dashboard and details view
sanne-san Dec 1, 2025
67ce345
Add percentages to Countries, Regions, and Cities reports
sanne-san Dec 1, 2025
9c8cfb6
Add percentages to Channels, Sources, and UTM reports
sanne-san Dec 1, 2025
e0b12c1
Add percentages to top pages, entry pages, and exit pages reports
sanne-san Dec 1, 2025
7cbc03c
Update tests to include percentages
sanne-san Dec 3, 2025
e892bea
Change dashboard copy from title case to sentence case
sanne-san Dec 3, 2025
82b530d
Update details modal style
sanne-san Dec 3, 2025
4b7991f
Make animations snappier
sanne-san Dec 4, 2025
9f49efc
Introduce max height to modal and make inner content scrollable
sanne-san Dec 4, 2025
5360b49
Improve modal mobile design
sanne-san Dec 8, 2025
7a70414
Added mobile tap behavior to external link in list report
sanne-san Dec 8, 2025
ffed82c
Show tooltips only when in comparison mode or when the number is abbr…
sanne-san Dec 8, 2025
12a3f9f
remove previously added showTooltip prop
sanne-san Dec 8, 2025
abcaec4
Show long format upon hovering detailed view metrics
sanne-san Dec 8, 2025
4a29193
Added mobile tapping behaviour to detailed view
sanne-san Dec 8, 2025
e82d7c7
Added percentages to all detailed views
sanne-san Dec 8, 2025
8f81b62
Add mobile swipe-to-close behavior for modal
sanne-san Dec 9, 2025
73a14a1
Adjust sensitivity of modal drag to close
sanne-san Dec 9, 2025
8628a99
Use hammerjs for swipe-to-close modal behaviour
sanne-san Dec 9, 2025
bddceaa
Prevent dragging if gesture starts inside table
sanne-san Dec 9, 2025
bef38e6
Show 2 decimal places for percentages < 0.1% across dashboard
sanne-san Dec 9, 2025
cf372a9
Adjust dark mode styles
sanne-san Dec 9, 2025
f061498
Add hover effect to external link icon
sanne-san Dec 9, 2025
8f3ca15
Update tests to expect two-decimal percentages
sanne-san Dec 10, 2025
78327b2
Undo hammer install and revert to old modal styling
sanne-san Dec 10, 2025
97199d5
Remove CR and % columns from goals and custom props reports on dashbo…
sanne-san Dec 10, 2025
954f3e1
Remove unused constants
sanne-san Dec 10, 2025
99fb33f
Undo conversion rate on hover behaviour
sanne-san Dec 11, 2025
53b4bdf
Show percentages permanently in custom props detailed view
sanne-san Dec 11, 2025
4265f76
Adjust width of conversion metrics column
sanne-san Dec 11, 2025
db70e69
Updated metric-value test
sanne-san Dec 11, 2025
3dc2abc
Update top-bar test
sanne-san Dec 11, 2025
4cc0a85
Added changelog entry
sanne-san Dec 11, 2025
367f931
Fix test expectations for percentages with imported data
sanne-san Dec 11, 2025
bd0c7f0
Add imported_visitors to tests to ensure correct total_visitors calcu…
sanne-san Dec 16, 2025
841ad38
Correct imported_visitors count in test
sanne-san Dec 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.

### Added

- A visitor percentage breakdown is now shown on all reports, both on the dashboard and in the detailed breakdown

### Removed

### Changed
Expand Down
13 changes: 5 additions & 8 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
--color-gray-950: var(--color-zinc-950);

/* Custom gray shades from config (override some zinc values) */
--color-gray-75: rgb(247 247 248);
--color-gray-150: rgb(236 236 238);
--color-gray-750: rgb(50 50 54);
--color-gray-825: rgb(35 35 38);
Expand Down Expand Up @@ -294,16 +295,12 @@ blockquote {
display: inline;
}

.table-striped tbody tr:nth-child(odd) {
background-color: var(--color-gray-100);
.table-striped tbody tr:nth-child(odd) td {
background-color: var(--color-gray-75);
}

.dark .table-striped tbody tr:nth-child(odd) {
background-color: var(--color-gray-800);
}

.dark .table-striped tbody tr:nth-child(even) {
background-color: var(--color-gray-900);
.dark .table-striped tbody tr:nth-child(odd) td {
background-color: var(--color-gray-850);
}

.fade-enter {
Expand Down
27 changes: 0 additions & 27 deletions assets/css/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,6 @@
overflow: auto;
}

.modal__container {
background-color: #fff;
padding: 1rem 2rem;
border-radius: 4px;
margin: 50px auto;
box-sizing: border-box;
min-height: 509px;
transition: height 200ms ease-in;
}

.modal__close {
position: fixed;
color: #b8c2cc;
font-size: 48px;
font-weight: bold;
top: 12px;
right: 24px;
}

.modal__close::before {
content: '\2715';
}

.modal__content {
margin-bottom: 2rem;
}

@keyframes mm-fade-in {
from {
opacity: 0;
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/components/search-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const SearchInput = ({
type="text"
placeholder={isFocused ? placeholderFocused : placeholderUnfocused}
className={classNames(
'dark:text-gray-100 block border-gray-300 dark:border-gray-750 rounded-md dark:bg-gray-750 w-48 dark:placeholder:text-gray-400 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500',
'text-sm dark:text-gray-100 block border-gray-300 dark:border-gray-750 rounded-md dark:bg-gray-750 max-w-64 w-full dark:placeholder:text-gray-400 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500',
className
)}
onChange={debouncedOnSearchInputChange}
Expand Down
13 changes: 8 additions & 5 deletions assets/js/dashboard/components/sort-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,27 @@ export const SortButton = ({
return (
<button
onClick={toggleSort}
className={classNames('group', 'hover:underline', 'relative')}
className={classNames(
'group',
'hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-100',
'relative'
)}
>
{children}
<span
title={next.hint}
className={classNames(
'absolute',
'rounded inline-block h-4 w-4',
'rounded inline-block size-4',
'ml-1',
{
[SortDirection.asc]: 'rotate-180',
[SortDirection.desc]: 'rotate-0'
}[sortDirection ?? next.direction],
!sortDirection && 'opacity-0',
!sortDirection && 'group-hover:opacity-100',
sortDirection &&
'group-hover:bg-gray-100 dark:group-hover:bg-gray-900',
'transition'
'group-hover:bg-gray-100 dark:group-hover:bg-gray-900',
'transition-all duration-100'
)}
>
Expand Down
59 changes: 49 additions & 10 deletions assets/js/dashboard/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type ColumnConfiguraton<T extends Record<string, unknown>> = {
/**
* Function used to transform the value found at item[key] for the cell. Superseded by renderItem if present. @example 1120 => "1.1k"
*/
renderValue?: (item: T) => ReactNode
renderValue?: (item: T, isRowHovered?: boolean) => ReactNode
/** Function used to create richer cells */
renderItem?: (item: T) => ReactNode
}
Expand All @@ -38,7 +38,7 @@ export const TableHeaderCell = ({
return (
<th
className={classNames(
'p-2 text-xs font-bold text-gray-500 dark:text-gray-400 tracking-wide',
'p-2 text-xs font-semibold text-gray-500 dark:text-gray-400',
className
)}
align={align}
Expand All @@ -58,7 +58,13 @@ export const TableCell = ({
align?: 'left' | 'right'
}) => {
return (
<td className={classNames('p-2 font-medium', className)} align={align}>
<td
className={classNames(
'p-2 font-medium first:rounded-s-sm last:rounded-e-sm',
className
)}
align={align}
>
{children}
</td>
)
Expand All @@ -68,15 +74,42 @@ export const ItemRow = <T extends Record<string, string | number | ReactNode>>({
rowIndex,
pageIndex,
item,
columns
columns,
tappedRowName,
onRowTap
}: {
rowIndex: number
pageIndex?: number
item: T
columns: ColumnConfiguraton<T>[]
tappedRowName?: string | null
onRowTap?: (rowName: string | null) => void
}) => {
const [isHovered, setIsHovered] = React.useState(false)

const rowName = (item as unknown as { name: string }).name
const isTapped = tappedRowName === rowName
const isRowActive = isHovered || isTapped

const handleRowClick = (e: React.MouseEvent) => {
if (window.innerWidth < 768 && !(e.target as HTMLElement).closest('a')) {
if (onRowTap) {
if (isTapped) {
onRowTap(null)
} else {
onRowTap(rowName)
}
}
}
}

return (
<tr className="text-sm dark:text-gray-200">
<tr
className="group text-sm dark:text-gray-200 md:cursor-default cursor-pointer"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleRowClick}
>
{columns.map(({ key, width, align, renderValue, renderItem }) => (
<TableCell
key={`${(pageIndex ?? null) === null ? '' : `page_${pageIndex}_`}row_${rowIndex}_${String(key)}`}
Expand All @@ -86,7 +119,7 @@ export const ItemRow = <T extends Record<string, string | number | ReactNode>>({
{renderItem
? renderItem(item)
: renderValue
? renderValue(item)
? renderValue(item, isRowActive)
: (item[key] ?? '')}
</TableCell>
))}
Expand All @@ -101,6 +134,8 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
columns: ColumnConfiguraton<T>[]
data: T[] | { pages: T[][] }
}) => {
const [tappedRowName, setTappedRowName] = React.useState<string | null>(null)

const renderColumnLabel = (column: ColumnConfiguraton<T>) => {
if (column.metricWarning) {
return (
Expand All @@ -125,13 +160,13 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
}

return (
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr className="text-xs font-bold text-gray-500 dark:text-gray-400">
<table className="border-collapse table-striped table-fixed w-max min-w-full">
<thead className="sticky top-0 bg-white dark:bg-gray-900 z-10">
<tr className="text-xs font-semibold text-gray-500 dark:text-gray-400">
{columns.map((column) => (
<TableHeaderCell
key={`header_${String(column.key)}`}
className={classNames('p-2 tracking-wide', column.width)}
className={classNames('p-2', column.width)}
align={column.align}
>
{column.onSort ? (
Expand All @@ -156,6 +191,8 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
columns={columns}
rowIndex={rowIndex}
key={rowIndex}
tappedRowName={tappedRowName}
onRowTap={setTappedRowName}
/>
))
: data.pages.map((page, pageIndex) =>
Expand All @@ -166,6 +203,8 @@ export const Table = <T extends Record<string, string | number | ReactNode>>({
rowIndex={rowIndex}
pageIndex={pageIndex}
key={`page_${pageIndex}_row_${rowIndex}`}
tappedRowName={tappedRowName}
onRowTap={setTappedRowName}
/>
))
)}
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/components/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const Items = ({
<SearchInput
searchRef={searchRef}
placeholderUnfocused="Press / to search"
className="ml-auto w-full py-1 text-sm"
className="ml-auto w-full py-1"
onSearch={handleSearchInput}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const SearchableSegmentsSection = ({
<SearchInput
searchRef={searchRef}
placeholderUnfocused="Press / to search"
className="ml-auto w-full py-1 text-sm"
className="ml-auto w-full py-1"
onSearch={handleSearchInput}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/nav-menu/top-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ test('user can open and close filters dropdown', async () => {
'Location',
'Screen size',
'Browser',
'Operating System',
'Operating system',
'Goal'
])
await userEvent.click(toggleFilters)
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/stats/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function Bar({
return (
<div className="w-full h-full relative" style={style}>
<div
className={`absolute top-0 left-0 h-full rounded-sm transition-colors duration-150 ${bg || ''}`}
className={`absolute top-0 left-0 h-full rounded-sm ${bg || ''}`}
style={{ width: `${width}%` }}
></div>
{children}
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/stats/behaviours/conversions.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function Conversions({ afterFetchData, onGoalFilterClick }) {
path: conversionsRoute.path,
search: (search) => search
}}
color="bg-red-50 group-hover:bg-red-100"
color="bg-red-50 group-hover/row:bg-red-100"
colMinWidth={90}
/>
)
Expand Down
1 change: 0 additions & 1 deletion assets/js/dashboard/stats/behaviours/goal-conversions.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ function SpecialPropBreakdown({ prop, afterFetchData }) {
search: (search) => search
}}
getExternalLinkUrl={getExternalLinkUrlFactory()}
maybeHideDetails={true}
color="bg-red-50"
colMinWidth={90}
/>
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/stats/behaviours/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export const PROPS = 'props'
export const FUNNELS = 'funnels'

export const sectionTitles = {
[CONVERSIONS]: 'Goal Conversions',
[PROPS]: 'Custom Properties',
[CONVERSIONS]: 'Goal conversions',
[PROPS]: 'Custom properties',
[FUNNELS]: 'Funnels'
}

Expand Down
3 changes: 1 addition & 2 deletions assets/js/dashboard/stats/behaviours/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ export default function Properties({ afterFetchData }) {
params: { propKey },
search: (search) => search
}}
maybeHideDetails={true}
color="bg-red-50 group-hover:bg-red-100"
color="bg-red-50 group-hover/row:bg-red-100"
colMinWidth={90}
/>
)
Expand Down
28 changes: 17 additions & 11 deletions assets/js/dashboard/stats/devices/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ function Browsers({ afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: { plot: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate(),
!hasConversionGoalFilter(query) && metrics.createPercentage()
!hasConversionGoalFilter(query) &&
metrics.createPercentage({ meta: { showOnHover: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate()
].filter((metric) => !!metric)
}

Expand Down Expand Up @@ -121,8 +122,9 @@ function BrowserVersions({ afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: { plot: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate(),
!hasConversionGoalFilter(query) && metrics.createPercentage()
!hasConversionGoalFilter(query) &&
metrics.createPercentage({ meta: { showOnHover: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate()
].filter((metric) => !!metric)
}

Expand Down Expand Up @@ -187,9 +189,11 @@ function OperatingSystems({ afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: { plot: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate(),
!hasConversionGoalFilter(query) &&
metrics.createPercentage({ meta: { hiddenonMobile: true } })
metrics.createPercentage({
meta: { showOnHover: true, hiddenOnMobile: true }
}),
hasConversionGoalFilter(query) && metrics.createConversionRate()
].filter((metric) => !!metric)
}

Expand Down Expand Up @@ -238,8 +242,9 @@ function OperatingSystemVersions({ afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: { plot: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate(),
!hasConversionGoalFilter(query) && metrics.createPercentage()
!hasConversionGoalFilter(query) &&
metrics.createPercentage({ meta: { showOnHover: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate()
].filter((metric) => !!metric)
}

Expand Down Expand Up @@ -281,8 +286,9 @@ function ScreenSizes({ afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: { plot: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate(),
!hasConversionGoalFilter(query) && metrics.createPercentage()
!hasConversionGoalFilter(query) &&
metrics.createPercentage({ meta: { showOnHover: true } }),
hasConversionGoalFilter(query) && metrics.createConversionRate()
].filter((metric) => !!metric)
}

Expand Down Expand Up @@ -432,7 +438,7 @@ export default function Devices() {
}

return (
<div>
<div className="group/report overflow-x-hidden">
<div className="flex justify-between w-full">
<div className="flex gap-x-1">
<h3 className="font-bold dark:text-gray-100">Devices</h3>
Expand Down
Loading
Loading