Custom comparison mode (#2753)

This commit implements the custom comparison mode using a date range.
This commit is contained in:
Vini Brasil 2023-03-22 09:31:44 -03:00 committed by GitHub
parent 9f71bb3921
commit 87a63fe28a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 272 additions and 120 deletions

View File

@ -45,7 +45,13 @@ export function serializeQuery(query, extraQuery=[]) {
if (query.filters) { queryObj.filters = serializeFilters(query.filters) }
if (query.with_imported) { queryObj.with_imported = query.with_imported }
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)
return '?' + serialize(queryObj)

View File

@ -1,15 +1,18 @@
import React, { Fragment } from 'react'
import { withRouter } from "react-router-dom";
import { withRouter } from 'react-router-dom'
import { navigateToQuery } from './query'
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'
import * as storage from './util/storage'
import Flatpickr from 'react-flatpickr'
import { formatISO, parseUTCDate, formatDayShort } from './util/date.js'
const COMPARISON_MODES = {
'off': 'Disable comparison',
'previous_period': 'Previous period',
'year_over_year': 'Year over year',
'custom': 'Custom period',
}
const DEFAULT_COMPARISON_MODE = 'previous_period'
@ -26,6 +29,7 @@ export const getStoredComparisonMode = function(domain) {
}
const storeComparisonMode = function(domain, mode) {
if (mode == "custom") return
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 (
<Menu.Item
key={value}
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 key={value} onClick={click} disabled={disabled}>
{ render }
</Menu.Item>
)
}
@ -69,19 +87,46 @@ const ComparisonInput = function({ site, query, history }) {
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
if (!isComparisonEnabled(query.comparison)) return null
const updateMode = (key) => {
storeComparisonMode(site.domain, key)
navigateToQuery(history, query, { comparison: key })
const updateMode = (mode, from = null, to = null) => {
storeComparisonMode(site.domain, mode)
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 (
<>
<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>
<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.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" />
</Menu.Button>
<Transition
@ -93,7 +138,7 @@ const ComparisonInput = function({ site, query, history }) {
leaveFrom="transform opacity-100 scale-100"
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>
{ 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>
</Transition>
</Menu>

View File

@ -382,8 +382,6 @@ function DatePicker({query, site, history}) {
</div>
);
} if (mode === "calendar") {
const insertionDate = new Date(site.statsBegin);
const dayBeforeCreation = insertionDate - 86400000;
return (
<div className="h-0">
<Flatpickr
@ -391,7 +389,7 @@ function DatePicker({query, site, history}) {
options={{
mode: 'range',
maxDate: 'today',
minDate: dayBeforeCreation,
minDate: parseUTCDate(site.statsBegin),
showMonths: 1,
static: true,
animate: true}}

View File

@ -25,6 +25,8 @@ export function parseQuery(querystring, site) {
return {
period,
comparison,
compare_from: q.get('compare_from'),
compare_to: q.get('compare_to'),
date: q.get('date') ? parseUTCDate(q.get('date')) : nowForSite(site),
from: q.get('from') ? parseUTCDate(q.get('from')) : undefined,
to: q.get('to') ? parseUTCDate(q.get('to')) : undefined,

View File

@ -37,12 +37,12 @@ const buildTooltipData = function(query, graphData, metric, tooltipModel) {
const data = tooltipModel.dataPoints.find((dataPoint) => dataPoint.dataset.yAxisID == "y")
const comparisonData = tooltipModel.dataPoints.find((dataPoint) => dataPoint.dataset.yAxisID == "yComparison")
const label = renderBucketLabel(query, graphData, graphData.labels[data.dataIndex])
const comparisonLabel = comparisonData && renderBucketLabel(query, graphData, graphData.comparison_labels[data.dataIndex], true)
const label = data && renderBucketLabel(query, graphData, graphData.labels[data.dataIndex])
const comparisonLabel = comparisonData && renderBucketLabel(query, graphData, graphData.comparison_labels[comparisonData.dataIndex], true)
const value = data?.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 formattedValue = metricFormatter(value)
@ -84,21 +84,23 @@ export default function GraphTooltip(graphData, metric, query) {
<aside class="text-gray-100 flex flex-col">
<div class="flex justify-between items-center">
<span class="font-semibold mr-4 text-lg">${METRIC_LABELS[metric]}</span>
<div 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">&uarr;</span><span>${tooltipData.comparisonDifference}%</span>` : ""}
${tooltipData.comparisonDifference < 0 ? `<span class="font-semibold text-sm text-red-400">&darr;</span><span>${tooltipData.comparisonDifference * -1}%</span>` : ""}
${tooltipData.comparisonDifference == 0 ? `<span class="font-semibold text-sm">〰 0%</span>` : ""}
</div>
</div>` : ''}
</div>
<div class="flex flex-col">
${tooltipData.label ?
`<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>${tooltipData.label}</span>
</span>
<span class="text-base font-bold">${tooltipData.formattedValue}</span>
</div>
</div>` : ''}
${tooltipData.comparisonLabel ?
`<div class="flex flex-row justify-between items-center">

View File

@ -40,17 +40,11 @@ export const LoadingState = {
isLoadedOrRefreshing: function (state) { return [this.loaded, this.refreshing].includes(state) }
}
const buildComparisonDataset = function(comparisonPlot, presentIndex) {
const buildComparisonDataset = function(comparisonPlot) {
if (!comparisonPlot) return []
let data = [...comparisonPlot]
if (presentIndex) {
const dashedPartIncludedIndex = presentIndex + 1
data = data.slice(0, dashedPartIncludedIndex)
}
return [{
data: data,
data: comparisonPlot,
borderColor: 'rgba(60,70,110,0.2)',
pointBackgroundColor: 'rgba(60,70,110,0.2)',
pointHoverBackgroundColor: 'rgba(60, 70, 110)',
@ -98,7 +92,7 @@ export const buildDataSet = (plot, comparisonPlot, present_index, ctx, label) =>
const dataset = [
...buildMainPlotDataset(plot, present_index),
...buildDashedDataset(plot, present_index),
...buildComparisonDataset(comparisonPlot, present_index)
...buildComparisonDataset(comparisonPlot)
]
return dataset.map((item) => Object.assign(item, defaultOptions))

View File

@ -91,12 +91,12 @@ class LineGraph extends React.Component {
ticks: {
maxTicksLimit: 8,
callback: function (val, _index, _ticks) {
// realtime graph labels are not date strings
const hasMultipleYears = typeof graphData.labels[0] !== 'string' ? false :
if (this.getLabelForValue(val) == "__blank__") return ""
const hasMultipleYears =
graphData.labels
// date format: 'yyyy-mm-dd'; maps to -> 'yyyy'
.filter((date) => typeof date === 'string')
.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)
.length > 1
@ -220,26 +220,12 @@ class LineGraph extends React.Component {
onClick(e) {
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') {
navigateToQuery(
this.props.history,
this.props.query,
{
period: 'month',
date,
}
)
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,
}
)
navigateToQuery(this.props.history, this.props.query, { period: 'day', date })
}
}

View File

@ -40,6 +40,10 @@ export function formatYear(date) {
return `Year of ${date.getFullYear()}`;
}
export function formatYearShort(date) {
return date.getUTCFullYear().toString().substring(2)
}
export function formatDay(date) {
var weekday = DAYS_ABBREV[date.getDay()];
if (date.getFullYear() !== (new Date()).getFullYear()) {
@ -49,8 +53,13 @@ export function formatDay(date) {
}
}
export function formatDayShort(date) {
return `${date.getDate()} ${formatMonthShort(date)}`;
export function formatDayShort(date, includeYear = false) {
let formatted = `${date.getDate()} ${formatMonthShort(date)}`
if (includeYear) {
formatted += ` ${formatYearShort(date)}`
}
return formatted
}
export function parseUTCDate(dateString) {

View File

@ -9,40 +9,64 @@ defmodule Plausible.Stats.Comparisons do
alias Plausible.Stats
@modes ~w(previous_period year_over_year)
@modes ~w(previous_period year_over_year custom)
@disallowed_periods ~w(realtime all)
@type mode() :: String.t() | nil
@typep option() :: {:from, String.t()} | {:to, String.t()} | {:now, NaiveDateTime.t()}
@spec compare(
Plausible.Site.t(),
Stats.Query.t(),
mode(),
NaiveDateTime.t() | nil
) :: {:ok, Stats.Query.t()} | {:error, :not_supported}
def compare(
%Plausible.Site{} = site,
%Stats.Query{} = source_query,
mode,
now \\ nil
) do
@spec compare(Plausible.Site.t(), Stats.Query.t(), mode(), [option()]) ::
{:ok, Stats.Query.t()} | {:error, :not_supported} | {:error, :invalid_dates}
@doc """
Generates a comparison query based on the source query and comparison mode.
The mode parameter specifies the type of comparison and can be one of the
following:
* `"previous_period"` - shifts back the query by the same number of days the
source query has.
* `"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
now = now || Timex.now(site.timezone)
{:ok, do_compare(source_query, mode, now)}
opts = Keyword.put_new(opts, :now, Timex.now(site.timezone))
do_compare(source_query, mode, opts)
else
{:error, :not_supported}
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)
end_date = earliest(source_query.date_range.last, now) |> Date.add(-365)
range = Date.range(start_date, end_date)
%Stats.Query{source_query | date_range: range}
{:ok, %Stats.Query{source_query | date_range: range}}
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)
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)
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
defp earliest(a, b) do

View File

@ -110,21 +110,25 @@ defmodule PlausibleWeb.Api.StatsController do
end
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 =
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])
{:error, :not_supported} -> nil
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, %{
plot: plot_timeseries(timeseries_result, selected_metric),
labels: labels,
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,
interval: query.interval,
with_imported: query.include_imported,
@ -140,8 +144,20 @@ defmodule PlausibleWeb.Api.StatsController do
Enum.map(timeseries, fn row -> row[metric] || 0 end)
end
defp label_timeseries(timeseries) do
Enum.map(timeseries, & &1.date)
defp label_timeseries(main_result, nil) do
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
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
comparison_mode = params["comparison"] || "previous_period"
comparison_opts = [from: params["compare_from"], to: params["compare_to"]]
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, %{
top_stats: top_stats,
@ -245,7 +263,8 @@ defmodule PlausibleWeb.Api.StatsController do
defp fetch_top_stats(
site,
%Query{period: "realtime", filters: %{"event:goal" => _goal}} = query,
_comparison_mode
_comparison_mode,
_comparison_opts
) do
query_30m = %Query{query | period: "30m"}
@ -272,7 +291,12 @@ defmodule PlausibleWeb.Api.StatsController do
{stats, 100}
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"}
%{
@ -298,11 +322,16 @@ defmodule PlausibleWeb.Api.StatsController do
{stats, 100}
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])
{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} ->
%{visitors: %{value: prev_converted_visitors}, events: %{value: prev_completions}} =
Stats.aggregate(site, prev_query, [:visitors, :events])
@ -362,7 +391,7 @@ defmodule PlausibleWeb.Api.StatsController do
{stats, 100}
end
defp fetch_top_stats(site, query, comparison_mode) do
defp fetch_top_stats(site, query, comparison_mode, comparison_opts) do
metrics =
if query.filters["event:page"] do
[
@ -389,7 +418,7 @@ defmodule PlausibleWeb.Api.StatsController do
current_results = Stats.aggregate(site, query, metrics)
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)
{:error, :not_supported} -> nil
end

View File

@ -2,91 +2,91 @@ defmodule Plausible.Stats.ComparisonsTest do
use Plausible.DataCase
alias Plausible.Stats.{Query, Comparisons}
describe "this month" do
test "shifts back this month period" do
describe "with period set to this month" do
test "shifts back this month period when mode is previous_period" do
site = build(:site)
query = Query.from(site, %{"period" => "month", "date" => "2023-03-02"})
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.last == ~D[2023-02-28]
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)
query = Query.from(site, %{"period" => "month", "date" => "2023-03-01"})
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.last == ~D[2023-02-28]
end
end
describe "previous month" do
test "shifts back using the same number of days when previous_period" do
describe "with period set to previous month" do
test "shifts back using the same number of days when mode is previous_period" do
site = build(:site)
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
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.last == ~D[2023-01-31]
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)
query = Query.from(site, %{"period" => "month", "date" => "2023-02-01"})
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.last == ~D[2022-02-28]
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)
query = Query.from(site, %{"period" => "month", "date" => "2020-02-01"})
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.last == ~D[2019-03-01]
end
end
describe "year to date" do
test "shifts back by the same number of days when previous_period" do
describe "with period set to year to date" do
test "shifts back by the same number of days when mode is previous_period" do
site = build(:site)
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
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.last == ~D[2022-12-31]
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)
query = Query.from(site, %{"period" => "year", "date" => "2023-03-01"})
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.last == ~D[2022-03-01]
end
end
describe "previous year" do
test "shifts back a whole year when year_over_year" do
describe "with period set to previous year" do
test "shifts back a whole year when mode is year_over_year" do
site = build(:site)
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]
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)
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
@ -107,8 +107,8 @@ defmodule Plausible.Stats.ComparisonsTest do
end
end
describe "custom" do
test "shifts back by the same number of days when previous_period" do
describe "with period set to custom" do
test "shifts back by the same number of days when mode is previous_period" do
site = build(:site)
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]
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)
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]
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

View File

@ -806,5 +806,28 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
assert 1 == Enum.at(plot, 30)
assert 1 == Enum.at(comparison_plot, 30)
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