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

Adds ability to select graph detail (interval) #1574

Merged
merged 101 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
9422f36
First pass bringing in previous graph improvements, and comparsion co…
Vigasaurus May 26, 2021
a340a21
Completed FE implementation of stat selection, all of comparison viewing
Vigasaurus Jun 12, 2021
a51b8bf
Swaps issue template to new issue form syntax
Vigasaurus Jul 16, 2021
cedbb1a
Indentation update
Vigasaurus Jul 16, 2021
64d17fb
Indentation update?
Vigasaurus Jul 16, 2021
0277828
More indentation
Vigasaurus Jul 16, 2021
ebb7f47
Intendation is hard
Vigasaurus Jul 16, 2021
c55b419
Finalized indentation?
Vigasaurus Jul 16, 2021
083e29c
Github indentation
Vigasaurus Jul 16, 2021
80174ed
Missing fields
Vigasaurus Jul 16, 2021
871da79
Formatting changes
Vigasaurus Jul 16, 2021
fdcba2c
Checkbox changes
Vigasaurus Jul 16, 2021
14e92f1
Merge branch 'master' of github.com:Vigasaurus/plausible-analytics
Vigasaurus Oct 1, 2021
be8b575
Merge branch 'master' into graph-improvements
Vigasaurus Oct 1, 2021
c206270
Uses new timeseries API, various UI improvements, descopes conversion…
Vigasaurus Oct 1, 2021
5b49c52
Fixes Mobile UI Issues
Vigasaurus Oct 1, 2021
8028cc9
Improves point detection and display on hover
Vigasaurus Oct 1, 2021
9063275
Fixes & adds tests for updated main-graph API route
Vigasaurus Oct 2, 2021
1398500
Changelog
Vigasaurus Oct 2, 2021
356d0b7
Changes to better metric option declaration & minor UI/default fixes
Vigasaurus Oct 2, 2021
2575756
Fixes top stat tooltips showing unformatted numbers for special (non-…
Vigasaurus Oct 2, 2021
b33cb40
Formatting
Vigasaurus Oct 2, 2021
aa3e7d3
Merge branch 'master' into graph-improvements
Vigasaurus Oct 11, 2021
ebf6f40
Fixes regression with dashed portion not stopping at present_index
Vigasaurus Oct 11, 2021
6e44720
Removes comparison + lint
Vigasaurus Oct 13, 2021
702d9a7
Improves top stat active style
Vigasaurus Oct 13, 2021
4669125
Removes comparison tests
Vigasaurus Oct 13, 2021
5aedc48
Merge branch 'master' into graph-improvements
Vigasaurus Oct 15, 2021
dd91500
Merge branch 'master' into graph-improvements
ukutaht Nov 26, 2021
e46567f
Merge branch 'master' into graph-improvements
Vigasaurus Dec 18, 2021
9fe4df4
Splits out tooltip and top stats
Vigasaurus Dec 21, 2021
a56d53d
Adds/moves tests for top stats
Vigasaurus Dec 30, 2021
a99744d
Formatting
Vigasaurus Dec 30, 2021
ce21baa
First pass on rebasing interval selection
Vigasaurus Dec 31, 2021
3038d68
Various interval selection improvements
Vigasaurus Dec 31, 2021
8070e74
Fixes + adds tests
Vigasaurus Dec 31, 2021
530594a
Formatting
Vigasaurus Dec 31, 2021
8963dbd
Updates metric LS key, removes console log
Vigasaurus Dec 31, 2021
e4dbee8
Merge branch 'master' into graph-interval-2
Vigasaurus Jan 20, 2022
e65782b
Adds missing end statement
Vigasaurus Jan 20, 2022
6723562
Merge branch 'master' into graph-improvements
Vigasaurus Mar 25, 2022
59e3817
Various fixes + cleanup
Vigasaurus Mar 25, 2022
ac36e49
Merge branch 'graph-improvement' into graph-interval-2
Vigasaurus Mar 25, 2022
ad0b804
Merge branch 'master' into graph-improvements
Vigasaurus Apr 1, 2022
cc63483
Makes tooltip position & style more consistent
Vigasaurus Apr 1, 2022
5e736c9
Fixes test (returns import status on both main graph & top stats)
Vigasaurus Apr 1, 2022
06ad677
Fixes interaction with month dateFormatter
Vigasaurus Apr 1, 2022
4e9d0af
Merge branch 'graph-improvements' into graph-interval-2
Vigasaurus Apr 1, 2022
293d170
Formatting/Lint
Vigasaurus Apr 1, 2022
ad1ba8d
Adds missing interval mappings
Vigasaurus Apr 1, 2022
267c144
Fixes edge case tooltip behavior
Vigasaurus Apr 12, 2022
ab81001
Minor UI improvements
Vigasaurus Apr 12, 2022
4861b43
Make the entire top stat clickable
ukutaht Apr 12, 2022
414a5b8
Merge branch 'master' into graph-improvements
Vigasaurus Apr 12, 2022
d44b2ee
Merge branch 'graph-improvements' into graph-interval-2
Vigasaurus Apr 12, 2022
3df8c34
Minor UI improvements
Vigasaurus Apr 13, 2022
59377d6
Merge branch 'graph-improvements' into graph-interval-2
Vigasaurus Apr 13, 2022
cd10855
Improves interval icon display behavior
Vigasaurus Apr 13, 2022
d28b973
Fixes another tooltip visibility edge case + cleans up boolean algebra
Vigasaurus Apr 13, 2022
ce74b80
Merge branch 'graph-improvements' into graph-interval-2
Vigasaurus Apr 13, 2022
0a5d9f9
Merge branch 'master' into graph-interval-2
Vigasaurus Apr 19, 2022
3d33d9c
Fixes issues with recent master merge + cleanup
Vigasaurus Apr 19, 2022
4ca5a5e
Reorganizes FE imports
Vigasaurus Apr 19, 2022
50b80c0
Adds support for imported data + imported handling of week interval
Vigasaurus Apr 19, 2022
08dc676
Various UX loading improvements + minor code cleanup
Vigasaurus Apr 19, 2022
052598c
Removes outdated references to query-based interval
Vigasaurus Apr 19, 2022
93fb841
Formatting
Vigasaurus Apr 19, 2022
ea09256
Removes extraneous/erroneous merge changes
Vigasaurus Apr 20, 2022
cc9bb27
Merge branch 'master' into graph-interval-2
Vigasaurus Oct 12, 2022
f2322c5
Formatting
Vigasaurus Oct 12, 2022
30cf676
Re-adds Tooltip import
Vigasaurus Oct 12, 2022
92c873f
Remove commented out code and lint
vinibrsl Oct 26, 2022
9edbb83
Merge branch 'master' into graph-interval-2
vinibrsl Oct 26, 2022
73ec5ca
Lint changed files
vinibrsl Oct 26, 2022
7e99210
Add interval to params only when needed
vinibrsl Oct 26, 2022
2bd5169
Fixes undefined current
Vigasaurus Oct 31, 2022
572574f
Adds 24 hour support to minutes interval
Vigasaurus Nov 2, 2022
84245f4
Merge remote-tracking branch 'origin/master' into graph-interval-2
vinibrsl Nov 15, 2022
904f3c3
Merge remote-tracking branch 'origin/master' into graph-interval-2
vinibrsl Nov 15, 2022
34da83d
fixup! Adds 24 hour support to minutes interval
vinibrsl Nov 16, 2022
8a4c013
Replace interval icon with "Graph detail" button
vinibrsl Nov 16, 2022
5b59973
Put intervals under a feature flag
vinibrsl Nov 16, 2022
00ae4f7
Update CHANGELOG.md
vinibrsl Nov 16, 2022
64749b6
Revert "Update CHANGELOG.md"
vinibrsl Nov 17, 2022
5acfee6
Simplify renderLabel function
vinibrsl Nov 17, 2022
6076650
Pass valid intervals using data attributes
vinibrsl Nov 17, 2022
ad5ea63
Reimplement IntervalPicker with Headless UI
vinibrsl Nov 18, 2022
f0575bf
Clarify labels with date and hour
vinibrsl Nov 18, 2022
7793b3c
Create functions for interacting with local storage
vinibrsl Nov 18, 2022
fa18c52
Remove unnecessary checks from validateInterval function
vinibrsl Nov 18, 2022
ec65db5
Split big condition into multiple variables
vinibrsl Nov 18, 2022
9dbc310
Use classNames function instead of concat
vinibrsl Nov 18, 2022
80f3fcc
Lint Elixir code
vinibrsl Nov 18, 2022
bcc66ac
Refactor dateFormat function into a factory
vinibrsl Nov 18, 2022
80d9fc9
Remove unnecessary imports
vinibrsl Nov 18, 2022
5cc5e60
Make classNames function call more idiomatic
vinibrsl Nov 18, 2022
76e54d6
Enumerate loading state
vinibrsl Nov 19, 2022
df2486c
Convert IntervalPicker to function component
vinibrsl Nov 19, 2022
d1a92f7
Improve intervals user experience
vinibrsl Nov 19, 2022
c37d463
Remove unused import
vinibrsl Nov 19, 2022
e7f8d58
Rename allowed_for_period to valid_by_period
vinibrsl Nov 21, 2022
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
3 changes: 2 additions & 1 deletion assets/js/dashboard/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ if (container) {
embedded: container.dataset.embedded,
background: container.dataset.background,
isDbip: container.dataset.isDbip === 'true',
flags: JSON.parse(container.dataset.flags)
flags: JSON.parse(container.dataset.flags),
validIntervalsByPeriod: JSON.parse(container.dataset.validIntervalsByPeriod)
}

const loggedIn = container.dataset.loggedIn === 'true'
Expand Down
127 changes: 127 additions & 0 deletions assets/js/dashboard/stats/graph/date-formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {parseUTCDate, formatMonthYYYY, formatDay, formatDayShort} from '../../util/date'

const browserDateFormat = Intl.DateTimeFormat(navigator.language, { hour: 'numeric' })

const is12HourClock = function() {
return browserDateFormat.resolvedOptions().hour12
}

const parseISODate = function(isoDate) {
const date = parseUTCDate(isoDate)
const minutes = date.getMinutes();
return { date, minutes }
}

const formatHours = function(isoDate) {
const monthIndex = 1
const dateParts = isoDate.split(/[^0-9]/);
dateParts[monthIndex] = dateParts[monthIndex] - 1

const localDate = new Date(...dateParts)
return browserDateFormat.format(localDate)
}

const monthIntervalFormatter = {
long(isoDate, options) {
const formatted = this.short(isoDate, options)
return options.isBucketPartial ? `Partial of ${formatted}` : formatted
},
short(isoDate, _options) {
const { date } = parseISODate(isoDate)
return formatMonthYYYY(date)
}
}

const weekIntervalFormatter = {
long(isoDate, options) {
const formatted = this.short(isoDate, options)
return options.isBucketPartial ? `Partial week of ${formatted}` : `Week of ${formatted}`
},
short(isoDate, _options) {
const { date } = parseISODate(isoDate)
return formatDayShort(date)
}
}

const dateIntervalFormatter = {
long(isoDate, _options) {
const { date } = parseISODate(isoDate)
return formatDay(date)
},
short(isoDate, _options) {
const { date } = parseISODate(isoDate)
return formatDayShort(date)
}
}

const hourIntervalFormatter = {
long(isoDate, options) {
return this.short(isoDate, options)
},
short(isoDate, _options) {
const formatted = formatHours(isoDate)

if (is12HourClock()) {
return formatted.replace(' ', '').toLowerCase()
} else {
return formatted.replace(/[^0-9]/g, '').concat(":00")
}
}
}

const minuteIntervalFormatter = {
long(isoDate, options) {
if (options.period == 'realtime') {
const minutesAgo = Math.abs(isoDate)
return minutesAgo === 1 ? '1 minute ago' : minutesAgo + ' minutes ago'
} else {
return this.short(isoDate, options)
}
},
short(isoDate, options) {
if (options.period === 'realtime') return isoDate + 'm'

const { minutes } = parseISODate(isoDate)
const formatted = formatHours(isoDate)
if (is12HourClock()) {
return formatted.replace(' ', ':' + (minutes < 10 ? `0${minutes}` : minutes)).toLowerCase()
} else {
return formatted.replace(/[^0-9]/g, '').concat(":" + (minutes < 10 ? `0${minutes}` : minutes))
}
}
}

// Each interval has a different date and time format. This object maps each
// interval with two functions: `long` and `short`, that formats date and time
// accordingly.
const factory = {
month: monthIntervalFormatter,
week: weekIntervalFormatter,
date: dateIntervalFormatter,
hour: hourIntervalFormatter,
minute: minuteIntervalFormatter
}

/**
* Returns a function that formats a ISO 8601 timestamp based on the given
* arguments.
*
* The preferred date and time format in the dashboard depends on the selected
* interval and period. For example, in real-time view only the time is necessary,
* while other intervals require dates to be displayed.
*
* @param {string} interval - The interval of the query, e.g. `minute`, `hour`
* @param {boolean} longForm - Whether the formatted result should be in long or
* short form.
* @param {string} period - The period of the query, e.g. `12mo`, `day`
* @param {boolean} isPeriodFull - Indicates whether the interval has been cut
* off by the requested date range or not. If false, the returned formatted date
* indicates this cut off, e.g. `Partial week of November 8`.
*/
export default function dateFormatter(interval, longForm, period, isPeriodFull) {
const displayMode = longForm ? 'long' : 'short'
const options = { period: period, interval: interval, isBucketPartial: !isPeriodFull }
return function(isoDate, _index, _ticks) {
return factory[interval][displayMode](isoDate, options)
}
}
63 changes: 16 additions & 47 deletions assets/js/dashboard/stats/graph/graph-util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { parseUTCDate, formatMonthYYYY, formatDay } from '../../util/date'
import numberFormatter, {durationFormatter} from '../../util/number-formatter'
import dateFormatter from './date-formatter.js'

export const INTERVALS = ["month", "week", "date", "hour", "minute"]

export const METRIC_MAPPING = {
'Unique visitors (last 30 min)': 'visitors',
Expand Down Expand Up @@ -27,39 +29,7 @@ export const METRIC_FORMATTER = {
'conversions': numberFormatter,
}

export const dateFormatter = (interval, longForm) => {
return function(isoDate, _index, _ticks) {
let date = parseUTCDate(isoDate)

if (interval === 'month') {
return formatMonthYYYY(date);
} else if (interval === 'date') {
return formatDay(date);
} else if (interval === 'hour') {
const parts = isoDate.split(/[^0-9]/);
date = new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5])

const dateFormat = Intl.DateTimeFormat(navigator.language, { hour: 'numeric' })
const twelveHourClock = dateFormat.resolvedOptions().hour12
const formattedHours = dateFormat.format(date)

if (twelveHourClock) {
return formattedHours.replace(' ', '').toLowerCase()
} else {
return formattedHours.replace(/[^0-9]/g, '').concat(":00")
}
} else if (interval === 'minute') {
if (longForm) {
const minutesAgo = Math.abs(isoDate)
return minutesAgo === 1 ? '1 minute ago' : minutesAgo + ' minutes ago'
} else {
return isoDate + 'm'
}
}
}
}

