Implement "Year over Year" comparison mode (#2704)

This pull request adds support for multiple comparison modes, changes the comparison checkbox to a combobox, and implements the year over year comparison mode. The feature is still behind a feature flag.

Co-authored-by: Uku Taht <uku.taht@gmail.com>
This commit is contained in:
Vini Brasil 2023-03-07 12:52:26 -03:00 committed by GitHub
parent 874d664521
commit 8b0f0cabc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 362 additions and 90 deletions

View File

@ -45,7 +45,7 @@ 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 = true } if (query.comparison) { queryObj.comparison = query.comparison }
Object.assign(queryObj, ...extraQuery) Object.assign(queryObj, ...extraQuery)
return '?' + serialize(queryObj) return '?' + serialize(queryObj)

View File

@ -1,6 +1,14 @@
import React 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 { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'
const COMPARISON_MODES = {
'previous_period': 'Previous period',
'year_over_year': 'Year over year',
}
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all'] export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
@ -8,14 +16,46 @@ const ComparisonInput = function({ site, query, history }) {
if (!site.flags.comparisons) return null if (!site.flags.comparisons) return null
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
function update(event) { function update(key) {
navigateToQuery(history, query, { comparison: event.target.checked }) navigateToQuery(history, query, { comparison: key })
}
function renderItem({ label, value, isCurrentlySelected }) {
const labelClass = classNames("font-medium text-sm", { "font-bold disabled": isCurrentlySelected })
return (
<Menu.Item
key={value}
onClick={() => update(value)}
className="px-4 py-2 leading-tight hover:bg-gray-100 dark:text-white hover:text-gray-900 dark:hover:bg-gray-900 dark:hover:text-gray-100 flex hover:cursor-pointer">
<span className={labelClass}>{ label }</span>
</Menu.Item>
)
} }
return ( return (
<div className="flex-none mx-3"> <div className="flex ml-auto pl-2">
<input id="comparison-input" type="checkbox" onChange={update} checked={query.comparison} className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" /> <div className="w-20 sm:w-36 md:w-48 md:relative">
<label htmlFor="comparison-input" className="ml-1.5 font-medium text-xs md:text-sm text-gray-700 dark:text-white">Compare</label> <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 truncate cursor-pointer w-full">
<span>{ COMPARISON_MODES[query.comparison] || 'Compare to' }</span>
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-5" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
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>
{ renderItem({ label: "Disabled", value: false, isCurrentlySelected: !query.comparison }) }
{ Object.keys(COMPARISON_MODES).map((key) => renderItem({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison})) }
</Menu.Items>
</Transition>
</Menu>
</div>
</div> </div>
) )
} }

View File

