2022-04-13 10:38:47 +03:00
|
|
|
import React from 'react';
|
|
|
|
import { withRouter, Link } from 'react-router-dom'
|
|
|
|
import Chart from 'chart.js/auto';
|
|
|
|
import { navigateToQuery } from '../../query'
|
|
|
|
import * as api from '../../api'
|
|
|
|
import * as storage from '../../util/storage'
|
|
|
|
import LazyLoader from '../../components/lazy-loader'
|
2023-02-16 16:11:58 +03:00
|
|
|
import GraphTooltip from './graph-tooltip'
|
|
|
|
import { buildDataSet, METRIC_MAPPING, METRIC_LABELS, METRIC_FORMATTER, LoadingState } from './graph-util'
|
2022-11-22 15:50:58 +03:00
|
|
|
import dateFormatter from './date-formatter';
|
2022-04-13 10:38:47 +03:00
|
|
|
import TopStats from './top-stats';
|
2022-11-22 15:50:58 +03:00
|
|
|
import { IntervalPicker, getStoredInterval, storeInterval } from './interval-picker';
|
|
|
|
import FadeIn from '../../fade-in';
|
2022-04-13 10:38:47 +03:00
|
|
|
import * as url from '../../util/url'
|
2023-02-14 15:59:25 +03:00
|
|
|
import classNames from 'classnames';
|
2023-05-03 11:46:13 +03:00
|
|
|
import { parseNaiveDate, isBefore } from '../../util/date'
|
2023-05-04 12:42:48 +03:00
|
|
|
import { isComparisonEnabled } from '../../comparison-input'
|
2022-11-22 15:50:58 +03:00
|
|
|
|
2023-02-07 16:00:49 +03:00
|
|
|
const calculateMaximumY = function(dataset) {
|
|
|
|
const yAxisValues = dataset
|
|
|
|
.flatMap((item) => item.data)
|
|
|
|
.map((item) => item || 0)
|
|
|
|
|
|
|
|
if (yAxisValues) {
|
|
|
|
return Math.max(...yAxisValues)
|
|
|
|
} else {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-13 10:38:47 +03:00
|
|
|
class LineGraph extends React.Component {
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2022-09-13 12:46:01 +03:00
|
|
|
this.boundary = React.createRef()
|
2022-04-13 10:38:47 +03:00
|
|
|
this.regenerateChart = this.regenerateChart.bind(this);
|
|
|
|
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
|
|
|
|
this.state = {
|
|
|
|
exported: false
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
regenerateChart() {
|
2022-11-22 15:50:58 +03:00
|
|
|
const { graphData, metric, query } = this.props
|
2022-04-13 10:38:47 +03:00
|
|
|
const graphEl = document.getElementById("main-graph-canvas")
|
|
|
|
this.ctx = graphEl.getContext('2d');
|
2023-02-07 16:00:49 +03:00
|
|
|
const dataSet = buildDataSet(graphData.plot, graphData.comparison_plot, graphData.present_index, this.ctx, METRIC_LABELS[metric])
|
2022-04-13 10:38:47 +03:00
|
|
|
|
|
|
|
return new Chart(this.ctx, {
|
|
|
|
type: 'line',
|
|
|
|
data: {
|
|
|
|
labels: graphData.labels,
|
|
|
|
datasets: dataSet
|
|
|
|
},
|
|
|
|
options: {
|
|
|
|
animation: false,
|
|
|
|
plugins: {
|
|
|
|
legend: { display: false },
|
|
|
|
tooltip: {
|
|
|
|
enabled: false,
|
|
|
|
mode: 'index',
|
|
|
|
intersect: false,
|
|
|
|
position: 'average',
|
2022-11-22 15:50:58 +03:00
|
|
|
external: GraphTooltip(graphData, metric, query)
|
2022-04-13 10:38:47 +03:00
|
|
|
},
|
|
|
|
},
|
|
|
|
responsive: true,
|
2023-02-14 15:59:25 +03:00
|
|
|
maintainAspectRatio: false,
|
2022-04-13 10:38:47 +03:00
|
|
|
onResize: this.updateWindowDimensions,
|
|
|
|
elements: { line: { tension: 0 }, point: { radius: 0 } },
|
|
|
|
onClick: this.onClick.bind(this),
|
2023-05-02 11:30:55 +03:00
|
|
|
scale: {
|
|
|
|
ticks: { precision: 0, maxTicksLimit: 8 }
|
|
|
|
},
|
2022-04-13 10:38:47 +03:00
|
|
|
scales: {
|
|
|
|
y: {
|
2023-02-07 16:00:49 +03:00
|
|
|
min: 0,
|
|
|
|
suggestedMax: calculateMaximumY(dataSet),
|
2022-04-13 10:38:47 +03:00
|
|
|
ticks: {
|
|
|
|
callback: METRIC_FORMATTER[metric],
|
|
|
|
color: this.props.darkTheme ? 'rgb(243, 244, 246)' : undefined
|
|
|
|
},
|
|
|
|
grid: {
|
|
|
|
zeroLineColor: 'transparent',
|
|
|
|
drawBorder: false,
|
|
|
|
}
|
|
|
|
},
|
2023-02-07 16:00:49 +03:00
|
|
|
yComparison: {
|
|
|
|
min: 0,
|
|
|
|
suggestedMax: calculateMaximumY(dataSet),
|
|
|
|
display: false,
|
|
|
|
grid: { display: false },
|
|
|
|
},
|
2022-04-13 10:38:47 +03:00
|
|
|
x: {
|
|
|
|
grid: { display: false },
|
|
|
|
ticks: {
|
2022-11-22 15:50:58 +03:00
|
|
|
callback: function (val, _index, _ticks) {
|
2023-03-22 15:31:44 +03:00
|
|
|
if (this.getLabelForValue(val) == "__blank__") return ""
|
|
|
|
|
|
|
|
const hasMultipleYears =
|
|
|
|
graphData.labels
|
|
|
|
.filter((date) => typeof date === 'string')
|
|
|
|
.map(date => date.split('-')[0])
|
|
|
|
.filter((value, index, list) => list.indexOf(value) === index)
|
|
|
|
.length > 1
|
2023-02-14 14:54:22 +03:00
|
|
|
|
2022-11-22 15:50:58 +03:00
|
|
|
if (graphData.interval === 'hour' && query.period !== 'day') {
|
2023-02-14 14:54:22 +03:00
|
|
|
const date = dateFormatter({
|
|
|
|
interval: "date",
|
|
|
|
longForm: false,
|
|
|
|
period: query.period,
|
|
|
|
shouldShowYear: hasMultipleYears,
|
|
|
|
})(this.getLabelForValue(val))
|
|
|
|
|
|
|
|
const hour = dateFormatter({
|
|
|
|
interval: graphData.interval,
|
|
|
|
longForm: false,
|
|
|
|
period: query.period,
|
|
|
|
shouldShowYear: hasMultipleYears,
|
|
|
|
})(this.getLabelForValue(val))
|
2022-11-22 15:50:58 +03:00
|
|
|
|
|
|
|
// Returns a combination of date and hour. This is because
|
|
|
|
// small intervals like hour may return multiple days
|
|
|
|
// depending on the query period.
|
|
|
|
return `${date}, ${hour}`
|
|
|
|
}
|
|
|
|
|
|
|
|
if (graphData.interval === 'minute' && query.period !== 'realtime') {
|
2023-02-14 14:54:22 +03:00
|
|
|
return dateFormatter({
|
|
|
|
interval: "hour", longForm: false, period: query.period,
|
|
|
|
})(this.getLabelForValue(val))
|
2022-11-22 15:50:58 +03:00
|
|
|
}
|
|
|
|
|
2023-02-14 14:54:22 +03:00
|
|
|
return dateFormatter({
|
|
|
|
interval: graphData.interval, longForm: false, period: query.period, shouldShowYear: hasMultipleYears,
|
|
|
|
})(this.getLabelForValue(val))
|
2022-11-22 15:50:58 +03:00
|
|
|
},
|
2022-04-13 10:38:47 +03:00
|
|
|
color: this.props.darkTheme ? 'rgb(243, 244, 246)' : undefined
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
interaction: {
|
|
|
|
mode: 'index',
|
|
|
|
intersect: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
repositionTooltip(e) {
|
|
|
|
const tooltipEl = document.getElementById('chartjs-tooltip');
|
|
|
|
if (tooltipEl && window.innerWidth >= 768) {
|
|
|
|
if (e.clientX > 0.66 * window.innerWidth) {
|
|
|
|
tooltipEl.style.right = (window.innerWidth - e.clientX) + window.pageXOffset + 'px'
|
|
|
|
tooltipEl.style.left = null;
|
|
|
|
} else {
|
|
|
|
tooltipEl.style.right = null;
|
|
|
|
tooltipEl.style.left = e.clientX + window.pageXOffset + 'px'
|
|
|
|
}
|
|
|
|
tooltipEl.style.top = e.clientY + window.pageYOffset + 'px'
|
|
|
|
tooltipEl.style.opacity = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
2023-01-31 22:11:51 +03:00
|
|
|
if (this.props.graphData) {
|
2022-04-13 10:38:47 +03:00
|
|
|
this.chart = this.regenerateChart();
|
|
|
|
}
|
|
|
|
window.addEventListener('mousemove', this.repositionTooltip);
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps) {
|
2023-01-31 22:11:51 +03:00
|
|
|
const { graphData, darkTheme } = this.props;
|
2022-04-13 10:38:47 +03:00
|
|
|
const tooltip = document.getElementById('chartjs-tooltip');
|
|
|
|
|
2022-11-22 15:50:58 +03:00
|
|
|
if (
|
2022-04-13 10:38:47 +03:00
|
|
|
graphData !== prevProps.graphData ||
|
|
|
|
darkTheme !== prevProps.darkTheme
|
2022-11-22 15:50:58 +03:00
|
|
|
) {
|
|
|
|
|
2023-01-31 22:11:51 +03:00
|
|
|
if (graphData) {
|
2022-11-22 15:50:58 +03:00
|
|
|
if (this.chart) {
|
|
|
|
this.chart.destroy();
|
|
|
|
}
|
|
|
|
this.chart = this.regenerateChart();
|
|
|
|
this.chart.update();
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (tooltip) {
|
|
|
|
tooltip.style.display = 'none';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-31 22:11:51 +03:00
|
|
|
if (!graphData) {
|
2022-04-13 10:38:47 +03:00
|
|
|
if (this.chart) {
|
|
|
|
this.chart.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tooltip) {
|
|
|
|
tooltip.style.display = 'none';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
tooltip.style.display = 'none';
|
|
|
|
}
|
|
|
|
window.removeEventListener('mousemove', this.repositionTooltip)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The current ticks' limits are set to treat iPad (regular/Mini/Pro) as a regular screen.
|
|
|
|
* @param {*} chart - The chart instance.
|
|
|
|
* @param {*} dimensions - An object containing the new dimensions *of the chart.*
|
|
|
|
*/
|
|
|
|
updateWindowDimensions(chart, dimensions) {
|
|
|
|
chart.options.scales.x.ticks.maxTicksLimit = dimensions.width < 720 ? 5 : 8
|
|
|
|
}
|
|
|
|
|
|
|
|
onClick(e) {
|
|
|
|
const element = this.chart.getElementsAtEventForMode(e, 'index', { intersect: false })[0]
|
2023-03-22 15:31:44 +03:00
|
|
|
const date = this.props.graphData.labels[element.index] || this.props.graphData.comparison_labels[element.index]
|
2022-04-13 10:38:47 +03:00
|
|
|
|
|
|
|
if (this.props.graphData.interval === 'month') {
|
2023-03-22 15:31:44 +03:00
|
|
|
navigateToQuery(this.props.history, this.props.query, { period: 'month', date })
|
2022-04-13 10:38:47 +03:00
|
|
|
} else if (this.props.graphData.interval === 'date') {
|
2023-03-22 15:31:44 +03:00
|
|
|
navigateToQuery(this.props.history, this.props.query, { period: 'day', date })
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pollExportReady() {
|
|
|
|
if (document.cookie.includes('exporting')) {
|
|
|
|
setTimeout(this.pollExportReady.bind(this), 1000);
|
|
|
|
} else {
|
|
|
|
this.setState({exported: false})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
downloadSpinner() {
|
|
|
|
this.setState({exported: true});
|
|
|
|
document.cookie = "exporting=";
|
|
|
|
setTimeout(this.pollExportReady.bind(this), 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
downloadLink() {
|
|
|
|
if (this.props.query.period !== 'realtime') {
|
|
|
|
|
|
|
|
if (this.state.exported) {
|
|
|
|
return (
|
|
|
|
<div className="w-4 h-4 mx-2">
|
|
|
|
<svg className="animate-spin h-4 w-4 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
</svg>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
} else {
|
2023-01-02 18:42:57 +03:00
|
|
|
const interval = this.props.graphData?.interval
|
2022-11-25 11:53:22 +03:00
|
|
|
const queryParams = api.serializeQuery(this.props.query, [{ interval }])
|
|
|
|
const endpoint = `/${encodeURIComponent(this.props.site.domain)}/export${queryParams}`
|
2022-04-13 10:38:47 +03:00
|
|
|
|
|
|
|
return (
|
|
|
|
<a className="w-4 h-4 mx-2" href={endpoint} download onClick={this.downloadSpinner.bind(this)}>
|
2022-11-22 15:50:58 +03:00
|
|
|
<svg className="absolute text-gray-700 feather dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-600" 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>
|
2022-04-13 10:38:47 +03:00
|
|
|
</a>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
samplingNotice() {
|
|
|
|
const samplePercent = this.props.topStatData && this.props.topStatData.sample_percent
|
|
|
|
|
|
|
|
if (samplePercent < 100) {
|
|
|
|
return (
|
|
|
|
<div tooltip={`Stats based on a ${samplePercent}% sample of all visitors`} className="cursor-pointer w-4 h-4 mx-2">
|
|
|
|
<svg className="absolute w-4 h-4 dark:text-gray-300 text-gray-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
</svg>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
importedNotice() {
|
2023-05-03 11:46:13 +03:00
|
|
|
if (!this.props.topStatData?.imported_source) return
|
2022-04-13 10:38:47 +03:00
|
|
|
|
2023-05-03 11:46:13 +03:00
|
|
|
const isBeforeNativeStats = (date) => {
|
|
|
|
if (!date) return false
|
|
|
|
|
|
|
|
const nativeStatsBegin = parseNaiveDate(this.props.site.nativeStatsBegin)
|
|
|
|
const parsedDate = parseNaiveDate(date)
|
|
|
|
|
|
|
|
return isBefore(parsedDate, nativeStatsBegin, "day")
|
|
|
|
}
|
|
|
|
|
|
|
|
const isQueryingImportedPeriod = isBeforeNativeStats(this.props.topStatData.from)
|
|
|
|
const isComparingImportedPeriod = isBeforeNativeStats(this.props.topStatData.comparing_from)
|
|
|
|
|
|
|
|
if (isQueryingImportedPeriod || isComparingImportedPeriod) {
|
|
|
|
const source = this.props.topStatData.imported_source
|
2022-04-13 10:38:47 +03:00
|
|
|
const withImported = this.props.topStatData.with_imported;
|
|
|
|
const strike = withImported ? "" : " line-through"
|
|
|
|
const target = url.setQuery('with_imported', !withImported)
|
|
|
|
const tip = withImported ? "" : "do not ";
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Link to={target} className="w-4 h-4 mx-2">
|
|
|
|
<div tooltip={`Stats ${tip}include data imported from ${source}.`} className="cursor-pointer w-4 h-4">
|
|
|
|
<svg className="absolute dark:text-gray-300 text-gray-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
<text x="4" y="18" fontSize="24" fill="currentColor" className={"text-gray-700 dark:text-gray-300" + strike}>{ source[0].toUpperCase() }</text>
|
|
|
|
</svg>
|
|
|
|
</div>
|
|
|
|
</Link>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-14 15:59:25 +03:00
|
|
|
// This function is used for maintaining the main-graph/top-stats container height in the
|
|
|
|
// loading process. The container height depends on how many top stat metrics are returned
|
|
|
|
// from the API, but in the loading state, we don't know that yet. We can use localStorage
|
|
|
|
// to keep track of the Top Stats container height.
|
|
|
|
getTopStatsHeight() {
|
|
|
|
if (this.props.topStatData) {
|
|
|
|
return 'auto'
|
|
|
|
} else {
|
|
|
|
return `${storage.getItem(`topStatsHeight__${this.props.site.domain}`) || 89}px`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-13 10:38:47 +03:00
|
|
|
render() {
|
2023-02-14 15:59:25 +03:00
|
|
|
const { mainGraphRefreshing, updateMetric, updateInterval, metric, topStatData, query, site, graphData, lastLoadTimestamp } = this.props
|
|
|
|
const canvasClass = classNames('mt-4 select-none', {'cursor-pointer': !['minute', 'hour'].includes(graphData?.interval)})
|
2022-04-13 10:38:47 +03:00
|
|
|
|
|
|
|
return (
|
2023-02-14 15:59:25 +03:00
|
|
|
<div>
|
|
|
|
<div id="top-stats-container" className="flex flex-wrap" ref={this.boundary} style={{height: this.getTopStatsHeight()}}>
|
2023-04-11 15:59:10 +03:00
|
|
|
<TopStats site={site} query={query} metric={metric} updateMetric={updateMetric} topStatData={topStatData} tooltipBoundary={this.boundary.current} lastLoadTimestamp={lastLoadTimestamp} />
|
2022-04-13 10:38:47 +03:00
|
|
|
</div>
|
|
|
|
<div className="relative px-2">
|
2023-02-14 15:59:25 +03:00
|
|
|
{mainGraphRefreshing && renderLoader()}
|
2023-04-26 11:24:56 +03:00
|
|
|
<div className="absolute right-4 -top-8 py-1 flex items-center">
|
2022-11-22 15:50:58 +03:00
|
|
|
{ this.downloadLink() }
|
|
|
|
{ this.samplingNotice() }
|
2022-04-13 10:38:47 +03:00
|
|
|
{ this.importedNotice() }
|
2023-02-14 15:59:25 +03:00
|
|
|
<IntervalPicker site={site} query={query} graphData={graphData} metric={metric} updateInterval={updateInterval}/>
|
2022-04-13 10:38:47 +03:00
|
|
|
</div>
|
2022-11-22 15:50:58 +03:00
|
|
|
<FadeIn show={graphData}>
|
2023-02-22 15:23:38 +03:00
|
|
|
<div className="relative h-96 w-full z-0">
|
2023-02-14 15:59:25 +03:00
|
|
|
<canvas id="main-graph-canvas" className={canvasClass}></canvas>
|
|
|
|
</div>
|
2022-11-22 15:50:58 +03:00
|
|
|
</FadeIn>
|
2022-04-13 10:38:47 +03:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const LineGraphWithRouter = withRouter(LineGraph)
|
|
|
|
|
|
|
|
export default class VisitorGraph extends React.Component {
|
|
|
|
constructor(props) {
|
|
|
|
super(props)
|
|
|
|
this.state = {
|
2023-01-31 22:11:51 +03:00
|
|
|
topStatsLoadingState: LoadingState.loading,
|
|
|
|
mainGraphLoadingState: LoadingState.loading,
|
2023-01-02 18:42:57 +03:00
|
|
|
metric: storage.getItem(`metric__${this.props.site.domain}`) || 'visitors'
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
this.onVisible = this.onVisible.bind(this)
|
|
|
|
this.updateMetric = this.updateMetric.bind(this)
|
|
|
|
this.fetchTopStatData = this.fetchTopStatData.bind(this)
|
|
|
|
this.fetchGraphData = this.fetchGraphData.bind(this)
|
2022-11-22 15:50:58 +03:00
|
|
|
this.updateInterval = this.updateInterval.bind(this)
|
|
|
|
}
|
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
isIntervalValid(interval) {
|
|
|
|
const { query, site } = this.props
|
|
|
|
const validIntervals = site.validIntervalsByPeriod[query.period] || []
|
2022-11-22 15:50:58 +03:00
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
return validIntervals.includes(interval)
|
2022-11-22 15:50:58 +03:00
|
|
|
}
|
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
getIntervalFromStorage() {
|
|
|
|
const { query, site } = this.props
|
|
|
|
const storedInterval = getStoredInterval(query.period, site.domain)
|
2022-11-22 15:50:58 +03:00
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
if (this.isIntervalValid(storedInterval)) {
|
|
|
|
return storedInterval
|
2022-11-22 15:50:58 +03:00
|
|
|
} else {
|
2023-01-02 18:42:57 +03:00
|
|
|
return null
|
2022-11-22 15:50:58 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateInterval(interval) {
|
2023-01-02 18:42:57 +03:00
|
|
|
if (this.isIntervalValid(interval)) {
|
2022-11-22 15:50:58 +03:00
|
|
|
storeInterval(this.props.query.period, this.props.site.domain, interval)
|
2023-02-14 15:59:25 +03:00
|
|
|
this.setState({ mainGraphLoadingState: LoadingState.refreshing, graphData: null }, this.fetchGraphData)
|
2022-11-22 15:50:58 +03:00
|
|
|
}
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
onVisible() {
|
2023-01-31 22:11:51 +03:00
|
|
|
this.setState({mainGraphLoadingState: LoadingState.loading}, this.fetchGraphData)
|
2022-04-13 10:38:47 +03:00
|
|
|
this.fetchTopStatData()
|
2023-01-02 18:42:57 +03:00
|
|
|
if (this.props.query.period === 'realtime') {
|
|
|
|
document.addEventListener('tick', this.fetchGraphData)
|
|
|
|
document.addEventListener('tick', this.fetchTopStatData)
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
2023-01-18 17:37:03 +03:00
|
|
|
const { metric } = this.state;
|
|
|
|
const { query } = this.props
|
2022-04-13 10:38:47 +03:00
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
if (query !== prevProps.query) {
|
2023-01-31 22:11:51 +03:00
|
|
|
this.setState({ mainGraphLoadingState: LoadingState.loading, topStatsLoadingState: LoadingState.loading, graphData: null, topStatData: null }, this.fetchGraphData)
|
2022-04-13 10:38:47 +03:00
|
|
|
this.fetchTopStatData()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (metric !== prevState.metric) {
|
2023-01-31 22:11:51 +03:00
|
|
|
this.setState({mainGraphLoadingState: LoadingState.refreshing}, this.fetchGraphData)
|
2022-11-22 15:50:58 +03:00
|
|
|
}
|
2023-01-18 17:37:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
resetMetric() {
|
|
|
|
const { topStatData } = this.state
|
|
|
|
const { query, site } = this.props
|
2022-11-22 15:50:58 +03:00
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
const savedMetric = storage.getItem(`metric__${site.domain}`)
|
2023-01-18 17:37:03 +03:00
|
|
|
const selectableMetrics = topStatData && topStatData.top_stats.map(({ name }) => METRIC_MAPPING[name]).filter(name => name)
|
|
|
|
const canSelectSavedMetric = selectableMetrics && selectableMetrics.includes(savedMetric)
|
|
|
|
|
|
|
|
if (query.filters.goal) {
|
|
|
|
this.setState({ metric: 'conversions' })
|
2023-01-31 22:11:51 +03:00
|
|
|
} else if (canSelectSavedMetric) {
|
2023-01-18 17:37:03 +03:00
|
|
|
this.setState({ metric: savedMetric })
|
|
|
|
} else {
|
|
|
|
this.setState({ metric: 'visitors' })
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
componentWillUnmount() {
|
|
|
|
document.removeEventListener('tick', this.fetchGraphData)
|
|
|
|
document.removeEventListener('tick', this.fetchTopStatData)
|
|
|
|
}
|
|
|
|
|
2023-02-14 15:59:25 +03:00
|
|
|
storeTopStatsContainerHeight() {
|
|
|
|
storage.setItem(`topStatsHeight__${this.props.site.domain}`, document.getElementById('top-stats-container').clientHeight)
|
|
|
|
}
|
|
|
|
|
2023-01-02 18:42:57 +03:00
|
|
|
updateMetric(clickedMetric) {
|
2023-01-31 22:11:51 +03:00
|
|
|
if (this.state.metric == clickedMetric) return
|
2023-01-02 18:42:57 +03:00
|
|
|
|
2023-01-31 22:11:51 +03:00
|
|
|
storage.setItem(`metric__${this.props.site.domain}`, clickedMetric)
|
|
|
|
this.setState({ metric: clickedMetric, graphData: null })
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fetchGraphData() {
|
2022-11-22 15:50:58 +03:00
|
|
|
const url = `/api/stats/${encodeURIComponent(this.props.site.domain)}/main-graph`
|
2023-01-02 18:42:57 +03:00
|
|
|
let params = { metric: this.state.metric }
|
|
|
|
const interval = this.getIntervalFromStorage()
|
|
|
|
if (interval) { params.interval = interval }
|
2022-11-22 15:50:58 +03:00
|
|
|
|
|
|
|
api.get(url, this.props.query, params)
|
2022-04-13 10:38:47 +03:00
|
|
|
.then((res) => {
|
2023-01-31 22:11:51 +03:00
|
|
|
this.setState({ mainGraphLoadingState: LoadingState.loaded, graphData: res })
|
2022-04-13 10:38:47 +03:00
|
|
|
return res
|
|
|
|
})
|
2022-11-22 15:50:58 +03:00
|
|
|
.catch((err) => {
|
|
|
|
console.log(err)
|
2023-01-31 22:11:51 +03:00
|
|
|
this.setState({ mainGraphLoadingState: LoadingState.loaded, graphData: false })
|
2022-11-22 15:50:58 +03:00
|
|
|
})
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
fetchTopStatData() {
|
2023-05-04 12:42:48 +03:00
|
|
|
const query = { ...this.props.query }
|
|
|
|
if (!isComparisonEnabled(query.comparison)) query.comparison = 'previous_period'
|
|
|
|
|
|
|
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/top-stats`, query)
|
2022-04-13 10:38:47 +03:00
|
|
|
.then((res) => {
|
2023-02-14 15:59:25 +03:00
|
|
|
this.setState({ topStatsLoadingState: LoadingState.loaded, topStatData: res }, () => {
|
|
|
|
this.storeTopStatsContainerHeight()
|
|
|
|
this.resetMetric()
|
|
|
|
})
|
2022-04-13 10:38:47 +03:00
|
|
|
return res
|
|
|
|
})
|
2022-11-22 15:50:58 +03:00
|
|
|
}
|
2022-04-13 10:38:47 +03:00
|
|
|
|
|
|
|
renderInner() {
|
|
|
|
const { query, site } = this.props;
|
2022-11-22 15:50:58 +03:00
|
|
|
const { graphData, metric, topStatData, topStatsLoadingState, mainGraphLoadingState } = this.state;
|
2022-04-13 10:38:47 +03:00
|
|
|
|
|
|
|
const theme = document.querySelector('html').classList.contains('dark') || false
|
|
|
|
|
2023-01-31 22:11:51 +03:00
|
|
|
const mainGraphRefreshing = (mainGraphLoadingState === LoadingState.refreshing)
|
2022-11-22 15:50:58 +03:00
|
|
|
const topStatAndGraphLoaded = !!(topStatData && graphData)
|
|
|
|
|
2023-02-14 15:59:25 +03:00
|
|
|
const shouldShow =
|
|
|
|
topStatsLoadingState === LoadingState.loaded &&
|
2023-01-31 22:11:51 +03:00
|
|
|
LoadingState.isLoadedOrRefreshing(mainGraphLoadingState) &&
|
|
|
|
(topStatData && mainGraphRefreshing || topStatAndGraphLoaded)
|
2022-11-22 15:50:58 +03:00
|
|
|
|
|
|
|
return (
|
2023-02-14 15:59:25 +03:00
|
|
|
<FadeIn show={shouldShow}>
|
|
|
|
<LineGraphWithRouter mainGraphRefreshing={mainGraphRefreshing} graphData={graphData} topStatData={topStatData} site={site} query={query} darkTheme={theme} metric={metric} updateMetric={this.updateMetric} updateInterval={this.updateInterval} lastLoadTimestamp={this.props.lastLoadTimestamp} />
|
2022-11-22 15:50:58 +03:00
|
|
|
</FadeIn>
|
|
|
|
)
|
2022-04-13 10:38:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2023-01-31 22:11:51 +03:00
|
|
|
const {mainGraphLoadingState, topStatsLoadingState} = this.state
|
2022-11-22 15:50:58 +03:00
|
|
|
|
2023-01-31 22:11:51 +03:00
|
|
|
const showLoader =
|
2023-02-14 15:59:25 +03:00
|
|
|
[mainGraphLoadingState, topStatsLoadingState].includes(LoadingState.loading) &&
|
|
|
|
mainGraphLoadingState !== LoadingState.refreshing
|
2022-04-13 10:38:47 +03:00
|
|
|
|
|
|
|
return (
|
|
|
|
<LazyLoader onVisible={this.onVisible}>
|
2023-02-14 15:59:25 +03:00
|
|
|
<div className={"relative w-full mt-2 bg-white rounded shadow-xl dark:bg-gray-825"}>
|
|
|
|
{showLoader && renderLoader()}
|
2022-04-13 10:38:47 +03:00
|
|
|
{this.renderInner()}
|
|
|
|
</div>
|
|
|
|
</LazyLoader>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2023-02-14 15:59:25 +03:00
|
|
|
|
|
|
|
function renderLoader() {
|
|
|
|
return (
|
|
|
|
<div className="absolute h-full w-full flex items-center justify-center">
|
|
|
|
<div className="loading">
|
|
|
|
<div></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|