mirror of
https://github.com/plausible/analytics.git
synced 2025-01-05 17:16:44 +03:00
Custom comparison mode (#2753)
This commit implements the custom comparison mode using a date range.
This commit is contained in:
parent
9f71bb3921
commit
87a63fe28a
@ -45,7 +45,13 @@ export function serializeQuery(query, extraQuery=[]) {
|
|||||||
if (query.filters) { queryObj.filters = serializeFilters(query.filters) }
|
if (query.filters) { queryObj.filters = serializeFilters(query.filters) }
|
||||||
if (query.with_imported) { queryObj.with_imported = query.with_imported }
|
if (query.with_imported) { queryObj.with_imported = query.with_imported }
|
||||||
if (SHARED_LINK_AUTH) { queryObj.auth = SHARED_LINK_AUTH }
|
if (SHARED_LINK_AUTH) { queryObj.auth = SHARED_LINK_AUTH }
|
||||||
if (query.comparison) { queryObj.comparison = query.comparison }
|
|
||||||
|
if (query.comparison) {
|
||||||
|
queryObj.comparison = query.comparison
|
||||||
|
queryObj.compare_from = query.compare_from
|
||||||
|
queryObj.compare_to = query.compare_to
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(queryObj, ...extraQuery)
|
Object.assign(queryObj, ...extraQuery)
|
||||||
|
|
||||||
return '?' + serialize(queryObj)
|
return '?' + serialize(queryObj)
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from 'react-router-dom'
|
||||||
import { navigateToQuery } from './query'
|
import { navigateToQuery } from './query'
|
||||||
import { Menu, Transition } from '@headlessui/react'
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import * as storage from './util/storage'
|
import * as storage from './util/storage'
|
||||||
|
import Flatpickr from 'react-flatpickr'
|
||||||
|
import { formatISO, parseUTCDate, formatDayShort } from './util/date.js'
|
||||||
|
|
||||||
const COMPARISON_MODES = {
|
const COMPARISON_MODES = {
|
||||||
'off': 'Disable comparison',
|
'off': 'Disable comparison',
|
||||||
'previous_period': 'Previous period',
|
'previous_period': 'Previous period',
|
||||||
'year_over_year': 'Year over year',
|
'year_over_year': 'Year over year',
|
||||||
|
'custom': 'Custom period',
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_COMPARISON_MODE = 'previous_period'
|
const DEFAULT_COMPARISON_MODE = 'previous_period'
|
||||||
@ -26,6 +29,7 @@ export const getStoredComparisonMode = function(domain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const storeComparisonMode = function(domain, mode) {
|
const storeComparisonMode = function(domain, mode) {
|
||||||
|
if (mode == "custom") return
|
||||||
storage.setItem(`comparison_mode__${domain}`, mode)
|
storage.setItem(`comparison_mode__${domain}`, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,17 +53,31 @@ export const toggleComparisons = function(history, query, site) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownItem({ label, value, isCurrentlySelected, updateMode }) {
|
function DropdownItem({ label, value, isCurrentlySelected, updateMode, calendar }) {
|
||||||
|
const click = () => {
|
||||||
|
if (value == "custom") {
|
||||||
|
// https://github.com/flatpickr/flatpickr/issues/399#issuecomment-260007013
|
||||||
|
// FIXME: Use setState to prevent this issue
|
||||||
|
setTimeout(() => calendar.current.flatpickr.open(), 100)
|
||||||
|
} else {
|
||||||
|
updateMode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const render = ({ active }) => {
|
||||||
|
const buttonClass = classNames("px-4 py-2 w-full text-left font-medium text-sm dark:text-white cursor-pointer", {
|
||||||
|
"bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100": active,
|
||||||
|
"font-bold": isCurrentlySelected,
|
||||||
|
})
|
||||||
|
|
||||||
|
return <button className={buttonClass}>{ label }</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabled = isCurrentlySelected && value !== "custom"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.Item
|
<Menu.Item key={value} onClick={click} disabled={disabled}>
|
||||||
key={value}
|
{ render }
|
||||||
onClick={() => updateMode(value)}
|
|
||||||
disabled={isCurrentlySelected}>
|
|
||||||
{({ active }) => (
|
|
||||||
<button className={classNames("px-4 py-2 w-full text-left font-medium text-sm dark:text-white cursor-pointer", { "bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100": active, "font-bold": isCurrentlySelected })}>
|
|
||||||
{ label }
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -69,19 +87,46 @@ const ComparisonInput = function({ site, query, history }) {
|
|||||||
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
|
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
|
||||||
if (!isComparisonEnabled(query.comparison)) return null
|
if (!isComparisonEnabled(query.comparison)) return null
|
||||||
|
|
||||||
const updateMode = (key) => {
|
const updateMode = (mode, from = null, to = null) => {
|
||||||
storeComparisonMode(site.domain, key)
|
storeComparisonMode(site.domain, mode)
|
||||||
navigateToQuery(history, query, { comparison: key })
|
navigateToQuery(history, query, { comparison: mode, compare_from: from, compare_to: to })
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildLabel = (query) => {
|
||||||
|
if (query.comparison == "custom") {
|
||||||
|
const from = parseUTCDate(query.compare_from)
|
||||||
|
const to = parseUTCDate(query.compare_to)
|
||||||
|
return `${formatDayShort(from, false)} - ${formatDayShort(to, false)}`
|
||||||
|
} else {
|
||||||
|
return COMPARISON_MODES[query.comparison]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendar = React.useRef(null)
|
||||||
|
|
||||||
|
const flatpickrOptions = {
|
||||||
|
mode: 'range',
|
||||||
|
showMonths: 1,
|
||||||
|
maxDate: 'today',
|
||||||
|
minDate: parseUTCDate(site.statsBegin),
|
||||||
|
animate: true,
|
||||||
|
static: true,
|
||||||
|
onChange: ([from, to]) => {
|
||||||
|
if (from && to) updateMode("custom", formatISO(from), formatISO(to))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<span className="h-0 w-0 invisible">
|
||||||
|
<Flatpickr ref={calendar} options={flatpickrOptions} />
|
||||||
|
</span>
|
||||||
<span className="pl-2 text-sm font-medium text-gray-800 dark:text-gray-200">vs.</span>
|
<span className="pl-2 text-sm font-medium text-gray-800 dark:text-gray-200">vs.</span>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="min-w-32 md:w-52 md:relative">
|
<div className="min-w-32 md:w-48 md:relative">
|
||||||
<Menu as="div" className="relative inline-block pl-2 w-full">
|
<Menu as="div" className="relative inline-block pl-2 w-full">
|
||||||
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow cursor-pointer w-full truncate">
|
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow cursor-pointer w-full truncate">
|
||||||
<span className="truncate">{ COMPARISON_MODES[query.comparison] || 'Compare to' }</span>
|
<span className="truncate">{ buildLabel(query) }</span>
|
||||||
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-2" aria-hidden="true" />
|
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-2" aria-hidden="true" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Transition
|
<Transition
|
||||||
@ -93,7 +138,7 @@ const ComparisonInput = function({ site, query, history }) {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95">
|
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" static>
|
<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" static>
|
||||||
{ Object.keys(COMPARISON_MODES).map((key) => DropdownItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode })) }
|
{ Object.keys(COMPARISON_MODES).map((key) => DropdownItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode, calendar })) }
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -382,8 +382,6 @@ function DatePicker({query, site, history}) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} if (mode === "calendar") {
|
} if (mode === "calendar") {
|
||||||
const insertionDate = new Date(site.statsBegin);
|
|
||||||
const dayBeforeCreation = insertionDate - 86400000;
|
|
||||||
return (
|
return (
|
||||||
<div className="h-0">
|
<div className="h-0">
|
||||||
<Flatpickr
|
<Flatpickr
|
||||||
@ -391,7 +389,7 @@ function DatePicker({query, site, history}) {
|
|||||||
options={{
|
options={{
|
||||||
mode: 'range',
|
mode: 'range',
|
||||||
maxDate: 'today',
|
maxDate: 'today',
|
||||||
minDate: dayBeforeCreation,
|
minDate: parseUTCDate(site.statsBegin),
|
||||||
showMonths: 1,
|
showMonths: 1,
|
||||||
static: true,
|
static: true,
|
||||||
animate: true}}
|
animate: true}}
|
||||||
|
@ -25,6 +25,8 @@ export function parseQuery(querystring, site) {
|
|||||||
return {
|
return {
|
||||||
period,
|
period,
|
||||||
comparison,
|
comparison,
|
||||||
|
compare_from: q.get('compare_from'),
|
||||||
|
compare_to: q.get('compare_to'),
|
||||||
date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site),
|
date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site),
|
||||||
from: q.get('from') ? parseUTCDate(q.get('from')) : undefined,
|
from: q.get('from') ? parseUTCDate(q.get('from')) : undefined,
|
||||||
to: q.get('to') ? parseUTCDate(q.get('to')) : undefined,
|
to: q.get('to') ? parseUTCDate(q.get('to')) : undefined,
|
||||||
|
@ -37,12 +37,12 @@ const buildTooltipData = function(query, graphData, metric, tooltipModel) {
|
|||||||
const data = tooltipModel.dataPoints.find((dataPoint) => dataPoint.dataset.yAxisID == "y")
|
const data = tooltipModel.dataPoints.find((dataPoint) => dataPoint.dataset.yAxisID == "y")
|
||||||
const comparisonData = tooltipModel.dataPoints.find((dataPoint) => dataPoint.dataset.yAxisID == "yComparison")
|
const comparisonData = tooltipModel.dataPoints.find((dataPoint) => dataPoint.dataset.yAxisID == "yComparison")
|
||||||
|
|
||||||
const label = renderBucketLabel(query, graphData, graphData.labels[data.dataIndex])
|
const label = data && renderBucketLabel(query, graphData, graphData.labels[data.dataIndex])
|
||||||
const comparisonLabel = comparisonData && renderBucketLabel(query, graphData, graphData.comparison_labels[data.dataIndex], true)
|
const comparisonLabel = comparisonData && renderBucketLabel(query, graphData, graphData.comparison_labels[comparisonData.dataIndex], true)
|
||||||
|
|
||||||
const value = data?.raw || 0
|
const value = data?.raw || 0
|
||||||
const comparisonValue = comparisonData?.raw || 0
|
const comparisonValue = comparisonData?.raw || 0
|
||||||
const comparisonDifference = comparisonData && calculatePercentageDifference(comparisonValue, value)
|
const comparisonDifference = label && comparisonLabel && calculatePercentageDifference(comparisonValue, value)
|
||||||
|
|
||||||
const metricFormatter = METRIC_FORMATTER[metric]
|
const metricFormatter = METRIC_FORMATTER[metric]
|
||||||
const formattedValue = metricFormatter(value)
|
const formattedValue = metricFormatter(value)
|
||||||
@ -84,21 +84,23 @@ export default function GraphTooltip(graphData, metric, query) {
|
|||||||
<aside class="text-gray-100 flex flex-col">
|
<aside class="text-gray-100 flex flex-col">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="font-semibold mr-4 text-lg">${METRIC_LABELS[metric]}</span>
|
<span class="font-semibold mr-4 text-lg">${METRIC_LABELS[metric]}</span>
|
||||||
<div class="inline-flex items-center space-x-1">
|
${tooltipData.comparisonDifference ?
|
||||||
|
`<div class="inline-flex items-center space-x-1">
|
||||||
${tooltipData.comparisonDifference > 0 ? `<span class="font-semibold text-sm text-green-500">↑</span><span>${tooltipData.comparisonDifference}%</span>` : ""}
|
${tooltipData.comparisonDifference > 0 ? `<span class="font-semibold text-sm text-green-500">↑</span><span>${tooltipData.comparisonDifference}%</span>` : ""}
|
||||||
${tooltipData.comparisonDifference < 0 ? `<span class="font-semibold text-sm text-red-400">↓</span><span>${tooltipData.comparisonDifference * -1}%</span>` : ""}
|
${tooltipData.comparisonDifference < 0 ? `<span class="font-semibold text-sm text-red-400">↓</span><span>${tooltipData.comparisonDifference * -1}%</span>` : ""}
|
||||||
${tooltipData.comparisonDifference == 0 ? `<span class="font-semibold text-sm">〰 0%</span>` : ""}
|
${tooltipData.comparisonDifference == 0 ? `<span class="font-semibold text-sm">〰 0%</span>` : ""}
|
||||||
</div>
|
</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
${tooltipData.label ?
|
||||||
|
`<div class="flex flex-col">
|
||||||
<div class="flex flex-row justify-between items-center">
|
<div class="flex flex-row justify-between items-center">
|
||||||
<span class="flex items-center mr-4">
|
<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>
|
<div class="w-3 h-3 mr-1 rounded-full" style="background-color: rgba(101,116,205)"></div>
|
||||||
<span>${tooltipData.label}</span>
|
<span>${tooltipData.label}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-base font-bold">${tooltipData.formattedValue}</span>
|
<span class="text-base font-bold">${tooltipData.formattedValue}</span>
|
||||||
</div>
|
</div>` : ''}
|
||||||
|
|
||||||
${tooltipData.comparisonLabel ?
|
${tooltipData.comparisonLabel ?
|
||||||
`<div class="flex flex-row justify-between items-center">
|
`<div class="flex flex-row justify-between items-center">
|
||||||
|
@ -40,17 +40,11 @@ export const LoadingState = {
|
|||||||
isLoadedOrRefreshing: function (state) { return [this.loaded, this.refreshing].includes(state) }
|
isLoadedOrRefreshing: function (state) { return [this.loaded, this.refreshing].includes(state) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildComparisonDataset = function(comparisonPlot, presentIndex) {
|
const buildComparisonDataset = function(comparisonPlot) {
|
||||||
if (!comparisonPlot) return []
|
if (!comparisonPlot) return []
|
||||||
|
|
||||||
let data = [...comparisonPlot]
|
|
||||||
if (presentIndex) {
|
|
||||||
const dashedPartIncludedIndex = presentIndex + 1
|
|
||||||
data = data.slice(0, dashedPartIncludedIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
data: data,
|
data: comparisonPlot,
|
||||||
borderColor: 'rgba(60,70,110,0.2)',
|
borderColor: 'rgba(60,70,110,0.2)',
|
||||||
pointBackgroundColor: 'rgba(60,70,110,0.2)',
|
pointBackgroundColor: 'rgba(60,70,110,0.2)',
|
||||||
pointHoverBackgroundColor: 'rgba(60, 70, 110)',
|
pointHoverBackgroundColor: 'rgba(60, 70, 110)',
|
||||||
@ -98,7 +92,7 @@ export const buildDataSet = (plot, comparisonPlot, present_index, ctx, label) =>
|
|||||||
const dataset = [
|
const dataset = [
|
||||||
...buildMainPlotDataset(plot, present_index),
|
...buildMainPlotDataset(plot, present_index),
|
||||||
...buildDashedDataset(plot, present_index),
|
...buildDashedDataset(plot, present_index),
|
||||||
...buildComparisonDataset(comparisonPlot, present_index)
|
...buildComparisonDataset(comparisonPlot)
|
||||||
]
|
]
|
||||||
|
|
||||||
return dataset.map((item) => Object.assign(item, defaultOptions))
|
return dataset.map((item) => Object.assign(item, defaultOptions))
|
||||||
|
@ -91,12 +91,12 @@ class LineGraph extends React.Component {
|
|||||||
ticks: {
|
ticks: {
|
||||||
maxTicksLimit: 8,
|
maxTicksLimit: 8,
|
||||||
callback: function (val, _index, _ticks) {
|
callback: function (val, _index, _ticks) {
|
||||||
// realtime graph labels are not date strings
|
if (this.getLabelForValue(val) == "__blank__") return ""
|
||||||
const hasMultipleYears = typeof graphData.labels[0] !== 'string' ? false :
|
|
||||||
|
const hasMultipleYears =
|
||||||
graphData.labels
|
graphData.labels
|
||||||
// date format: 'yyyy-mm-dd'; maps to -> 'yyyy'
|
.filter((date) => typeof date === 'string')
|
||||||
.map(date => date.split('-')[0])
|
.map(date => date.split('-')[0])
|
||||||
// reject any year that appears at a previous index, unique years only
|
|
||||||
.filter((value, index, list) => list.indexOf(value) === index)
|
.filter((value, index, list) => list.indexOf(value) === index)
|
||||||
.length > 1
|
.length > 1
|
||||||
|
|
||||||
@ -220,26 +220,12 @@ class LineGraph extends React.Component {
|
|||||||
|
|
||||||
onClick(e) {
|
onClick(e) {
|
||||||
const element = this.chart.getElementsAtEventForMode(e, 'index', { intersect: false })[0]
|
const element = this.chart.getElementsAtEventForMode(e, 'index', { intersect: false })[0]
|
||||||
const date = this.chart.data.labels[element.index]
|
const date = this.props.graphData.labels[element.index] || this.props.graphData.comparison_labels[element.index]
|
||||||
|
|
||||||
if (this.props.graphData.interval === 'month') {
|
if (this.props.graphData.interval === 'month') {
|
||||||
navigateToQuery(
|
navigateToQuery(this.props.history, this.props.query, { period: 'month', date })
|
||||||
this.props.history,
|
|
||||||
this.props.query,
|
|
||||||
{
|
|
||||||
period: 'month',
|
|
||||||
date,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (this.props.graphData.interval === 'date') {
|
} else if (this.props.graphData.interval === 'date') {
|
||||||
navigateToQuery(
|
navigateToQuery(this.props.history, this.props.query, { period: 'day', date })
|
||||||
this.props.history,
|
|
||||||
this.props.query,
|
|
||||||
{
|
|
||||||
period: 'day',
|
|
||||||
date,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,10 @@ export function formatYear(date) {
|
|||||||
return `Year of ${date.getFullYear()}`;
|
return `Year of ${date.getFullYear()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatYearShort(date) {
|
||||||
|
return date.getUTCFullYear().toString().substring(2)
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDay(date) {
|
export function formatDay(date) {
|
||||||
var weekday = DAYS_ABBREV[date.getDay()];
|
var weekday = DAYS_ABBREV[date.getDay()];
|
||||||
if (date.getFullYear() !== (new Date()).getFullYear()) {
|
if (date.getFullYear() !== (new Date()).getFullYear()) {
|
||||||
@ -49,8 +53,13 @@ export function formatDay(date) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDayShort(date) {
|
export function formatDayShort(date, includeYear = false) {
|
||||||
return `${date.getDate()} ${formatMonthShort(date)}`;
|
let formatted = `${date.getDate()} ${formatMonthShort(date)}`
|
||||||
|
if (includeYear) {
|
||||||
|
formatted += ` ${formatYearShort(date)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseUTCDate(dateString) {
|
export function parseUTCDate(dateString) {
|
||||||
|
@ -9,40 +9,64 @@ defmodule Plausible.Stats.Comparisons do
|
|||||||
|
|
||||||
alias Plausible.Stats
|
alias Plausible.Stats
|
||||||
|
|
||||||
@modes ~w(previous_period year_over_year)
|
@modes ~w(previous_period year_over_year custom)
|
||||||
@disallowed_periods ~w(realtime all)
|
@disallowed_periods ~w(realtime all)
|
||||||
|
|
||||||
@type mode() :: String.t() | nil
|
@type mode() :: String.t() | nil
|
||||||
|
@typep option() :: {:from, String.t()} | {:to, String.t()} | {:now, NaiveDateTime.t()}
|
||||||
|
|
||||||
@spec compare(
|
@spec compare(Plausible.Site.t(), Stats.Query.t(), mode(), [option()]) ::
|
||||||
Plausible.Site.t(),
|
{:ok, Stats.Query.t()} | {:error, :not_supported} | {:error, :invalid_dates}
|
||||||
Stats.Query.t(),
|
@doc """
|
||||||
mode(),
|
Generates a comparison query based on the source query and comparison mode.
|
||||||
NaiveDateTime.t() | nil
|
|
||||||
) :: {:ok, Stats.Query.t()} | {:error, :not_supported}
|
The mode parameter specifies the type of comparison and can be one of the
|
||||||
def compare(
|
following:
|
||||||
%Plausible.Site{} = site,
|
|
||||||
%Stats.Query{} = source_query,
|
* `"previous_period"` - shifts back the query by the same number of days the
|
||||||
mode,
|
source query has.
|
||||||
now \\ nil
|
|
||||||
) do
|
* `"year_over_year"` - shifts back the query by 1 year.
|
||||||
|
|
||||||
|
* `"custom"` - compares the query using a custom date range. See options for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
The comparison query returned by the function has its end date restricted to
|
||||||
|
the current day. This can be overriden by the `now` option, described below.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `:now` - a `NaiveDateTime` struct with the current date and time. This is
|
||||||
|
optional and used for testing purposes.
|
||||||
|
|
||||||
|
* `:from` - a ISO-8601 date string used when mode is `"custom"`.
|
||||||
|
|
||||||
|
* `:to` - a ISO-8601 date string used when mode is `"custom"`. Must be
|
||||||
|
after `from`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def compare(%Plausible.Site{} = site, %Stats.Query{} = source_query, mode, opts \\ []) do
|
||||||
if valid_mode?(source_query, mode) do
|
if valid_mode?(source_query, mode) do
|
||||||
now = now || Timex.now(site.timezone)
|
opts = Keyword.put_new(opts, :now, Timex.now(site.timezone))
|
||||||
{:ok, do_compare(source_query, mode, now)}
|
do_compare(source_query, mode, opts)
|
||||||
else
|
else
|
||||||
{:error, :not_supported}
|
{:error, :not_supported}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_compare(source_query, "year_over_year", now) do
|
defp do_compare(source_query, "year_over_year", opts) do
|
||||||
|
now = Keyword.fetch!(opts, :now)
|
||||||
|
|
||||||
start_date = Date.add(source_query.date_range.first, -365)
|
start_date = Date.add(source_query.date_range.first, -365)
|
||||||
end_date = earliest(source_query.date_range.last, now) |> Date.add(-365)
|
end_date = earliest(source_query.date_range.last, now) |> Date.add(-365)
|
||||||
|
|
||||||
range = Date.range(start_date, end_date)
|
range = Date.range(start_date, end_date)
|
||||||
%Stats.Query{source_query | date_range: range}
|
{:ok, %Stats.Query{source_query | date_range: range}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_compare(source_query, "previous_period", now) do
|
defp do_compare(source_query, "previous_period", opts) do
|
||||||
|
now = Keyword.fetch!(opts, :now)
|
||||||
|
|
||||||
last = earliest(source_query.date_range.last, now)
|
last = earliest(source_query.date_range.last, now)
|
||||||
diff_in_days = Date.diff(source_query.date_range.first, last) - 1
|
diff_in_days = Date.diff(source_query.date_range.first, last) - 1
|
||||||
|
|
||||||
@ -50,7 +74,17 @@ defmodule Plausible.Stats.Comparisons do
|
|||||||
new_last = Date.add(last, diff_in_days)
|
new_last = Date.add(last, diff_in_days)
|
||||||
|
|
||||||
range = Date.range(new_first, new_last)
|
range = Date.range(new_first, new_last)
|
||||||
%Stats.Query{source_query | date_range: range}
|
{:ok, %Stats.Query{source_query | date_range: range}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_compare(source_query, "custom", opts) do
|
||||||
|
with {:ok, from} <- opts |> Keyword.fetch!(:from) |> Date.from_iso8601(),
|
||||||
|
{:ok, to} <- opts |> Keyword.fetch!(:to) |> Date.from_iso8601(),
|
||||||
|
result when result in [:eq, :lt] <- Date.compare(from, to) do
|
||||||
|
{:ok, %Stats.Query{source_query | date_range: Date.range(from, to)}}
|
||||||
|
else
|
||||||
|
_error -> {:error, :invalid_dates}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp earliest(a, b) do
|
defp earliest(a, b) do
|
||||||
|
@ -110,21 +110,25 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
timeseries_result = Stats.timeseries(site, timeseries_query, [selected_metric])
|
timeseries_result = Stats.timeseries(site, timeseries_query, [selected_metric])
|
||||||
labels = label_timeseries(timeseries_result)
|
|
||||||
present_index = present_index_for(site, query, labels)
|
|
||||||
full_intervals = build_full_intervals(query, labels)
|
|
||||||
|
|
||||||
comparison_result =
|
comparison_result =
|
||||||
case Comparisons.compare(site, query, params["comparison"]) do
|
case Comparisons.compare(site, query, params["comparison"],
|
||||||
|
from: params["compare_from"],
|
||||||
|
to: params["compare_to"]
|
||||||
|
) do
|
||||||
{:ok, comparison_query} -> Stats.timeseries(site, comparison_query, [selected_metric])
|
{:ok, comparison_query} -> Stats.timeseries(site, comparison_query, [selected_metric])
|
||||||
{:error, :not_supported} -> nil
|
{:error, :not_supported} -> nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
labels = label_timeseries(timeseries_result, comparison_result)
|
||||||
|
present_index = present_index_for(site, query, labels)
|
||||||
|
full_intervals = build_full_intervals(query, labels)
|
||||||
|
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
plot: plot_timeseries(timeseries_result, selected_metric),
|
plot: plot_timeseries(timeseries_result, selected_metric),
|
||||||
labels: labels,
|
labels: labels,
|
||||||
comparison_plot: comparison_result && plot_timeseries(comparison_result, selected_metric),
|
comparison_plot: comparison_result && plot_timeseries(comparison_result, selected_metric),
|
||||||
comparison_labels: comparison_result && label_timeseries(comparison_result),
|
comparison_labels: comparison_result && label_timeseries(comparison_result, nil),
|
||||||
present_index: present_index,
|
present_index: present_index,
|
||||||
interval: query.interval,
|
interval: query.interval,
|
||||||
with_imported: query.include_imported,
|
with_imported: query.include_imported,
|
||||||
@ -140,8 +144,20 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
Enum.map(timeseries, fn row -> row[metric] || 0 end)
|
Enum.map(timeseries, fn row -> row[metric] || 0 end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp label_timeseries(timeseries) do
|
defp label_timeseries(main_result, nil) do
|
||||||
Enum.map(timeseries, & &1.date)
|
Enum.map(main_result, & &1.date)
|
||||||
|
end
|
||||||
|
|
||||||
|
@blank_value "__blank__"
|
||||||
|
defp label_timeseries(main_result, comparison_result) do
|
||||||
|
blanks_to_fill = Enum.count(comparison_result) - Enum.count(main_result)
|
||||||
|
|
||||||
|
if blanks_to_fill > 0 do
|
||||||
|
blanks = List.duplicate(@blank_value, blanks_to_fill)
|
||||||
|
Enum.map(main_result, & &1.date) ++ blanks
|
||||||
|
else
|
||||||
|
Enum.map(main_result, & &1.date)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_full_intervals(%{interval: "week", date_range: range}, labels) do
|
defp build_full_intervals(%{interval: "week", date_range: range}, labels) do
|
||||||
@ -175,9 +191,11 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
|
|
||||||
with :ok <- validate_params(params) do
|
with :ok <- validate_params(params) do
|
||||||
comparison_mode = params["comparison"] || "previous_period"
|
comparison_mode = params["comparison"] || "previous_period"
|
||||||
|
comparison_opts = [from: params["compare_from"], to: params["compare_to"]]
|
||||||
|
|
||||||
query = Query.from(site, params) |> Filters.add_prefix()
|
query = Query.from(site, params) |> Filters.add_prefix()
|
||||||
|
|
||||||
{top_stats, sample_percent} = fetch_top_stats(site, query, comparison_mode)
|
{top_stats, sample_percent} = fetch_top_stats(site, query, comparison_mode, comparison_opts)
|
||||||
|
|
||||||
json(conn, %{
|
json(conn, %{
|
||||||
top_stats: top_stats,
|
top_stats: top_stats,
|
||||||
@ -245,7 +263,8 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
defp fetch_top_stats(
|
defp fetch_top_stats(
|
||||||
site,
|
site,
|
||||||
%Query{period: "realtime", filters: %{"event:goal" => _goal}} = query,
|
%Query{period: "realtime", filters: %{"event:goal" => _goal}} = query,
|
||||||
_comparison_mode
|
_comparison_mode,
|
||||||
|
_comparison_opts
|
||||||
) do
|
) do
|
||||||
query_30m = %Query{query | period: "30m"}
|
query_30m = %Query{query | period: "30m"}
|
||||||
|
|
||||||
@ -272,7 +291,12 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
{stats, 100}
|
{stats, 100}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_top_stats(site, %Query{period: "realtime"} = query, _comparison_mode) do
|
defp fetch_top_stats(
|
||||||
|
site,
|
||||||
|
%Query{period: "realtime"} = query,
|
||||||
|
_comparison_mode,
|
||||||
|
_comparison_opts
|
||||||
|
) do
|
||||||
query_30m = %Query{query | period: "30m"}
|
query_30m = %Query{query | period: "30m"}
|
||||||
|
|
||||||
%{
|
%{
|
||||||
@ -298,11 +322,16 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
{stats, 100}
|
{stats, 100}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _goal}} = query, comparison_mode) do
|
defp fetch_top_stats(
|
||||||
|
site,
|
||||||
|
%Query{filters: %{"event:goal" => _goal}} = query,
|
||||||
|
comparison_mode,
|
||||||
|
comparison_opts
|
||||||
|
) do
|
||||||
total_q = Query.remove_event_filters(query, [:goal, :props])
|
total_q = Query.remove_event_filters(query, [:goal, :props])
|
||||||
|
|
||||||
{prev_converted_visitors, prev_completions} =
|
{prev_converted_visitors, prev_completions} =
|
||||||
case Stats.Comparisons.compare(site, query, comparison_mode) do
|
case Stats.Comparisons.compare(site, query, comparison_mode, comparison_opts) do
|
||||||
{:ok, prev_query} ->
|
{:ok, prev_query} ->
|
||||||
%{visitors: %{value: prev_converted_visitors}, events: %{value: prev_completions}} =
|
%{visitors: %{value: prev_converted_visitors}, events: %{value: prev_completions}} =
|
||||||
Stats.aggregate(site, prev_query, [:visitors, :events])
|
Stats.aggregate(site, prev_query, [:visitors, :events])
|
||||||
@ -362,7 +391,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
{stats, 100}
|
{stats, 100}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_top_stats(site, query, comparison_mode) do
|
defp fetch_top_stats(site, query, comparison_mode, comparison_opts) do
|
||||||
metrics =
|
metrics =
|
||||||
if query.filters["event:page"] do
|
if query.filters["event:page"] do
|
||||||
[
|
[
|
||||||
@ -389,7 +418,7 @@ defmodule PlausibleWeb.Api.StatsController do
|
|||||||
current_results = Stats.aggregate(site, query, metrics)
|
current_results = Stats.aggregate(site, query, metrics)
|
||||||
|
|
||||||
prev_results =
|
prev_results =
|
||||||
case Stats.Comparisons.compare(site, query, comparison_mode) do
|
case Stats.Comparisons.compare(site, query, comparison_mode, comparison_opts) do
|
||||||
{:ok, prev_results_query} -> Stats.aggregate(site, prev_results_query, metrics)
|
{:ok, prev_results_query} -> Stats.aggregate(site, prev_results_query, metrics)
|
||||||
{:error, :not_supported} -> nil
|
{:error, :not_supported} -> nil
|
||||||
end
|
end
|
||||||
|
@ -2,91 +2,91 @@ defmodule Plausible.Stats.ComparisonsTest do
|
|||||||
use Plausible.DataCase
|
use Plausible.DataCase
|
||||||
alias Plausible.Stats.{Query, Comparisons}
|
alias Plausible.Stats.{Query, Comparisons}
|
||||||
|
|
||||||
describe "this month" do
|
describe "with period set to this month" do
|
||||||
test "shifts back this month period" do
|
test "shifts back this month period when mode is previous_period" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"})
|
query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"})
|
||||||
now = ~N[2023-03-02 14:00:00]
|
now = ~N[2023-03-02 14:00:00]
|
||||||
|
|
||||||
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now: now)
|
||||||
|
|
||||||
assert comparison.date_range.first == ~D[2023-02-27]
|
assert comparison.date_range.first == ~D[2023-02-27]
|
||||||
assert comparison.date_range.last == ~D[2023-02-28]
|
assert comparison.date_range.last == ~D[2023-02-28]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shifts back this month period when it's the first day of the month" do
|
test "shifts back this month period when it's the first day of the month and mode is previous_period" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "month", "date" => "2023-03-01"})
|
query = Query.from(site, %{"period" => "month", "date" => "2023-03-01"})
|
||||||
now = ~N[2023-03-01 14:00:00]
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now: now)
|
||||||
|
|
||||||
assert comparison.date_range.first == ~D[2023-02-28]
|
assert comparison.date_range.first == ~D[2023-02-28]
|
||||||
assert comparison.date_range.last == ~D[2023-02-28]
|
assert comparison.date_range.last == ~D[2023-02-28]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "previous month" do
|
describe "with period set to previous month" do
|
||||||
test "shifts back using the same number of days when previous_period" do
|
test "shifts back using the same number of days when mode is previous_period" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
|
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
|
||||||
now = ~N[2023-03-01 14:00:00]
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now: now)
|
||||||
|
|
||||||
assert comparison.date_range.first == ~D[2023-01-04]
|
assert comparison.date_range.first == ~D[2023-01-04]
|
||||||
assert comparison.date_range.last == ~D[2023-01-31]
|
assert comparison.date_range.last == ~D[2023-01-31]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shifts back the full month when year_over_year" do
|
test "shifts back the full month when mode is year_over_year" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
|
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
|
||||||
now = ~N[2023-03-01 14:00:00]
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now)
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now: now)
|
||||||
|
|
||||||
assert comparison.date_range.first == ~D[2022-02-01]
|
assert comparison.date_range.first == ~D[2022-02-01]
|
||||||
assert comparison.date_range.last == ~D[2022-02-28]
|
assert comparison.date_range.last == ~D[2022-02-28]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shifts back whole month plus one day when year_over_year and a leap year" do
|
test "shifts back whole month plus one day when mode is year_over_year and a leap year" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "month", "date" => "2020-02-01"})
|
query = Query.from(site, %{"period" => "month", "date" => "2020-02-01"})
|
||||||
now = ~N[2023-03-01 14:00:00]
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now)
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now: now)
|
||||||
|
|
||||||
assert comparison.date_range.first == ~D[2019-02-01]
|
assert comparison.date_range.first == ~D[2019-02-01]
|
||||||
assert comparison.date_range.last == ~D[2019-03-01]
|
assert comparison.date_range.last == ~D[2019-03-01]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "year to date" do
|
describe "with period set to year to date" do
|
||||||
test "shifts back by the same number of days when previous_period" do
|
test "shifts back by the same number of days when mode is previous_period" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
|
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
|
||||||
now = ~N[2023-03-01 14:00:00]
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now)
|
{:ok, comparison} = Comparisons.compare(site, query, "previous_period", now: now)
|
||||||
|
|
||||||
assert comparison.date_range.first == ~D[2022-11-02]
|
assert comparison.date_range.first == ~D[2022-11-02]
|
||||||
assert comparison.date_range.last == ~D[2022-12-31]
|
assert comparison.date_range.last == ~D[2022-12-31]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shifts back by the same number of days when year_over_year" do
|
test "shifts back by the same number of days when mode is year_over_year" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
|
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
|
||||||
now = ~N[2023-03-01 14:00:00]
|
now = ~N[2023-03-01 14:00:00]
|
||||||
|
|
||||||
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now)
|
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year", now: now)
|
||||||
|
|
||||||
assert comparison.date_range.first == ~D[2022-01-01]
|
assert comparison.date_range.first == ~D[2022-01-01]
|
||||||
assert comparison.date_range.last == ~D[2022-03-01]
|
assert comparison.date_range.last == ~D[2022-03-01]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "previous year" do
|
describe "with period set to previous year" do
|
||||||
test "shifts back a whole year when year_over_year" do
|
test "shifts back a whole year when mode is year_over_year" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
|
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ defmodule Plausible.Stats.ComparisonsTest do
|
|||||||
assert comparison.date_range.last == ~D[2021-12-31]
|
assert comparison.date_range.last == ~D[2021-12-31]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shifts back a whole year when previous_period" do
|
test "shifts back a whole year when mode is previous_period" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
|
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
|
||||||
|
|
||||||
@ -107,8 +107,8 @@ defmodule Plausible.Stats.ComparisonsTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "custom" do
|
describe "with period set to custom" do
|
||||||
test "shifts back by the same number of days when previous_period" do
|
test "shifts back by the same number of days when mode is previous_period" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ defmodule Plausible.Stats.ComparisonsTest do
|
|||||||
assert comparison.date_range.last == ~D[2022-12-31]
|
assert comparison.date_range.last == ~D[2022-12-31]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shifts back to last year when year_over_year" do
|
test "shifts back to last year when mode is year_over_year" do
|
||||||
site = build(:site)
|
site = build(:site)
|
||||||
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
||||||
|
|
||||||
@ -128,4 +128,28 @@ defmodule Plausible.Stats.ComparisonsTest do
|
|||||||
assert comparison.date_range.last == ~D[2022-01-07]
|
assert comparison.date_range.last == ~D[2022-01-07]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "with mode set to custom" do
|
||||||
|
test "sets first and last dates" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
||||||
|
|
||||||
|
{:ok, comparison} =
|
||||||
|
Comparisons.compare(site, query, "custom", from: "2022-05-25", to: "2022-05-30")
|
||||||
|
|
||||||
|
assert comparison.date_range.first == ~D[2022-05-25]
|
||||||
|
assert comparison.date_range.last == ~D[2022-05-30]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validates from and to dates" do
|
||||||
|
site = build(:site)
|
||||||
|
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
|
||||||
|
|
||||||
|
assert {:error, :invalid_dates} ==
|
||||||
|
Comparisons.compare(site, query, "custom", from: "2022-05-41", to: "2022-05-30")
|
||||||
|
|
||||||
|
assert {:error, :invalid_dates} ==
|
||||||
|
Comparisons.compare(site, query, "custom", from: "2022-05-30", to: "2022-05-25")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -806,5 +806,28 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
|
|||||||
assert 1 == Enum.at(plot, 30)
|
assert 1 == Enum.at(plot, 30)
|
||||||
assert 1 == Enum.at(comparison_plot, 30)
|
assert 1 == Enum.at(comparison_plot, 30)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fill in gaps when custom comparison period is larger than original query", %{
|
||||||
|
conn: conn,
|
||||||
|
site: site
|
||||||
|
} do
|
||||||
|
populate_stats(site, [
|
||||||
|
build(:pageview, timestamp: ~N[2020-01-01 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2020-01-05 00:00:00]),
|
||||||
|
build(:pageview, timestamp: ~N[2020-01-30 00:00:00])
|
||||||
|
])
|
||||||
|
|
||||||
|
conn =
|
||||||
|
get(
|
||||||
|
conn,
|
||||||
|
"/api/stats/#{site.domain}/main-graph?period=month&date=2020-01-01&comparison=custom&compare_from=2022-01-01&compare_to=2022-06-01"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert %{"labels" => labels, "comparison_plot" => comparison_labels} =
|
||||||
|
json_response(conn, 200)
|
||||||
|
|
||||||
|
assert length(labels) == length(comparison_labels)
|
||||||
|
assert "__blank__" == List.last(labels)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user