analytics/assets/js/dashboard/stats/graph/graph-util.js
RobertJoonas 47e21121db
Realtime dashboard improvements (#2445)
* add a new realtime-update-timer module

* hook to the new 'tick' event in ListReport for auto-updates

This commit fixes the bug where all reports using the `ListReport` component did not
auto-update in realtime mode. Those reports are:

- Pages (Top / Entry / Exit)
- Locations (Countries / Regions / Cities)
- Devices (Screen Sizes / Browsers + versions / OS-s + versions)

* fetch data for ListReports only when scrolled into view

* refactor fetching data in ListReport

* refer to one source of truth for utm tags

* make the 'All' tab in Sources auto-update

* make all UTM tabs in Sources auto-update

* fetch UTM data only when scrolled into view

* auto-update Referrers with the new timer

* auto-update google search terms

* auto-update Conversions

* make countries map auto-update

* auto-update visitor-graph and top stats with new timer

* use new tick event for current visitors (in Historical)

* remove the old timer class

* update changelog

* Visual improvements to automatic realtime updates (#2532)

* minor consistency fix for text color in dark mode

* use FlipMove in goal conversions report

* use FlipMove in ListReports

* set main graph and top stats loading state correctly

* refactor isIntervalValid function

* enforce intervals are valid when set and stored

* remove duplicate data fetching on interval change

Fetching new data is handled by the `fetchGraphData` callback in `updateInterval`

* refactor updateMetric function

* make it clearer why 'metric' can be a faulty value

* extract 'query' and 'site' variables from 'this.props'

* reset interval state only when period is changed

The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces
all those function calls with 'fetchGraphData' which better describes the actual behavior.
We should only worry about rolling back the interval if 'query.period' has changed.

This commit also stops the graph from flickering when it is updated in realtime.

* update names of two variables

* remove unnecessary negation

* make collapsed graph state more explicit

* consider stored invalid intervals when graph mounts

* fix not showing loading spinner regression

* remove interval state from VisitorGraph (#2540)

* Realtime prop breakdown (#2535)

* disable load more in realtime mode

* extract doFetch function

* separate fetchPropBreakdown and fetchNextPage functions

* subscribe for auto-updates in realtime

* improve readability with function name changes
2023-01-02 17:42:57 +02:00

188 lines
5.9 KiB
JavaScript

import numberFormatter, {durationFormatter} from '../../util/number-formatter'
import dateFormatter from './date-formatter.js'
export const METRIC_MAPPING = {
'Unique visitors (last 30 min)': 'visitors',
'Pageviews (last 30 min)': 'pageviews',
'Unique visitors': 'visitors',
'Visit duration': 'visit_duration',
'Total pageviews': 'pageviews',
'Bounce rate': 'bounce_rate',
'Unique conversions': 'conversions',
}
export const METRIC_LABELS = {
'visitors': 'Visitors',
'pageviews': 'Pageviews',
'bounce_rate': 'Bounce Rate',
'visit_duration': 'Visit Duration',
'conversions': 'Converted Visitors',
}
export const METRIC_FORMATTER = {
'visitors': numberFormatter,
'pageviews': numberFormatter,
'bounce_rate': (number) => (`${number}%`),
'visit_duration': durationFormatter,
'conversions': numberFormatter,
}
export const GraphTooltip = (graphData, metric, query) => {
return (context) => {
const tooltipModel = context.tooltip;
const offset = document.getElementById("main-graph-canvas").getBoundingClientRect()
// Tooltip Element
let tooltipEl = document.getElementById('chartjs-tooltip');
// Create element on first render
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'chartjs-tooltip';
tooltipEl.style.display = 'none';
tooltipEl.style.opacity = 0;
document.body.appendChild(tooltipEl);
}
if (tooltipEl && offset && window.innerWidth < 768) {
tooltipEl.style.top = offset.y + offset.height + window.scrollY + 15 + 'px'
tooltipEl.style.left = offset.x + 'px'
tooltipEl.style.right = null;
tooltipEl.style.opacity = 1;
}
// Stop if no tooltip showing
if (tooltipModel.opacity === 0) {
tooltipEl.style.display = 'none';
return;
}
function getBody(bodyItem) {
return bodyItem.lines;
}
// 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 (query.period === 'realtime') {
return dateFormatter(graphData.interval, true, query.period)(label)
}
if (graphData.interval === 'hour' || graphData.interval == 'minute') {
const date = dateFormatter("date", true, query.period)(label)
return `${date}, ${formattedLabel}`
}
return formattedLabel
}
// Set Tooltip Body
if (tooltipModel.body) {
var bodyLines = tooltipModel.body.map(getBody);
// Remove duplicated line on overlap between dashed and normal
if (bodyLines.length == 3) {
bodyLines[1] = false
}
const data = tooltipModel.dataPoints[0]
const label = graphData.labels[data.dataIndex]
const point = data.raw || 0
let innerHtml = `
<div class='text-gray-100 flex flex-col'>
<div class='flex justify-between items-center'>
<span class='font-semibold mr-4 text-lg'>${METRIC_LABELS[metric]}</span>
</div>
<div class='flex flex-col'>
<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>${renderBucketLabel(label)}</span>
</span>
<span class='text-base font-bold'>${METRIC_FORMATTER[metric](point)}</span>
</div>
</div>
<span class='font-semibold italic'>${graphData.interval === 'month' ? 'Click to view month' : graphData.interval === 'date' ? 'Click to view day' : ''}</span>
</div>
`;
tooltipEl.innerHTML = innerHtml;
}
tooltipEl.style.display = null;
}
}
export const buildDataSet = (plot, present_index, ctx, label, isPrevious) => {
var gradient = ctx.createLinearGradient(0, 0, 0, 300);
var prev_gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(0, 'rgba(101,116,205, 0.2)');
gradient.addColorStop(1, 'rgba(101,116,205, 0)');
prev_gradient.addColorStop(0, 'rgba(101,116,205, 0.075)');
prev_gradient.addColorStop(1, 'rgba(101,116,205, 0)');
if (!isPrevious) {
if (present_index) {
var dashedPart = plot.slice(present_index - 1, present_index + 1);
var dashedPlot = (new Array(present_index - 1)).concat(dashedPart)
const _plot = [...plot]
for (var i = present_index; i < _plot.length; i++) {
_plot[i] = undefined
}
return [{
label,
data: _plot,
borderWidth: 3,
borderColor: 'rgba(101,116,205)',
pointBackgroundColor: 'rgba(101,116,205)',
pointHoverBackgroundColor: 'rgba(71, 87, 193)',
pointBorderColor: 'transparent',
pointHoverRadius: 4,
backgroundColor: gradient,
fill: true,
},
{
label,
data: dashedPlot,
borderWidth: 3,
borderDash: [3, 3],
borderColor: 'rgba(101,116,205)',
pointHoverBackgroundColor: 'rgba(71, 87, 193)',
pointBorderColor: 'transparent',
pointHoverRadius: 4,
backgroundColor: gradient,
fill: true,
}]
} else {
return [{
label,
data: plot,
borderWidth: 3,
borderColor: 'rgba(101,116,205)',
pointHoverBackgroundColor: 'rgba(71, 87, 193)',
pointBorderColor: 'transparent',
pointHoverRadius: 4,
backgroundColor: gradient,
fill: true,
}]
}
} else {
return [{
label,
data: plot,
borderWidth: 2,
borderColor: 'rgba(166,187,210,0.5)',
pointHoverBackgroundColor: 'rgba(166,187,210,0.8)',
pointBorderColor: 'transparent',
pointHoverBorderColor: 'transparent',
pointHoverRadius: 4,
backgroundColor: prev_gradient,
fill: true,
}]
}
}