Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embed improvements #2148

Merged
merged 6 commits into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
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 = Object.keys(METRIC_MAPPING).includes(stat.name) && !(this.props.query.filters.goal && stat.name === 'Unique visitors')
ukutaht marked this conversation as resolved.
Show resolved Hide resolved

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 %>">
ukutaht marked this conversation as resolved.
Show resolved Hide resolved
<%= 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