mirror of
https://github.com/plausible/analytics.git
synced 2025-01-03 15:17:58 +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.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)
|
||||
|
@ -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>
|
||||
|
@ -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}}
|
||||
|
@ -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,
|
||||
|
@ -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">↑</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">〰 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">
|
||||
|
@ -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))
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user