import React from 'react'; import { withRouter, Link } from 'react-router-dom' import Chart from 'chart.js/auto'; import { navigateToQuery } from '../../query' import numberFormatter, {durationFormatter} from '../../util/number-formatter' import * as api from '../../api' import * as storage from '../../util/storage' import LazyLoader from '../../components/lazy-loader' import {GraphTooltip, buildDataSet, dateFormatter} from './graph-util'; import TopStats from './top-stats'; import * as url from '../../util/url' 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', // 'Time on Page': 'time', // 'Conversion rate': 'conversion_rate', // 'Total conversions': 't_conversions', } export const METRIC_LABELS = { 'visitors': 'Visitors', 'pageviews': 'Pageviews', 'bounce_rate': 'Bounce Rate', 'visit_duration': 'Visit Duration', 'conversions': 'Converted Visitors', // 'time': 'Time on Page', // 'conversion_rate': 'Conversion Rate', // 't_conversions': 'Total Conversions' } export const METRIC_FORMATTER = { 'visitors': numberFormatter, 'pageviews': numberFormatter, 'bounce_rate': (number) => (`${number}%`), 'visit_duration': durationFormatter, 'conversions': numberFormatter, // 'time': durationFormatter, // 'conversion_rate': (number) => (`${Math.max(number, 100)}%`), // 't_conversions': numberFormatter } class LineGraph extends React.Component { constructor(props) { super(props); this.regenerateChart = this.regenerateChart.bind(this); this.updateWindowDimensions = this.updateWindowDimensions.bind(this); this.state = { exported: false }; } regenerateChart() { const { graphData, metric } = this.props const graphEl = document.getElementById("main-graph-canvas") this.ctx = graphEl.getContext('2d'); const dataSet = buildDataSet(graphData.plot, graphData.present_index, this.ctx, METRIC_LABELS[metric]) // const prev_dataSet = graphData.prev_plot && buildDataSet(graphData.prev_plot, false, this.ctx, METRIC_LABELS[metric], true) // const combinedDataSets = comparison.enabled && prev_dataSet ? [...dataSet, ...prev_dataSet] : dataSet; 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', external: GraphTooltip(graphData, metric) }, }, responsive: true, onResize: this.updateWindowDimensions, elements: { line: { tension: 0 }, point: { radius: 0 } }, onClick: this.onClick.bind(this), scales: { y: { beginAtZero: true, ticks: { callback: METRIC_FORMATTER[metric], maxTicksLimit: 8, color: this.props.darkTheme ? 'rgb(243, 244, 246)' : undefined }, grid: { zeroLineColor: 'transparent', drawBorder: false, } }, x: { grid: { display: false }, ticks: { maxTicksLimit: 8, callback: function(val, _index, _ticks) { return dateFormatter(graphData.interval)(this.getLabelForValue(val)) }, 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() { if (this.props.metric && this.props.graphData) { this.chart = this.regenerateChart(); } window.addEventListener('mousemove', this.repositionTooltip); } componentDidUpdate(prevProps) { const { graphData, metric, darkTheme } = this.props; const tooltip = document.getElementById('chartjs-tooltip'); if (metric && graphData && ( graphData !== prevProps.graphData || darkTheme !== prevProps.darkTheme )) { if (this.chart) { this.chart.destroy(); } this.chart = this.regenerateChart(); this.chart.update(); if (tooltip) { tooltip.style.display = 'none'; } } if (!graphData || !metric) { 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 chart.options.scales.y.ticks.maxTicksLimit = dimensions.height < 233 ? 3 : 8 } onClick(e) { const element = this.chart.getElementsAtEventForMode(e, 'index', { intersect: false })[0] const date = this.chart.data.labels[element.index] if (this.props.graphData.interval === 'month') { navigateToQuery( this.props.history, this.props.query, { period: 'month', date, } ) } else if (this.props.graphData.interval === 'date') { navigateToQuery( this.props.history, this.props.query, { period: 'day', date, } ) } } 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 (