mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 09:33:19 +03:00
Add Comparison Line and Improved Tooltip to Visitors Graph (#624)
* First pass * Title Section of Tooltip+Formatting * Cleanup, Formatting, Rest of Tooltip * Fixes bug in "month" calculate_plot Bug: where calculate_plot on "month" didn't correctly use the end date in the date range, and instead always used the current time - making requesting past chunks of 6 or 12 month data impossible * Cleanup + Comments * Cleanup+Percentage Formatting Changes * Fixes Realtime Tooltip * Prevent FE Crash * Minor format, code cleanup * Removes erroneous changes * Github UI is bad at indenting
This commit is contained in:
parent
5220f98476
commit
979d2526d5
@ -287,3 +287,13 @@ blockquote {
|
||||
iframe[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chartjs-tooltip {
|
||||
background-color: rgba(25, 30, 56);
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
padding: 10px 12px;
|
||||
pointer-events: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
@ -2,47 +2,72 @@ import React from 'react';
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import Chart from 'chart.js'
|
||||
import { eventName, navigateToQuery } from '../query'
|
||||
import numberFormatter, {durationFormatter} from '../number-formatter'
|
||||
import numberFormatter, { durationFormatter } from '../number-formatter'
|
||||
import * as api from '../api'
|
||||
import {ThemeContext} from '../theme-context'
|
||||
import { ThemeContext } from '../theme-context'
|
||||
|
||||
function buildDataSet(plot, present_index, ctx, label) {
|
||||
function 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 (present_index) {
|
||||
var dashedPart = plot.slice(present_index - 1);
|
||||
var dashedPlot = (new Array(plot.length - dashedPart.length)).concat(dashedPart)
|
||||
for(var i = present_index; i < plot.length; i++) {
|
||||
plot[i] = undefined
|
||||
}
|
||||
if (!isPrevious) {
|
||||
if (present_index) {
|
||||
var dashedPart = plot.slice(present_index - 1);
|
||||
var dashedPlot = (new Array(plot.length - dashedPart.length)).concat(dashedPart)
|
||||
const _plot = [...plot]
|
||||
for (var i = present_index; i < _plot.length; i++) {
|
||||
_plot[i] = undefined
|
||||
}
|
||||
|
||||
return [{
|
||||
return [{
|
||||
label: label,
|
||||
data: plot,
|
||||
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,
|
||||
},
|
||||
{
|
||||
label: label,
|
||||
data: dashedPlot,
|
||||
borderWidth: 3,
|
||||
borderDash: [5, 10],
|
||||
borderDash: [3, 3],
|
||||
borderColor: 'rgba(101,116,205)',
|
||||
pointBackgroundColor: 'rgba(101,116,205)',
|
||||
pointHoverBackgroundColor: 'rgba(71, 87, 193)',
|
||||
pointBorderColor: 'transparent',
|
||||
pointHoverRadius: 4,
|
||||
backgroundColor: gradient,
|
||||
}]
|
||||
}]
|
||||
} else {
|
||||
return [{
|
||||
label: label,
|
||||
data: plot,
|
||||
borderWidth: 3,
|
||||
borderColor: 'rgba(101,116,205)',
|
||||
pointHoverBackgroundColor: 'rgba(71, 87, 193)',
|
||||
pointBorderColor: 'transparent',
|
||||
pointHoverRadius: 4,
|
||||
backgroundColor: gradient,
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
return [{
|
||||
label: label,
|
||||
data: plot,
|
||||
borderWidth: 3,
|
||||
borderColor: 'rgba(101,116,205)',
|
||||
pointBackgroundColor: 'rgba(101,116,205)',
|
||||
backgroundColor: gradient,
|
||||
borderWidth: 2,
|
||||
// borderDash: [10, 1],
|
||||
borderColor: 'rgba(166,187,210,0.5)',
|
||||
pointHoverBackgroundColor: 'rgba(166,187,210,0.8)',
|
||||
pointBorderColor: 'transparent',
|
||||
pointHoverBorderColor: 'transparent',
|
||||
pointHoverRadius: 4,
|
||||
backgroundColor: prev_gradient,
|
||||
}]
|
||||
}
|
||||
}
|
||||
@ -63,7 +88,7 @@ const DAYS_ABBREV = [
|
||||
]
|
||||
|
||||
function dateFormatter(interval, longForm) {
|
||||
return function(isoDate) {
|
||||
return function (isoDate) {
|
||||
let date = new Date(isoDate)
|
||||
|
||||
if (interval === 'month') {
|
||||
@ -75,7 +100,7 @@ function dateFormatter(interval, longForm) {
|
||||
return day + ', ' + date_ + ' ' + month;
|
||||
} 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])
|
||||
date = new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5])
|
||||
var hours = date.getHours(); // Not sure why getUTCHours doesn't work here
|
||||
var ampm = hours >= 12 ? 'pm' : 'am';
|
||||
hours = hours % 12;
|
||||
@ -99,61 +124,147 @@ class LineGraph extends React.Component {
|
||||
}
|
||||
|
||||
regenerateChart() {
|
||||
const {graphData} = this.props
|
||||
const { graphData } = this.props
|
||||
this.ctx = document.getElementById("main-graph-canvas").getContext('2d');
|
||||
const label = this.props.query.filters.goal ? 'Converted visitors' : graphData.interval === 'minute' ? 'Pageviews' : 'Visitors'
|
||||
const dataSet = buildDataSet(graphData.plot, graphData.present_index, this.ctx, label)
|
||||
const prev_dataSet = buildDataSet(graphData.prev_plot, false, this.ctx, label, true)
|
||||
|
||||
return new Chart(this.ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: graphData.labels,
|
||||
datasets: dataSet
|
||||
datasets: [...dataSet, ...prev_dataSet]
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
legend: {display: false},
|
||||
legend: { display: false },
|
||||
responsive: true,
|
||||
elements: {line: {tension: 0}, point: {radius: 0}},
|
||||
elements: { line: { tension: 0.1 }, point: { radius: 0 } },
|
||||
onClick: this.onClick.bind(this),
|
||||
tooltips: {
|
||||
hover: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
position: 'average',
|
||||
intersect: false,
|
||||
xPadding: 10,
|
||||
yPadding: 10,
|
||||
titleFontSize: 18,
|
||||
footerFontSize: 14,
|
||||
bodyFontSize: 14,
|
||||
backgroundColor: 'rgba(25, 30, 56)',
|
||||
titleMarginBottom: 8,
|
||||
bodySpacing: 6,
|
||||
footerMarginTop: 8,
|
||||
xPadding: 16,
|
||||
yPadding: 12,
|
||||
multiKeyBackground: 'none',
|
||||
callbacks: {
|
||||
title: function(dataPoints) {
|
||||
const data = dataPoints[0]
|
||||
return dateFormatter(graphData.interval, true)(data.xLabel)
|
||||
},
|
||||
beforeBody: function() {
|
||||
this.drawnLabels = {}
|
||||
},
|
||||
label: function(item) {
|
||||
const dataset = this._data.datasets[item.datasetIndex]
|
||||
if (!this.drawnLabels[dataset.label]) {
|
||||
this.drawnLabels[dataset.label] = true
|
||||
const pluralizedLabel = item.yLabel === 1 ? dataset.label.slice(0, -1) : dataset.label
|
||||
return ` ${item.yLabel} ${pluralizedLabel}`
|
||||
}
|
||||
},
|
||||
footer: function(dataPoints) {
|
||||
if (graphData.interval === 'month') {
|
||||
return 'Click to view month'
|
||||
} else if (graphData.interval === 'date') {
|
||||
return 'Click to view day'
|
||||
}
|
||||
custom: function (tooltipModel) {
|
||||
// Canvas Offset from 0,0
|
||||
const offset = this._chart.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';
|
||||
document.body.appendChild(tooltipEl);
|
||||
}
|
||||
|
||||
if (tooltipEl && offset && window.innerWidth < 768) {
|
||||
tooltipEl.style.top = offset.y + offset.height + window.pageYOffset + 'px'
|
||||
tooltipEl.style.left = offset.x + window.pageXOffset + 'px'
|
||||
}
|
||||
|
||||
// Stop if no tooltip showing
|
||||
if (tooltipModel.opacity === 0) {
|
||||
tooltipEl.style.opacity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
function getBody(bodyItem) {
|
||||
return bodyItem.lines;
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// Don't render if we only have one dataset available (this means the point is in the future)
|
||||
if (bodyLines.length == 1) {
|
||||
tooltipEl.style.opacity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = tooltipModel.dataPoints[0]
|
||||
const prev_data = tooltipModel.dataPoints.slice(-1)[0]
|
||||
const label = graphData.labels[data.index]
|
||||
const prev_label = graphData.prev_labels[prev_data.index]
|
||||
const point = data.yLabel || 0
|
||||
const prev_point = prev_data.yLabel || 0
|
||||
const pct_change = point === prev_point ? 0 : prev_point === 0 ? 100 : parseFloat(((point - prev_point) / prev_point * 100).toFixed(1))
|
||||
|
||||
function renderLabel(isPrevious) {
|
||||
const formattedLabel = dateFormatter(graphData.interval, true)(label)
|
||||
const prev_formattedLabel = dateFormatter(graphData.interval, true)(prev_label)
|
||||
|
||||
if (graphData.interval === 'month') {
|
||||
return !isPrevious ? `${formattedLabel} ${(new Date(label)).getUTCFullYear()}` : `${prev_formattedLabel} ${(new Date(prev_label)).getUTCFullYear()}`
|
||||
}
|
||||
|
||||
if (graphData.interval === 'date') {
|
||||
return !isPrevious ? formattedLabel : prev_formattedLabel
|
||||
}
|
||||
|
||||
if (graphData.interval === 'hour') {
|
||||
return !isPrevious ? `${dateFormatter("date", true)(label)}, ${formattedLabel}` : `${dateFormatter("date", true)(prev_label)}, ${dateFormatter(graphData.interval, true)(prev_label)}`
|
||||
}
|
||||
|
||||
return !isPrevious ? formattedLabel : prev_formattedLabel
|
||||
}
|
||||
|
||||
function renderComparison(change) {
|
||||
const formattedComparison = numberFormatter(Math.abs(change))
|
||||
|
||||
if (change > 0) {
|
||||
return `<span class='text-green-500 font-bold'>${formattedComparison}%</span>`
|
||||
}
|
||||
if (change < 0) {
|
||||
return `<span class='text-red-400 font-bold'>${formattedComparison}%</span>`
|
||||
}
|
||||
if (change === 0) {
|
||||
return `<span class='font-bold'>0%</span>`
|
||||
}
|
||||
}
|
||||
|
||||
let innerHtml = `
|
||||
<div class='text-gray-100 flex flex-col'>
|
||||
<div class='flex justify-between items-center'>
|
||||
<span class='font-bold mr-4 text-lg'>${bodyLines[0][0].split(':')[0]}</span>
|
||||
${graphData.interval === 'minute' ? '' : renderComparison(pct_change)}
|
||||
</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>${renderLabel()}</span>
|
||||
</span>
|
||||
<span>${numberFormatter(point)}</span>
|
||||
</div>
|
||||
${graphData.interval === 'minute' ? '' : `
|
||||
<div class='flex flex-row justify-between items-center mt-1'>
|
||||
<span class='flex items-center mr-4'>
|
||||
<div class='w-3 h-3 mr-1 rounded-full' style='background-color: rgba(166,187,210,0.5)'></div>
|
||||
<span>${renderLabel(true)}</span>
|
||||
</span>
|
||||
<span>${numberFormatter(prev_point)}</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<span class='font-bold text-'>${graphData.interval === 'month' ? 'Click to view month' : graphData.interval === 'date' ? 'Click to view day' : ''}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
tooltipEl.innerHTML = innerHtml;
|
||||
}
|
||||
|
||||
tooltipEl.style.opacity = 1;
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
@ -186,20 +297,25 @@ class LineGraph extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
repositionTooltip(e) {
|
||||
const tooltipEl = document.getElementById('chartjs-tooltip');
|
||||
if (tooltipEl && window.innerWidth >= 768) {
|
||||
tooltipEl.style.top = e.clientY + window.pageYOffset + 'px'
|
||||
tooltipEl.style.left = e.clientX + window.pageXOffset + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.chart = this.regenerateChart();
|
||||
|
||||
// Having the tooltip follow the mouse is much more intuitive
|
||||
window.addEventListener('mousemove', this.repositionTooltip);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.graphData !== prevProps.graphData) {
|
||||
const label = this.props.query.filters.goal ? 'Converted visitors' : this.props.graphData.interval === 'minute' ? 'Pageviews' : 'Visitors'
|
||||
const newDataset = buildDataSet(this.props.graphData.plot, this.props.graphData.present_index, this.ctx, label)
|
||||
|
||||
for (let i = 0; i < newDataset[0].data.length; i++) {
|
||||
this.chart.data.datasets[0].data[i] = newDataset[0].data[i]
|
||||
}
|
||||
|
||||
this.chart.update()
|
||||
if (JSON.stringify(this.props.graphData) !== JSON.stringify(prevProps.graphData)) {
|
||||
this.chart = this.regenerateChart();
|
||||
this.chart.update();
|
||||
}
|
||||
|
||||
if (prevProps.darkTheme !== this.props.darkTheme) {
|
||||
@ -208,8 +324,17 @@ class LineGraph extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Ensure that the tooltip doesn't hang around when we are loading more data
|
||||
const tooltip = document.getElementById('chartjs-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.style.opacity = 0;
|
||||
}
|
||||
window.removeEventListener('mousemove', this.repositionTooltip)
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
const element = this.chart.getElementsAtEventForMode(e, 'index', {intersect: false})[0]
|
||||
const element = this.chart.getElementsAtEventForMode(e, 'index', { intersect: false })[0]
|
||||
const date = element._chart.config.data.labels[element._index]
|
||||
if (this.props.graphData.interval === 'month') {
|
||||
navigateToQuery(
|
||||
@ -237,19 +362,21 @@ class LineGraph extends React.Component {
|
||||
|
||||
if (comparison > 0) {
|
||||
const color = name === 'Bounce rate' ? 'text-red-400' : 'text-green-500'
|
||||
return <span className="text-xs dark:text-gray-100"><span className={color + ' font-bold'}>↑</span> {formattedComparison}%</span>
|
||||
} else if (comparison < 0) {
|
||||
return <span className='text-sm flex dark:text-gray-300'><div className={color + ' transform -rotate-90'}>➔</div>{formattedComparison}%</span>
|
||||
}
|
||||
if (comparison < 0) {
|
||||
const color = name === 'Bounce rate' ? 'text-green-500' : 'text-red-400'
|
||||
return <span className="text-xs dark:text-gray-100"><span className={color + ' font-bold'}>↓</span> {formattedComparison}%</span>
|
||||
} else if (comparison === 0) {
|
||||
return <span className="text-xs text-gray-700 dark:text-gray-300">〰 N/A</span>
|
||||
return <span className='text-sm flex dark:text-gray-300'><div className={color + ' transform rotate-90'}>➔</div>{formattedComparison}%</span>
|
||||
}
|
||||
if (comparison === 0) {
|
||||
return <span className='text-sm text-gray-700 dark:text-gray-300'>〰 0%</span>
|
||||
}
|
||||
}
|
||||
|
||||
renderTopStatNumber(stat) {
|
||||
if (stat.name === 'Visit duration') {
|
||||
return durationFormatter(stat.count)
|
||||
} else if (typeof(stat.count) == 'number') {
|
||||
} else if (typeof (stat.count) == 'number') {
|
||||
return numberFormatter(stat.count)
|
||||
} else {
|
||||
return stat.percentage + '%'
|
||||
@ -257,7 +384,7 @@ class LineGraph extends React.Component {
|
||||
}
|
||||
|
||||
renderTopStats() {
|
||||
const {graphData} = this.props
|
||||
const { graphData } = this.props
|
||||
const stats = this.props.graphData.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
|
||||
@ -266,7 +393,7 @@ class LineGraph extends React.Component {
|
||||
<div className={`px-8 w-1/2 my-4 lg:w-auto ${border}`} key={stat.name}>
|
||||
<div className="text-gray-500 dark:text-gray-400 text-xs font-bold tracking-wide uppercase">{stat.name}</div>
|
||||
<div className="my-1 flex justify-between items-center">
|
||||
<b className="text-2xl mr-4 dark:text-gray-100">{ this.renderTopStatNumber(stat) }</b>
|
||||
<b className="text-2xl mr-4 dark:text-gray-100">{this.renderTopStatNumber(stat)}</b>
|
||||
{this.renderComparison(stat.name, stat.change)}
|
||||
</div>
|
||||
</div>
|
||||
@ -274,7 +401,7 @@ class LineGraph extends React.Component {
|
||||
})
|
||||
|
||||
if (graphData.interval === 'minute') {
|
||||
stats.push(<div key="dot" className="block pulsating-circle" style={{left: '125px', top: '52px'}}></div>)
|
||||
stats.push(<div key="dot" className="block pulsating-circle" style={{ left: '125px', top: '52px' }}></div>)
|
||||
}
|
||||
|
||||
return stats
|
||||
@ -285,7 +412,7 @@ class LineGraph extends React.Component {
|
||||
|
||||
return (
|
||||
<a href={endpoint} download>
|
||||
<svg className="feather w-4 h-5 absolute text-gray-700 dark:text-gray-300" style={{right: '2rem', top: '-2rem'}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
<svg className="feather w-4 h-5 absolute text-gray-700 dark:text-gray-300 cursor-pointer" style={{ right: '2rem', top: '-2rem' }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@ -296,10 +423,10 @@ class LineGraph extends React.Component {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="flex flex-wrap">
|
||||
{ this.renderTopStats() }
|
||||
{this.renderTopStats()}
|
||||
</div>
|
||||
<div className="px-2 relative">
|
||||
{ this.downloadLink() }
|
||||
{this.downloadLink()}
|
||||
<canvas id="main-graph-canvas" className={'mt-4 ' + extraClass} width="1054" height="342"></canvas>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
@ -312,7 +439,7 @@ LineGraph = withRouter(LineGraph)
|
||||
export default class VisitorGraph extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {loading: true}
|
||||
this.state = { loading: true }
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -322,7 +449,7 @@ export default class VisitorGraph extends React.Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query) {
|
||||
this.setState({loading: true, graphData: null})
|
||||
this.setState({ loading: true, graphData: null })
|
||||
this.fetchGraphData()
|
||||
}
|
||||
}
|
||||
@ -330,7 +457,7 @@ export default class VisitorGraph extends React.Component {
|
||||
fetchGraphData() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/main-graph`, this.props.query)
|
||||
.then((res) => {
|
||||
this.setState({loading: false, graphData: res})
|
||||
this.setState({ loading: false, graphData: res })
|
||||
return res
|
||||
})
|
||||
}
|
||||
@ -340,7 +467,7 @@ export default class VisitorGraph extends React.Component {
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => (
|
||||
<LineGraph graphData={this.state.graphData} site={this.props.site} query={this.props.query} darkTheme={theme}/>
|
||||
<LineGraph graphData={this.state.graphData} site={this.props.site} query={this.props.query} darkTheme={theme} />
|
||||
)}
|
||||
</ThemeContext.Consumer>
|
||||
)
|
||||
@ -350,8 +477,8 @@ export default class VisitorGraph extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="w-full relative bg-white dark:bg-gray-825 shadow-xl rounded mt-6 main-graph">
|
||||
{ this.state.loading && <div className="loading pt-24 sm:pt-32 md:pt-48 mx-auto"><div></div></div> }
|
||||
{ this.renderInner() }
|
||||
{ this.state.loading && <div className="loading pt-24 sm:pt-32 md:pt-48 mx-auto"><div></div></div>}
|
||||
{ this.renderInner()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
def calculate_plot(site, %Query{step_type: "month"} = query) do
|
||||
steps =
|
||||
Enum.map((query.steps - 1)..0, fn shift ->
|
||||
Timex.now(site.timezone)
|
||||
Timex.to_datetime(query.date_range.last)
|
||||
|> Timex.beginning_of_month()
|
||||
|> Timex.shift(months: -shift)
|
||||
|> DateTime.to_date()
|
||||
|
@ -8,15 +8,20 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
def main_graph(conn, params) do
|
||||
site = conn.assigns[:site]
|
||||
query = Query.from(site.timezone, params)
|
||||
prev_query = Query.shift_back(query, site)
|
||||
|
||||
plot_task = Task.async(fn -> Stats.calculate_plot(site, query) end)
|
||||
prev_plot_task = Task.async(fn -> Stats.calculate_plot(site, prev_query) end)
|
||||
top_stats = fetch_top_stats(site, query)
|
||||
{plot, labels, present_index} = Task.await(plot_task)
|
||||
{prev_plot, prev_labels, _} = Task.await(prev_plot_task)
|
||||
|
||||
json(conn, %{
|
||||
plot: plot,
|
||||
labels: labels,
|
||||
present_index: present_index,
|
||||
prev_plot: prev_plot,
|
||||
prev_labels: prev_labels,
|
||||
top_stats: top_stats,
|
||||
interval: query.step_type
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user