Skip to content

Commit

Permalink
Embed improvements (#2148)
Browse files Browse the repository at this point in the history
* Replace current tooltip with Popper.js

* Merge tooltip and title for top stats

* Format bounce rate and visit duration numbers in tooltip

* Add 'width=manual' mode for embed

* Add changelog entry

* Use helper function canMetricBeGraphed
  • Loading branch information
ukutaht committed Sep 1, 2022
1 parent f4a242a commit 7683638
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file.
- Filter goals in realtime filter by clicking goal name
- The time format (12 hour or 24 hour) for graph timelines is now presented based on the browser's defined language
- Choice of metric for main-graph both in UI and API (visitors, pageviews, bounce_rate, visit_duration) plausible/analytics#1364
- New width=manual mode for embedded dashboards plausible/analytics#2148

### Fixed
- UI fix where multi-line text in pills would not be underlined properly on small screens.
Expand Down
23 changes: 23 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,26 @@ iframe[hidden] {
/* This class is used for styling embedded dashboards. Do not remove. */
/* stylelint-disable */
.date-option-group { }


.popper-tooltip {
background-color: rgba(25, 30, 56);
}

.tooltip-arrow,
.tooltip-arrow::before {
position: absolute;
width: 10px;
height: 10px;
background: inherit;
}

.tooltip-arrow {
visibility: hidden;
}

.tooltip-arrow::before {
visibility: visible;
content: '';
transform: rotate(45deg) translateY(1px);
}
103 changes: 64 additions & 39 deletions assets/js/dashboard/stats/graph/top-stats.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from "react";
import numberFormatter, {durationFormatter} from '../../util/number-formatter'
import { METRIC_MAPPING, METRIC_LABELS } from './visitor-graph'
import classNames from "classnames";
import { Tooltip } from '../../util/tooltip'
import numberFormatter, { durationFormatter } from '../../util/number-formatter'
import { METRIC_MAPPING } from './visitor-graph'

export default class TopStats extends React.Component {
renderComparison(name, comparison) {
renderComparison(name, comparison) {
const formattedComparison = numberFormatter(Math.abs(comparison))

if (comparison > 0) {
Expand All @@ -27,64 +29,87 @@ export default class TopStats extends React.Component {
}
}

topStatTooltip(stat) {
if (['visit duration', 'time on page', 'bounce rate', 'conversion rate'].includes(stat.name.toLowerCase())) {
return null
topStatNumberLong(stat) {
if (['visit duration', 'time on page'].includes(stat.name.toLowerCase())) {
return durationFormatter(stat.value)
} else if (['bounce rate', 'conversion rate'].includes(stat.name.toLowerCase())) {
return stat.value + '%'
} else {
let name = stat.name.toLowerCase()
name = stat.value === 1 ? name.slice(0, -1) : name
return stat.value.toLocaleString() + ' ' + name
return stat.value.toLocaleString()
}
}

topStatTooltip(stat) {
let statName = stat.name.toLowerCase()
statName = stat.value === 1 ? statName.slice(0, -1) : statName

return (
<div>
<div>{this.topStatNumberLong(stat)} {statName}</div>
{this.canMetricBeGraphed(stat) && <div className="font-normal text-xs">{this.titleFor(stat)}</div>}
</div>
)
}

titleFor(stat) {
if(this.props.metric === METRIC_MAPPING[stat.name]) {
return `Hide ${METRIC_LABELS[METRIC_MAPPING[stat.name]].toLowerCase()} from graph`
const isClickable = this.canMetricBeGraphed(stat)

if (isClickable && this.props.metric === METRIC_MAPPING[stat.name]) {
return "Click to hide"
} else if (isClickable) {
return "Click to show"
} else {
return `Show ${METRIC_LABELS[METRIC_MAPPING[stat.name]].toLowerCase()} on graph`
return null
}
}

canMetricBeGraphed(stat) {
const isTotalUniqueVisitors = this.props.query.filters.goal && stat.name === 'Unique visitors'
const isKnownMetric = Object.keys(METRIC_MAPPING).includes(stat.name)

return isKnownMetric && !isTotalUniqueVisitors
}

maybeUpdateMetric(stat) {
if (this.canMetricBeGraphed(stat)) {
this.props.updateMetric(METRIC_MAPPING[stat.name])
}
}

renderStat(stat) {
return (
<div className="flex items-center justify-between my-1 whitespace-nowrap">
<b className="mr-4 text-xl md:text-2xl dark:text-gray-100" tooltip={this.topStatTooltip(stat)}>{this.topStatNumberShort(stat)}</b>
<Tooltip info={this.topStatTooltip(stat)} className="flex items-center justify-between my-1 whitespace-nowrap">
<b className="mr-4 text-xl md:text-2xl dark:text-gray-100">{this.topStatNumberShort(stat)}</b>
{this.renderComparison(stat.name, stat.change)}
</div>
</Tooltip>
)
}

render() {
const { updateMetric, metric, topStatData, query } = this.props
const { metric, topStatData, query } = this.props

const stats = topStatData && topStatData.top_stats.map((stat, index) => {
let border = index > 0 ? 'lg:border-l border-gray-300' : ''
border = index % 2 === 0 ? border + ' border-r lg:border-r-0' : border
const isClickable = Object.keys(METRIC_MAPPING).includes(stat.name) && !(query.filters.goal && stat.name === 'Unique visitors')
const isSelected = metric === METRIC_MAPPING[stat.name]
const [statDisplayName, statExtraName] = stat.name.split(/(\(.+\))/g)

const className = classNames('px-4 md:px-6 w-1/2 my-4 lg:w-auto group select-none', {
'cursor-pointer': this.canMetricBeGraphed(stat),
'lg:border-l border-gray-300': index > 0,
'border-r lg:border-r-0': index % 2 === 0
})

return (
<React.Fragment key={stat.name}>
{ isClickable ?
(
<div className={`px-4 md:px-6 w-1/2 my-4 lg:w-auto group cursor-pointer select-none ${border}`} onClick={() => { updateMetric(METRIC_MAPPING[stat.name]) }} tabIndex={0} title={this.titleFor(stat)}>
<div
className={`text-xs font-bold tracking-wide text-gray-500 uppercase dark:text-gray-400 whitespace-nowrap flex w-content border-b ${isSelected ? 'text-indigo-700 dark:text-indigo-500 border-indigo-700 dark:border-indigo-500' : 'group-hover:text-indigo-700 dark:group-hover:text-indigo-500 border-transparent'}`}>
{statDisplayName}
{statExtraName && <span className="hidden sm:inline-block ml-1">{statExtraName}</span>}
</div>
{ this.renderStat(stat) }
</div>
) : (
<div className={`px-4 md:px-6 w-1/2 my-4 lg:w-auto ${border}`}>
<div className='text-xs font-bold tracking-wide text-gray-500 uppercase dark:text-gray-400 whitespace-nowrap flex'>
{stat.name}
</div>
{ this.renderStat(stat) }
</div>
)}
</React.Fragment>
<Tooltip key={stat.name} info={this.topStatTooltip(stat)} className={className} onClick={() => { this.maybeUpdateMetric(stat) }}>
<div
className={`text-xs font-bold tracking-wide text-gray-500 uppercase dark:text-gray-400 whitespace-nowrap flex w-content border-b ${isSelected ? 'text-indigo-700 dark:text-indigo-500 border-indigo-700 dark:border-indigo-500' : 'group-hover:text-indigo-700 dark:group-hover:text-indigo-500 border-transparent'}`}>
{statDisplayName}
{statExtraName && <span className="hidden sm:inline-block ml-1">{statExtraName}</span>}
</div>
<div className="flex items-center justify-between my-1 whitespace-nowrap">
<b className="mr-4 text-xl md:text-2xl dark:text-gray-100">{this.topStatNumberShort(stat)}</b>
{this.renderComparison(stat.name, stat.change)}
</div>
</Tooltip>
)
})

Expand Down
36 changes: 36 additions & 0 deletions assets/js/dashboard/util/tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useState } from "react";
import { usePopper } from 'react-popper';
import classNames from 'classnames'

export function Tooltip({ children, info, className, onClick }) {
const [visible, setVisible] = useState(false);
const [referenceElement, setReferenceElement] = useState(null);
const [popperElement, setPopperElement] = useState(null);
const [arrowElement, setArrowElement] = useState(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'top',
modifiers: [
{ name: 'arrow', options: { element: arrowElement } },
{
name: 'offset',
options: {
offset: [0, 4],
},
},
],
});

return (
<div className={classNames('relative', className)}>
<div ref={setReferenceElement} onMouseEnter={() => setVisible(true)} onMouseLeave={() => setVisible(false)} onClick={onClick}>
{children}

</div>
{info && visible && <div ref={setPopperElement} style={styles.popper} {...attributes.popper} className="z-50 p-2 rounded text-sm text-gray-100 font-bold popper-tooltip" role="tooltip">
{info}
<div ref={setArrowElement} style={styles.arrow} className="tooltip-arrow"></div>
</div>
}
</div>
)
}
65 changes: 65 additions & 0 deletions assets/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@heroicons/react": "^1.0.1",
"@juggle/resize-observer": "^3.3.1",
"@kunukn/react-collapse": "^2.2.9",
"@popperjs/core": "^2.11.6",
"@tailwindcss/aspect-ratio": "^0.2.1",
"@tailwindcss/forms": "^0.3.2",
"@tailwindcss/typography": "^0.4.1",
Expand All @@ -43,6 +44,7 @@
"react-dom": "^16.13.1",
"react-flatpickr": "3.10.5",
"react-flip-move": "^3.0.4",
"react-popper": "^2.3.0",
"react-router-dom": "^5.2.0",
"react-transition-group": "^4.4.2",
"tailwindcss": "^2.1.2",
Expand Down
2 changes: 1 addition & 1 deletion lib/plausible_web/templates/stats/stats.html.eex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="<%= if !@conn.assigns[:embedded], do: "container", else: "max-w-screen-lg mx-auto" %>" data-site-domain="<%= @site.domain %>">
<div class="<%= stats_container_class(@conn) %>" data-site-domain="<%= @site.domain %>">
<%= if @offer_email_report do %>
<div class="w-full px-4 text-sm font-bold text-center text-blue-900 bg-blue-200 rounded transition" style="top: 91px" role="alert">
<%= link("Click here to enable weekly email reports →", to: "/#{URI.encode_www_form(@site.domain)}/settings/email-reports", class: "py-2 block") %>
Expand Down
8 changes: 8 additions & 0 deletions lib/plausible_web/views/stats_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ defmodule PlausibleWeb.StatsView do
"""
end

def stats_container_class(conn) do
cond do
conn.assigns[:embedded] && conn.assigns[:width] == "manual" -> ""
conn.assigns[:embedded] -> "max-width-screen-lg mx-auto"
!conn.assigns[:embedded] -> "container"
end
end

defp bar_width(count, all) do
max =
Enum.max_by(all, fn
Expand Down

0 comments on commit 7683638

Please sign in to comment.