@ -19,7 +19,7 @@ export function parseQuery(querystring, site) {
period = '30d' period = '30d'
} }
let comparison = !!q.get('comparison') let comparison = q.get('comparison')
if (COMPARISON_DISABLED_PERIODS.includes(period)) comparison = null if (COMPARISON_DISABLED_PERIODS.includes(period)) comparison = null
return { return {

View File

@ -0,0 +1,70 @@
defmodule Plausible.Stats.Comparisons do
@moduledoc """
This module provides functions for comparing query periods.
It allows you to compare a given period with a previous period or with the
same period from the previous year. For example, you can compare this month's
main graph with last month or with the same month from last year.
"""
alias Plausible.Stats
@modes ~w(previous_period year_over_year)
@disallowed_periods ~w(realtime all)
@type mode() :: String.t() | nil
@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
if valid_mode?(source_query, mode) do
now = now || Timex.now(site.timezone)
{:ok, do_compare(source_query, mode, now)}
else
{:error, :not_supported}
end
end
defp do_compare(source_query, "year_over_year", now) do
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}
end
defp do_compare(source_query, "previous_period", now) do
last = earliest(source_query.date_range.last, now)
diff_in_days = Date.diff(source_query.date_range.first, last) - 1
new_first = Date.add(source_query.date_range.first, diff_in_days)
new_last = Date.add(last, diff_in_days)
range = Date.range(new_first, new_last)
%Stats.Query{source_query | date_range: range}
end
defp earliest(a, b) do
if Date.compare(a, b) in [:eq, :lt], do: a, else: b
end
@spec valid_mode?(Stats.Query.t(), mode()) :: boolean()
@doc """
Returns whether the source query and the selected mode support comparisons.
For example, the realtime view doesn't support comparisons. Additionally, only
#{inspect(@modes)} are supported.
"""
def valid_mode?(%Stats.Query{period: period}, mode) do
mode in @modes && period not in @disallowed_periods
end
end

View File

@ -10,58 +10,7 @@ defmodule Plausible.Stats.Query do
require OpenTelemetry.Tracer, as: Tracer require OpenTelemetry.Tracer, as: Tracer
alias Plausible.Stats.{FilterParser, Interval} alias Plausible.Stats.{FilterParser, Interval}
def shift_back(%__MODULE__{period: "year"} = query, site) do @type t :: %__MODULE__{}
# Querying current year to date
{new_first, new_last} =
if Timex.compare(Timex.now(site.timezone), query.date_range.first, :year) == 0 do
diff =
Timex.diff(
Timex.beginning_of_year(Timex.now(site.timezone)),
Timex.now(site.timezone),
:days
) - 1
{query.date_range.first |> Timex.shift(days: diff),
Timex.now(site.timezone) |> Timex.to_date() |> Timex.shift(days: diff)}
else
diff = Timex.diff(query.date_range.first, query.date_range.last, :days) - 1
{query.date_range.first |> Timex.shift(days: diff),
query.date_range.last |> Timex.shift(days: diff)}
end
Map.put(query, :date_range, Date.range(new_first, new_last))
end
def shift_back(%__MODULE__{period: "month"} = query, site) do
# Querying current month to date
{new_first, new_last} =
if Timex.compare(Timex.now(site.timezone), query.date_range.first, :month) == 0 do
diff =
Timex.diff(
Timex.beginning_of_month(Timex.now(site.timezone)),
Timex.now(site.timezone),
:days
) - 1
{query.date_range.first |> Timex.shift(days: diff),
Timex.now(site.timezone) |> Timex.to_date() |> Timex.shift(days: diff)}
else
diff = Timex.diff(query.date_range.first, query.date_range.last, :days) - 1
{query.date_range.first |> Timex.shift(days: diff),
query.date_range.last |> Timex.shift(days: diff)}
end
Map.put(query, :date_range, Date.range(new_first, new_last))
end
def shift_back(query, _site) do
diff = Timex.diff(query.date_range.first, query.date_range.last, :days) - 1
new_first = query.date_range.first |> Timex.shift(days: diff)
new_last = query.date_range.last |> Timex.shift(days: diff)
Map.put(query, :date_range, Date.range(new_first, new_last))
end
def from(site, %{"period" => "realtime"} = params) do def from(site, %{"period" => "realtime"} = params) do
date = today(site.timezone) date = today(site.timezone)

View File

@ -4,6 +4,10 @@ defmodule Plausible.Stats.Timeseries do
import Plausible.Stats.{Base, Util} import Plausible.Stats.{Base, Util}
use Plausible.Stats.Fragments use Plausible.Stats.Fragments
@typep metric :: :pageviews | :visitors | :visits | :bounce_rate | :visit_duration
@typep value :: nil | integer() | float()
@type results :: nonempty_list(%{required(:date) => Date.t(), required(metric()) => value()})
@event_metrics [:visitors, :pageviews] @event_metrics [:visitors, :pageviews]
@session_metrics [:visits, :bounce_rate, :visit_duration, :views_per_visit] @session_metrics [:visits, :bounce_rate, :visit_duration, :views_per_visit]
def timeseries(site, query, metrics) do def timeseries(site, query, metrics) do

View File

@ -20,7 +20,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do
{:ok, metrics} <- parse_and_validate_metrics(params, nil, query) do {:ok, metrics} <- parse_and_validate_metrics(params, nil, query) do
results = results =
if params["compare"] == "previous_period" do if params["compare"] == "previous_period" do
prev_query = Query.shift_back(query, site) {:ok, prev_query} = Plausible.Stats.Comparisons.compare(site, query, "previous_period")
[prev_result, curr_result] = [prev_result, curr_result] =
Plausible.ClickhouseRepo.parallel_tasks([ Plausible.ClickhouseRepo.parallel_tasks([

View File

@ -3,7 +3,7 @@ defmodule PlausibleWeb.Api.StatsController do
use Plausible.Repo use Plausible.Repo
use Plug.ErrorHandler use Plug.ErrorHandler
alias Plausible.Stats alias Plausible.Stats
alias Plausible.Stats.{Query, Filters} alias Plausible.Stats.{Query, Filters, Comparisons}
require Logger require Logger
@ -115,9 +115,9 @@ defmodule PlausibleWeb.Api.StatsController do
full_intervals = build_full_intervals(query, labels) full_intervals = build_full_intervals(query, labels)
comparison_result = comparison_result =
if params["comparison"] do case Comparisons.compare(site, query, params["comparison"]) do
comparison_query = Query.shift_back(query, site) {:ok, comparison_query} -> Stats.timeseries(site, comparison_query, [selected_metric])
Stats.timeseries(site, comparison_query, [selected_metric]) {:error, :not_supported} -> nil
end end
json(conn, %{ json(conn, %{
@ -174,9 +174,10 @@ defmodule PlausibleWeb.Api.StatsController do
site = conn.assigns[:site] site = conn.assigns[:site]
with :ok <- validate_params(params) do with :ok <- validate_params(params) do
comparison_mode = params["comparison"] || "previous_period"
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) {top_stats, sample_percent} = fetch_top_stats(site, query, comparison_mode)
json(conn, %{ json(conn, %{
top_stats: top_stats, top_stats: top_stats,
@ -243,7 +244,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
) do ) do
query_30m = %Query{query | period: "30m"} query_30m = %Query{query | period: "30m"}
@ -270,7 +272,7 @@ defmodule PlausibleWeb.Api.StatsController do
{stats, 100} {stats, 100}
end end
defp fetch_top_stats(site, %Query{period: "realtime"} = query) do defp fetch_top_stats(site, %Query{period: "realtime"} = query, _comparison_mode) do
query_30m = %Query{query | period: "30m"} query_30m = %Query{query | period: "30m"}
%{ %{
@ -296,29 +298,41 @@ defmodule PlausibleWeb.Api.StatsController do
{stats, 100} {stats, 100}
end end
defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _goal}} = query) do defp fetch_top_stats(site, %Query{filters: %{"event:goal" => _goal}} = query, comparison_mode) do
total_q = Query.remove_event_filters(query, [:goal, :props]) total_q = Query.remove_event_filters(query, [:goal, :props])
prev_query = Query.shift_back(query, site)
prev_total_query = Query.shift_back(total_q, site) {prev_converted_visitors, prev_completions} =
case Stats.Comparisons.compare(site, query, comparison_mode) do
{:ok, prev_query} ->
%{visitors: %{value: prev_converted_visitors}, events: %{value: prev_completions}} =
Stats.aggregate(site, prev_query, [:visitors, :events])
{prev_converted_visitors, prev_completions}
{:error, :not_supported} ->
{nil, nil}
end
prev_unique_visitors =
case Stats.Comparisons.compare(site, total_q, comparison_mode) do
{:ok, prev_total_query} ->
site
|> Stats.aggregate(prev_total_query, [:visitors])
|> get_in([:visitors, :value])
{:error, :not_supported} ->
nil
end
%{ %{
visitors: %{value: unique_visitors} visitors: %{value: unique_visitors}
} = Stats.aggregate(site, total_q, [:visitors]) } = Stats.aggregate(site, total_q, [:visitors])
%{
visitors: %{value: prev_unique_visitors}
} = Stats.aggregate(site, prev_total_query, [:visitors])
%{ %{
visitors: %{value: converted_visitors}, visitors: %{value: converted_visitors},
events: %{value: completions} events: %{value: completions}
} = Stats.aggregate(site, query, [:visitors, :events]) } = Stats.aggregate(site, query, [:visitors, :events])
%{
visitors: %{value: prev_converted_visitors},
events: %{value: prev_completions}
} = Stats.aggregate(site, prev_query, [:visitors, :events])
conversion_rate = calculate_cr(unique_visitors, converted_visitors) conversion_rate = calculate_cr(unique_visitors, converted_visitors)
prev_conversion_rate = calculate_cr(prev_unique_visitors, prev_converted_visitors) prev_conversion_rate = calculate_cr(prev_unique_visitors, prev_converted_visitors)
@ -348,9 +362,7 @@ defmodule PlausibleWeb.Api.StatsController do
{stats, 100} {stats, 100}
end end
defp fetch_top_stats(site, query) do defp fetch_top_stats(site, query, comparison_mode) do
prev_query = Query.shift_back(query, site)
metrics = metrics =
if query.filters["event:page"] do if query.filters["event:page"] do
[ [
@ -375,7 +387,12 @@ defmodule PlausibleWeb.Api.StatsController do
end end
current_results = Stats.aggregate(site, query, metrics) current_results = Stats.aggregate(site, query, metrics)
prev_results = Stats.aggregate(site, prev_query, metrics)
prev_results =
case Stats.Comparisons.compare(site, query, comparison_mode) do
{:ok, prev_results_query} -> Stats.aggregate(site, prev_results_query, metrics)
{:error, :not_supported} -> nil
end
stats = stats =
[ [
@ -394,11 +411,11 @@ defmodule PlausibleWeb.Api.StatsController do
defp top_stats_entry(current_results, prev_results, name, key) do defp top_stats_entry(current_results, prev_results, name, key) do
if current_results[key] do if current_results[key] do
%{ value = get_in(current_results, [key, :value])
name: name, prev_value = get_in(prev_results, [key, :value])
value: current_results[key][:value], change = prev_value && calculate_change(key, prev_value, value)
change: calculate_change(key, prev_results[key][:value], current_results[key][:value])
} %{name: name, value: value, change: change}
end end
end end

View File

@ -49,7 +49,7 @@ defmodule Plausible.Workers.SendEmailReport do
end end
defp send_report(email, site, name, unsubscribe_link, query) do defp send_report(email, site, name, unsubscribe_link, query) do
prev_query = Query.shift_back(query, site) {:ok, prev_query} = Stats.Comparisons.compare(site, query, "previous_period")
curr_period = Stats.aggregate(site, query, [:pageviews, :visitors, :bounce_rate]) curr_period = Stats.aggregate(site, query, [:pageviews, :visitors, :bounce_rate])
prev_period = Stats.aggregate(site, prev_query, [:pageviews, :visitors, :bounce_rate]) prev_period = Stats.aggregate(site, prev_query, [:pageviews, :visitors, :bounce_rate])

View File

@ -0,0 +1,131 @@
defmodule Plausible.Stats.ComparisonsTest do
use Plausible.DataCase
alias Plausible.Stats.{Query, Comparisons}
describe "this month" do
test "shifts back this month 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)
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
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)
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
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)
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
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)
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
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)
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
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)
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
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)
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
site = build(:site)
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year")
assert comparison.date_range.first == ~D[2021-01-01]
assert comparison.date_range.last == ~D[2021-12-31]
end
test "shifts back a whole year when previous_period" do
site = build(:site)
query = Query.from(site, %{"period" => "year", "date" => "2022-03-02"})
{:ok, comparison} = Comparisons.compare(site, query, "previous_period")
assert comparison.date_range.first == ~D[2021-01-01]
assert comparison.date_range.last == ~D[2021-12-31]
end
end
describe "custom" do
test "shifts back by the same number of days when previous_period" do
site = build(:site)
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
{:ok, comparison} = Comparisons.compare(site, query, "previous_period")
assert comparison.date_range.first == ~D[2022-12-25]
assert comparison.date_range.last == ~D[2022-12-31]
end
test "shifts back to last year when year_over_year" do
site = build(:site)
query = Query.from(site, %{"period" => "custom", "date" => "2023-01-01,2023-01-07"})
{:ok, comparison} = Comparisons.compare(site, query, "year_over_year")
assert comparison.date_range.first == ~D[2022-01-01]
assert comparison.date_range.last == ~D[2022-01-07]
end
end
end

View File

@ -591,4 +591,65 @@ defmodule PlausibleWeb.Api.StatsController.MainGraphTest do
} }
end end
end end
describe "GET /api/stats/main-graph - comparisons" do
setup [:create_user, :log_in, :create_new_site, :add_imported_data]
test "returns past month stats when period=30d and comparison=previous_period", %{
conn: conn,
site: site
} do
conn =
get(conn, "/api/stats/#{site.domain}/main-graph?period=30d&comparison=previous_period")
assert %{"labels" => labels, "comparison_labels" => comparison_labels} =
json_response(conn, 200)
{:ok, first} = Timex.today() |> Timex.shift(days: -30) |> Timex.format("{ISOdate}")
{:ok, last} = Timex.today() |> Timex.format("{ISOdate}")
assert List.first(labels) == first
assert List.last(labels) == last
{:ok, first} = Timex.today() |> Timex.shift(days: -61) |> Timex.format("{ISOdate}")
{:ok, last} = Timex.today() |> Timex.shift(days: -31) |> Timex.format("{ISOdate}")
assert List.first(comparison_labels) == first
assert List.last(comparison_labels) == last
end
test "returns past year stats when period=month and comparison=year_over_year", %{
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]),
build(:pageview, timestamp: ~N[2020-01-31 00:00:00]),
build(:pageview, timestamp: ~N[2019-01-01 00:00:00]),
build(:pageview, timestamp: ~N[2019-01-01 00:00:00]),
build(:pageview, timestamp: ~N[2019-01-05 00:00:00]),
build(:pageview, timestamp: ~N[2019-01-05 00:00:00]),
build(:pageview, timestamp: ~N[2019-01-31 00:00:00])
])
conn =
get(
conn,
"/api/stats/#{site.domain}/main-graph?period=month&date=2020-01-01&comparison=year_over_year"
)
assert %{"plot" => plot, "comparison_plot" => comparison_plot} = json_response(conn, 200)
assert 1 == Enum.at(plot, 0)
assert 2 == Enum.at(comparison_plot, 0)
assert 1 == Enum.at(plot, 4)
assert 2 == Enum.at(comparison_plot, 4)
assert 1 == Enum.at(plot, 30)
assert 1 == Enum.at(comparison_plot, 30)
end
end
end end