mirror of
https://github.com/plausible/analytics.git
synced 2024-11-23 11:12:15 +03:00
Embed improvements (#2148)
* 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
This commit is contained in:
parent
f4a242ac71
commit
7683638b84
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
|
36
assets/js/dashboard/util/tooltip.js
Normal file
36
assets/js/dashboard/util/tooltip.js
Normal file
@ -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
assets/package-lock.json
generated
65
assets/package-lock.json
generated
@ -15,6 +15,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",
|
||||
@ -40,6 +41,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",
|
||||
@ -1716,6 +1718,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/aspect-ratio": {
|
||||
"version": "0.2.1",
|
||||
"license": "MIT",
|
||||
@ -7258,6 +7269,11 @@
|
||||
"react": "^16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||
},
|
||||
"node_modules/react-flatpickr": {
|
||||
"version": "3.10.5",
|
||||
"resolved": "https://registry.npmjs.org/react-flatpickr/-/react-flatpickr-3.10.5.tgz",
|
||||
@ -7279,6 +7295,20 @@
|
||||
"version": "16.11.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-popper": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
|
||||
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
|
||||
"dependencies": {
|
||||
"react-fast-compare": "^3.0.1",
|
||||
"warning": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.0.0",
|
||||
"react": "^16.8.0 || ^17 || ^18",
|
||||
"react-dom": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.2.0",
|
||||
"license": "MIT",
|
||||
@ -8851,6 +8881,14 @@
|
||||
"version": "0.2.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.2.0",
|
||||
"license": "MIT",
|
||||
@ -10208,6 +10246,11 @@
|
||||
"version": "1.0.0-next.15",
|
||||
"dev": true
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
|
||||
},
|
||||
"@tailwindcss/aspect-ratio": {
|
||||
"version": "0.2.1",
|
||||
"requires": {}
|
||||
@ -13919,6 +13962,11 @@
|
||||
"scheduler": "^0.19.1"
|
||||
}
|
||||
},
|
||||
"react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||
},
|
||||
"react-flatpickr": {
|
||||
"version": "3.10.5",
|
||||
"resolved": "https://registry.npmjs.org/react-flatpickr/-/react-flatpickr-3.10.5.tgz",
|
||||
@ -13935,6 +13983,15 @@
|
||||
"react-is": {
|
||||
"version": "16.11.0"
|
||||
},
|
||||
"react-popper": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
|
||||
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
|
||||
"requires": {
|
||||
"react-fast-compare": "^3.0.1",
|
||||
"warning": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.2.0",
|
||||
"requires": {
|
||||
@ -15043,6 +15100,14 @@
|
||||
"vlq": {
|
||||
"version": "0.2.3"
|
||||
},
|
||||
"warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "2.2.0",
|
||||
"requires": {
|
||||
|
@ -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",
|
||||
@ -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",
|
||||
|
@ -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") %>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user