export const GraphTooltip = (graphData, metric) => {
export const GraphTooltip = (graphData, metric, query) => {
return (context) => {
const tooltipModel = context.tooltip;
const offset = document.getElementById("main-graph-canvas").getBoundingClientRect()
Expand Down Expand Up @@ -93,23 +63,22 @@ export const GraphTooltip = (graphData, metric) => {
return bodyItem.lines;
}

function renderLabel(label, prev_label) {
const formattedLabel = dateFormatter(graphData.interval, true)(label)
const prev_formattedLabel = prev_label && dateFormatter(graphData.interval, true)(prev_label)

if (graphData.interval === 'month') {
return !prev_label ? formattedLabel : prev_formattedLabel
}
// Returns a string describing the bucket. Used when hovering the graph to
// show time buckets.
function renderBucketLabel(label) {
const isPeriodFull = graphData.full_intervals?.[label]
const formattedLabel = dateFormatter(graphData.interval, true, query.period, isPeriodFull)(label)

if (graphData.interval === 'date') {
return !prev_label ? formattedLabel : prev_formattedLabel
if (query.period === 'realtime') {
return dateFormatter(graphData.interval, true, query.period)(label)
}

if (graphData.interval === 'hour') {
return !prev_label ? `${dateFormatter("date", true)(label)}, ${formattedLabel}` : `${dateFormatter("date", true)(prev_label)}, ${dateFormatter(graphData.interval, true)(prev_label)}`
if (graphData.interval === 'hour' || graphData.interval == 'minute') {
const date = dateFormatter("date", true, query.period)(label)
return `${date}, ${formattedLabel}`
}

return !prev_label ? formattedLabel : prev_formattedLabel
return formattedLabel
}

// Set Tooltip Body
Expand All @@ -134,7 +103,7 @@ export const GraphTooltip = (graphData, metric) => {
<div class='flex flex-row justify-between items-center'>
<span class='flex items-center mr-4'>
<div class='w-3 h-3 mr-1 rounded-full' style='background-color: rgba(101,116,205)'></div>
<span>${renderLabel(label)}</span>
<span>${renderBucketLabel(label)}</span>
</span>
<span class='text-base font-bold'>${METRIC_FORMATTER[metric](point)}</span>
</div>
Expand Down
67 changes: 67 additions & 0 deletions assets/js/dashboard/stats/graph/interval-picker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Menu, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import React, { Fragment } from 'react';
import classNames from 'classnames'
import * as storage from '../../util/storage'

export const INTERVAL_LABELS = {
'minute': 'Minutes',
'hour': 'Hours',
'date': 'Days',
'week': 'Weeks',
'month': 'Months'
}

export const getStoredInterval = function(period, domain) {
return storage.getItem(`interval__${period}__${domain}`)
}

export const storeInterval = function(period, domain, interval) {
storage.setItem(`interval__${period}__${domain}`, interval)
}

function DropdownItem({ option, currentInterval, updateInterval }) {
return (
<Menu.Item onClick={() => updateInterval(option)} key={option} disabled={option == currentInterval}>
{({ active }) => (
<span className={classNames({
'bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-200 cursor-pointer': active,
'text-gray-700 dark:text-gray-200': !active,
'font-bold cursor-none select-none': option == currentInterval,
}, 'block px-4 py-2 text-sm')}>
{ INTERVAL_LABELS[option] }
</span>
)}
</Menu.Item>
)
}

export function IntervalPicker({ graphData, query, site, updateInterval }) {
if (query.period == 'realtime') return null

const currentInterval = graphData?.interval
const options = site.validIntervalsByPeriod[query.period]

return (
<Menu as="div" className="relative inline-block pl-2">
<Menu.Button className="text-sm inline-flex focus:outline-none text-gray-700 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-600 items-center">
{ INTERVAL_LABELS[currentInterval] }
<ChevronDownIcon className="w-4 h-4" aria-hidden="true" />
</Menu.Button>

<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
{ options.map((option) => DropdownItem({ option, currentInterval, updateInterval })) }
</Menu.Items>
</Transition>
</Menu>
)
}
6 changes: 3 additions & 3 deletions assets/js/dashboard/stats/graph/top-stats.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import classNames from "classnames";
import { Tooltip } from '../../util/tooltip'
import classNames from "classnames";
import numberFormatter, { durationFormatter } from '../../util/number-formatter'
import { METRIC_MAPPING } from './graph-util'

Expand Down Expand Up @@ -113,10 +113,10 @@ export default class TopStats extends React.Component {
)
})

if (query && query.period === 'realtime') {
if (stats && query && query.period === 'realtime') {
stats.push(<div key="dot" className="block pulsating-circle" style={{ left: '125px', top: '52px' }}></div>)
}

return stats
return stats || null;
}
}
Loading