lookout-ahoy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +99 -0
- data/Rakefile +24 -0
- data/app/assets/images/lookout/apple-touch-icon.png +0 -0
- data/app/assets/images/lookout/favicon-16x16.png +0 -0
- data/app/assets/images/lookout/favicon-32x32.png +0 -0
- data/app/assets/images/lookout/logo.png +0 -0
- data/app/assets/images/lookout/safari-pinned-tab.png +0 -0
- data/app/assets/images/lookout/safari-pinned-tab.svg +199 -0
- data/app/assets/javascript/lookout/application.js +2 -0
- data/app/assets/javascript/lookout/controllers/application.js +9 -0
- data/app/assets/javascript/lookout/controllers/application_controller.js +33 -0
- data/app/assets/javascript/lookout/controllers/combobox_controller.js +371 -0
- data/app/assets/javascript/lookout/controllers/details_modal_controller.js +18 -0
- data/app/assets/javascript/lookout/controllers/dropdown_label_controller.js +39 -0
- data/app/assets/javascript/lookout/controllers/filter/item_controller.js +12 -0
- data/app/assets/javascript/lookout/controllers/filter_form_controller.js +13 -0
- data/app/assets/javascript/lookout/controllers/filter_modal_controller.js +45 -0
- data/app/assets/javascript/lookout/controllers/frame_link_controller.js +20 -0
- data/app/assets/javascript/lookout/controllers/funnel_chart_controller.js +159 -0
- data/app/assets/javascript/lookout/controllers/index.js +4 -0
- data/app/assets/javascript/lookout/controllers/interval_controller.js +15 -0
- data/app/assets/javascript/lookout/controllers/line_chart_controller.js +251 -0
- data/app/assets/javascript/lookout/controllers/predicate_select_controller.js +10 -0
- data/app/assets/javascript/lookout/controllers/properties_controller.js +8 -0
- data/app/assets/javascript/lookout/controllers/property_filter_controller.js +45 -0
- data/app/assets/javascript/lookout/controllers/realtime_controller.js +30 -0
- data/app/assets/javascript/lookout/controllers/sparkline_controller.js +64 -0
- data/app/assets/javascript/lookout/controllers/tile_controller.js +33 -0
- data/app/assets/javascript/lookout/controllers/toggle_controller.js +17 -0
- data/app/assets/javascript/lookout/helpers/chart_utils.js +156 -0
- data/app/assets/javascript/lookout/helpers/countries.js +2261 -0
- data/app/assets/javascript/lookout/helpers/number_formatters.js +55 -0
- data/app/assets/manifest/lookout/manifest.js +2 -0
- data/app/components/lookout/combobox_component.html.erb +33 -0
- data/app/components/lookout/combobox_component.rb +13 -0
- data/app/components/lookout/comparison_link_component.html.erb +17 -0
- data/app/components/lookout/comparison_link_component.rb +44 -0
- data/app/components/lookout/dropdown_button_component.html.erb +16 -0
- data/app/components/lookout/dropdown_button_component.rb +14 -0
- data/app/components/lookout/dropdown_link_component.html.erb +17 -0
- data/app/components/lookout/dropdown_link_component.rb +19 -0
- data/app/components/lookout/filter/dropdown_component.html.erb +50 -0
- data/app/components/lookout/filter/dropdown_component.rb +51 -0
- data/app/components/lookout/filter/modal_component.html.erb +16 -0
- data/app/components/lookout/filter/modal_component.rb +13 -0
- data/app/components/lookout/filter/select_component.html.erb +25 -0
- data/app/components/lookout/filter/select_component.rb +64 -0
- data/app/components/lookout/filter/tag_component.html.erb +13 -0
- data/app/components/lookout/filter/tag_component.rb +14 -0
- data/app/components/lookout/filter/tag_container_component.html.erb +4 -0
- data/app/components/lookout/filter/tag_container_component.rb +6 -0
- data/app/components/lookout/previous_next_component.html.erb +8 -0
- data/app/components/lookout/previous_next_component.rb +11 -0
- data/app/components/lookout/stats/comparable_container_component.html.erb +25 -0
- data/app/components/lookout/stats/comparable_container_component.rb +103 -0
- data/app/components/lookout/stats/container_component.html.erb +23 -0
- data/app/components/lookout/stats/container_component.rb +28 -0
- data/app/components/lookout/sticky_nav_component.html.erb +32 -0
- data/app/components/lookout/sticky_nav_component.rb +24 -0
- data/app/components/lookout/table_component.html.erb +16 -0
- data/app/components/lookout/table_component.rb +48 -0
- data/app/components/lookout/tables/devices_table_component.rb +11 -0
- data/app/components/lookout/tables/dynamic_table.rb +13 -0
- data/app/components/lookout/tables/dynamic_table_component.rb +207 -0
- data/app/components/lookout/tables/goals_table_component.rb +17 -0
- data/app/components/lookout/tables/header_component.html.erb +6 -0
- data/app/components/lookout/tables/header_component.rb +18 -0
- data/app/components/lookout/tables/headers/header_component.html.erb +5 -0
- data/app/components/lookout/tables/headers/header_component.rb +16 -0
- data/app/components/lookout/tables/properties_table_component.rb +27 -0
- data/app/components/lookout/tables/row_component.html.erb +4 -0
- data/app/components/lookout/tables/rows/row_component.html.erb +6 -0
- data/app/components/lookout/tables/rows/row_component.rb +40 -0
- data/app/components/lookout/tile_component.html.erb +24 -0
- data/app/components/lookout/tile_component.rb +24 -0
- data/app/components/lookout/tooltip_component.html.erb +3 -0
- data/app/components/lookout/tooltip_component.rb +18 -0
- data/app/controllers/lookout/application_controller.rb +83 -0
- data/app/controllers/lookout/campaigns_controller.rb +19 -0
- data/app/controllers/lookout/devices_controller.rb +20 -0
- data/app/controllers/lookout/entry_pages_controller.rb +19 -0
- data/app/controllers/lookout/exit_pages_controller.rb +19 -0
- data/app/controllers/lookout/exports_controller.rb +14 -0
- data/app/controllers/lookout/filters/base_controller.rb +15 -0
- data/app/controllers/lookout/filters/goals_controller.rb +9 -0
- data/app/controllers/lookout/filters/locations_controller.rb +11 -0
- data/app/controllers/lookout/filters/operating_systems/names_controller.rb +13 -0
- data/app/controllers/lookout/filters/operating_systems/versions_controller.rb +13 -0
- data/app/controllers/lookout/filters/pages/actions_controller.rb +13 -0
- data/app/controllers/lookout/filters/pages/entry_pages_controller.rb +14 -0
- data/app/controllers/lookout/filters/pages/exit_pages_controller.rb +15 -0
- data/app/controllers/lookout/filters/properties/names_controller.rb +29 -0
- data/app/controllers/lookout/filters/properties/values_controller.rb +15 -0
- data/app/controllers/lookout/filters/screens_controller.rb +11 -0
- data/app/controllers/lookout/filters/sources_controller.rb +11 -0
- data/app/controllers/lookout/filters/utms_controller.rb +10 -0
- data/app/controllers/lookout/funnels_controller.rb +8 -0
- data/app/controllers/lookout/goals_controller.rb +7 -0
- data/app/controllers/lookout/locations/cities_controller.rb +22 -0
- data/app/controllers/lookout/locations/countries_controller.rb +22 -0
- data/app/controllers/lookout/locations/maps_controller.rb +24 -0
- data/app/controllers/lookout/locations/regions_controller.rb +22 -0
- data/app/controllers/lookout/properties_controller.rb +73 -0
- data/app/controllers/lookout/realtimes_controller.rb +7 -0
- data/app/controllers/lookout/roots_controller.rb +6 -0
- data/app/controllers/lookout/sources_controller.rb +21 -0
- data/app/controllers/lookout/stats/base_controller.rb +148 -0
- data/app/controllers/lookout/stats/bounce_rates_controller.rb +12 -0
- data/app/controllers/lookout/stats/total_pageviews_controller.rb +10 -0
- data/app/controllers/lookout/stats/total_visits_controller.rb +10 -0
- data/app/controllers/lookout/stats/unique_visitors_controller.rb +11 -0
- data/app/controllers/lookout/stats/views_per_visits_controller.rb +11 -0
- data/app/controllers/lookout/stats/visit_durations_controller.rb +10 -0
- data/app/controllers/lookout/stats_controller.rb +7 -0
- data/app/controllers/lookout/top_pages_controller.rb +20 -0
- data/app/decorators/lookout/application_decorator.rb +58 -0
- data/app/decorators/lookout/campaign_decorator.rb +27 -0
- data/app/decorators/lookout/city_decorator.rb +24 -0
- data/app/decorators/lookout/country_decorator.rb +38 -0
- data/app/decorators/lookout/device_decorator.rb +27 -0
- data/app/decorators/lookout/entry_page_decorator.rb +7 -0
- data/app/decorators/lookout/exit_page_decorator.rb +7 -0
- data/app/decorators/lookout/page_decorator.rb +27 -0
- data/app/decorators/lookout/region_decorator.rb +28 -0
- data/app/decorators/lookout/source_decorator.rb +27 -0
- data/app/decorators/lookout/top_page_decorator.rb +7 -0
- data/app/helpers/lookout/application_helper.rb +124 -0
- data/app/models/concerns/lookout/compare_mode.rb +19 -0
- data/app/models/concerns/lookout/limitable.rb +17 -0
- data/app/models/concerns/lookout/range_options.rb +8 -0
- data/app/models/lookout/comparison_mode.rb +72 -0
- data/app/models/lookout/export.rb +48 -0
- data/app/models/lookout/filter_parser.rb +82 -0
- data/app/models/lookout/range_from_params.rb +78 -0
- data/app/models/lookout/rangeable.rb +7 -0
- data/app/models/lookout/widget.rb +15 -0
- data/app/presenters/lookout/dashboard_presenter.rb +53 -0
- data/app/presenters/lookout/funnel_presenter.rb +75 -0
- data/app/presenters/lookout/goals_presenter.rb +72 -0
- data/app/queries/concerns/lookout/comparable_queries.rb +25 -0
- data/app/queries/concerns/lookout/comparable_query.rb +152 -0
- data/app/queries/concerns/lookout/lazy_comparable_query.rb +42 -0
- data/app/queries/lookout/application_query.rb +186 -0
- data/app/queries/lookout/campaign_query.rb +14 -0
- data/app/queries/lookout/city_query.rb +14 -0
- data/app/queries/lookout/country_query.rb +10 -0
- data/app/queries/lookout/device_query.rb +10 -0
- data/app/queries/lookout/entry_pages_query.rb +18 -0
- data/app/queries/lookout/event_query.rb +42 -0
- data/app/queries/lookout/exit_pages_query.rb +19 -0
- data/app/queries/lookout/region_query.rb +14 -0
- data/app/queries/lookout/source_query.rb +11 -0
- data/app/queries/lookout/stats/average_views_per_visit_query.rb +20 -0
- data/app/queries/lookout/stats/average_visit_duration_query.rb +34 -0
- data/app/queries/lookout/stats/base_query.rb +18 -0
- data/app/queries/lookout/stats/bounce_rates_query.rb +33 -0
- data/app/queries/lookout/stats/total_pageviews_query.rb +9 -0
- data/app/queries/lookout/stats/total_visitors_query.rb +9 -0
- data/app/queries/lookout/stats/unique_visitors_query.rb +9 -0
- data/app/queries/lookout/stats/views_per_visit_query.rb +17 -0
- data/app/queries/lookout/stats/visit_duration_query.rb +19 -0
- data/app/queries/lookout/top_page_query.rb +13 -0
- data/app/queries/lookout/visit_query.rb +42 -0
- data/app/views/lookout/campaigns/index.html+details.erb +4 -0
- data/app/views/lookout/campaigns/index.html.erb +3 -0
- data/app/views/lookout/devices/_table.html.erb +2 -0
- data/app/views/lookout/devices/index.html+details.erb +4 -0
- data/app/views/lookout/devices/index.html.erb +3 -0
- data/app/views/lookout/entry_pages/index.html+details.erb +4 -0
- data/app/views/lookout/entry_pages/index.html.erb +3 -0
- data/app/views/lookout/exit_pages/index.html+details.erb +4 -0
- data/app/views/lookout/exit_pages/index.html.erb +3 -0
- data/app/views/lookout/funnels/index.html.erb +7 -0
- data/app/views/lookout/funnels/show.html.erb +15 -0
- data/app/views/lookout/goals/index.html.erb +4 -0
- data/app/views/lookout/layouts/application.html.erb +144 -0
- data/app/views/lookout/layouts/shared/_tile_loader.html.erb +5 -0
- data/app/views/lookout/layouts/shared/_widget_disabled.html+details.erb +3 -0
- data/app/views/lookout/layouts/shared/_widget_disabled.html.erb +3 -0
- data/app/views/lookout/locations/cities/index.html+details.erb +4 -0
- data/app/views/lookout/locations/cities/index.html.erb +3 -0
- data/app/views/lookout/locations/countries/index.html+details.erb +5 -0
- data/app/views/lookout/locations/countries/index.html.erb +3 -0
- data/app/views/lookout/locations/maps/_simple_map.html.erb +26 -0
- data/app/views/lookout/locations/maps/show.html.erb +106 -0
- data/app/views/lookout/locations/regions/index.html+details.erb +4 -0
- data/app/views/lookout/locations/regions/index.html.erb +3 -0
- data/app/views/lookout/properties/_form.html.erb +6 -0
- data/app/views/lookout/properties/index.html.erb +3 -0
- data/app/views/lookout/properties/show.html.erb +6 -0
- data/app/views/lookout/realtimes/show.html.erb +9 -0
- data/app/views/lookout/roots/_filters.html.erb +80 -0
- data/app/views/lookout/roots/show.html.erb +191 -0
- data/app/views/lookout/sources/index.html+details.erb +4 -0
- data/app/views/lookout/sources/index.html.erb +3 -0
- data/app/views/lookout/stats/base/index.html.erb +40 -0
- data/app/views/lookout/stats/show.html.erb +15 -0
- data/app/views/lookout/top_pages/index.html+details.erb +4 -0
- data/app/views/lookout/top_pages/index.html.erb +3 -0
- data/config/routes.rb +69 -0
- data/lib/generators/lookout/install_generator.rb +31 -0
- data/lib/generators/lookout/migration_generator.rb +21 -0
- data/lib/generators/lookout/templates/config.rb.tt +185 -0
- data/lib/generators/lookout/templates/migration.rb.tt +7 -0
- data/lib/lookout/active_record.rb +108 -0
- data/lib/lookout/ahoy/event_methods.rb +75 -0
- data/lib/lookout/ahoy/visit_methods.rb +24 -0
- data/lib/lookout/configuration.rb +58 -0
- data/lib/lookout/database_adapter.rb +168 -0
- data/lib/lookout/engine.rb +47 -0
- data/lib/lookout/filter_configuration/filter.rb +16 -0
- data/lib/lookout/filter_configuration/filter_collection.rb +48 -0
- data/lib/lookout/filters_configuration.rb +77 -0
- data/lib/lookout/funnels.rb +44 -0
- data/lib/lookout/goals.rb +51 -0
- data/lib/lookout/period_collection.rb +115 -0
- data/lib/lookout/predicate_label.rb +7 -0
- data/lib/lookout/railtie.rb +9 -0
- data/lib/lookout/version.rb +3 -0
- data/lib/lookout.rb +78 -0
- metadata +673 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const THOUSAND = 1000
|
|
2
|
+
const HUNDRED_THOUSAND = 100000
|
|
3
|
+
const MILLION = 1000000
|
|
4
|
+
const HUNDRED_MILLION = 100000000
|
|
5
|
+
const BILLION = 1000000000
|
|
6
|
+
const HUNDRED_BILLION = 100000000000
|
|
7
|
+
const TRILLION = 1000000000000
|
|
8
|
+
|
|
9
|
+
export function numberFormatter(num) {
|
|
10
|
+
if (num >= THOUSAND && num < MILLION) {
|
|
11
|
+
const thousands = num / THOUSAND
|
|
12
|
+
if (thousands === Math.floor(thousands) || num >= HUNDRED_THOUSAND) {
|
|
13
|
+
return Math.floor(thousands) + 'k'
|
|
14
|
+
} else {
|
|
15
|
+
return (Math.floor(thousands * 10) / 10) + 'k'
|
|
16
|
+
}
|
|
17
|
+
} else if (num >= MILLION && num < BILLION) {
|
|
18
|
+
const millions = num / MILLION
|
|
19
|
+
if (millions === Math.floor(millions) || num >= HUNDRED_MILLION) {
|
|
20
|
+
return Math.floor(millions) + 'M'
|
|
21
|
+
} else {
|
|
22
|
+
return (Math.floor(millions * 10) / 10) + 'M'
|
|
23
|
+
}
|
|
24
|
+
} else if (num >= BILLION && num < TRILLION) {
|
|
25
|
+
const billions = num / BILLION
|
|
26
|
+
if (billions === Math.floor(billions) || num >= HUNDRED_BILLION) {
|
|
27
|
+
return Math.floor(billions) + 'B'
|
|
28
|
+
} else {
|
|
29
|
+
return (Math.floor(billions * 10) / 10) + 'B'
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
return num
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pad(num, size) {
|
|
37
|
+
return ('000' + num).slice(size * -1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function durationFormatter(duration) {
|
|
41
|
+
const hours = Math.floor(duration / 60 / 60)
|
|
42
|
+
const minutes = Math.floor(duration / 60) % 60
|
|
43
|
+
const seconds = Math.floor(duration - (minutes * 60) - (hours * 60 * 60))
|
|
44
|
+
if (hours > 0) {
|
|
45
|
+
return `${hours}h ${minutes}m ${seconds}s`
|
|
46
|
+
} else if (minutes > 0) {
|
|
47
|
+
return `${minutes}m ${pad(seconds, 2)}s`
|
|
48
|
+
} else {
|
|
49
|
+
return `${seconds}s`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function percentageFormatter(float) {
|
|
54
|
+
return Number(float/100).toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2});
|
|
55
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div data-controller="combobox"
|
|
2
|
+
data-combobox-single-option-value="<%= !@multiple %>"
|
|
3
|
+
data-combobox-placeholder-value="Select an option..."
|
|
4
|
+
data-combobox-is-disabled-value="false"
|
|
5
|
+
data-combobox-url-value="<%= @url %>"
|
|
6
|
+
data-combobox-disabled-value="<%= @disabled %>"
|
|
7
|
+
data-combobox-query-value="q[<%= @column %>_i_cont]"
|
|
8
|
+
data-combobox-selected-value="<%= @value.map { |value| { text: value, value: value } }.to_json %>"
|
|
9
|
+
class=" w-full "
|
|
10
|
+
>
|
|
11
|
+
<div data-action="click->combobox#toggleOpen" data-combobox-target="box"
|
|
12
|
+
class="
|
|
13
|
+
bg-gray-900 ring-0 focus-within:ring-0 focus-within:ring-0 focus:ring-0 focus:outline-none w-full rounded-md py-2 text-sm
|
|
14
|
+
w-full"
|
|
15
|
+
data-combobox-box-open-class="border-secondary-500 ring-1 ring-secondary-500">
|
|
16
|
+
<select data-combobox-target="select" style="display:none;"
|
|
17
|
+
data-predicate-select-target="select"
|
|
18
|
+
name="<%= @name %>"
|
|
19
|
+
id="<%= @select_html[:id] || "filter_#{@name}" %>"
|
|
20
|
+
<% @select_html.each do |k,v| %><%=k %>="<%=v %>"<% end %>
|
|
21
|
+
><% @value.each do |value| %><option value="<%= value %>" selected><%= value %></option><% end %></select>
|
|
22
|
+
<div data-combobox-target="selected" class="px-2" style="display:none;"></div>
|
|
23
|
+
<input data-combobox-target="input"
|
|
24
|
+
data-action="input->combobox#onInput"
|
|
25
|
+
class="input input-sm input-ghost w-full inline-block rounded-md focus:outline-none focus:ring-0 focus:bg-gray-900"
|
|
26
|
+
type="text"
|
|
27
|
+
placeholder="Select an option...">
|
|
28
|
+
</div>
|
|
29
|
+
<ul data-combobox-target="list"
|
|
30
|
+
class="z-50 absolute mt-1 max-h-60 w-full overflow-auto rounded-md py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm bg-gray-800 text-gray-350"
|
|
31
|
+
style="display: none;">
|
|
32
|
+
</ul>
|
|
33
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
class ComboboxComponent < ViewComponent::Base
|
|
3
|
+
def initialize(name:, multiple: false, disabled: false, column:, url:, value: [], select_html: {})
|
|
4
|
+
@name = name
|
|
5
|
+
@multiple = multiple
|
|
6
|
+
@column = column
|
|
7
|
+
@url = url
|
|
8
|
+
@value = Array(value)
|
|
9
|
+
@select_html = select_html
|
|
10
|
+
@disabled = disabled
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="dropdown dropdown-end" data-controller='dropdown-label'>
|
|
2
|
+
<label
|
|
3
|
+
tabindex="0"
|
|
4
|
+
class="cursor-pointer flex <%= classes %>"
|
|
5
|
+
data-action='click->dropdown-label#removeHidden'
|
|
6
|
+
>
|
|
7
|
+
<span data-dropdown-label-target="label"><%= title %></span>
|
|
8
|
+
|
|
9
|
+
</label>
|
|
10
|
+
<ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" data-dropdown-label-target="close">
|
|
11
|
+
<% links.each do |link| %>
|
|
12
|
+
<li data-action="click->dropdown-label#setLabel">
|
|
13
|
+
<%= link %>
|
|
14
|
+
<li>
|
|
15
|
+
<% end %>
|
|
16
|
+
</ul>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lookout::ComparisonLinkComponent < ViewComponent::Base
|
|
4
|
+
include ::Lookout::CompareMode
|
|
5
|
+
include ::Lookout::RangeOptions
|
|
6
|
+
include ::Lookout::Rangeable
|
|
7
|
+
|
|
8
|
+
renders_many :links
|
|
9
|
+
renders_one :header
|
|
10
|
+
|
|
11
|
+
attr_reader :title, :classes
|
|
12
|
+
def initialize(title: "", classes: "btn btn-sm btn-base-100 no-underline hover:bg-base-100")
|
|
13
|
+
@classes = classes
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# cheating
|
|
17
|
+
def title
|
|
18
|
+
self.with_link_content(options_for_option)
|
|
19
|
+
|
|
20
|
+
comparison_mode.label
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render?
|
|
24
|
+
comparison_mode.enabled?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def options_for_option
|
|
28
|
+
[
|
|
29
|
+
(link_to "Custom period", "javascript:customComparisonModal.showModal()", class: selected(:custom)),
|
|
30
|
+
(link_to "Year-over-year", Lookout::Engine.routes.url_helpers.root_path(**helpers.search_params.merge(comparison: :year)), class: selected(:year)),
|
|
31
|
+
(link_to "Previous period", Lookout::Engine.routes.url_helpers.root_path(**helpers.search_params.merge(comparison: :previous)), class: selected(:previous, :true)),
|
|
32
|
+
(link_to "Disable Comparison", Lookout::Engine.routes.url_helpers.root_path(**helpers.search_params.merge(comparison: false))),
|
|
33
|
+
|
|
34
|
+
].reverse.join.html_safe
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def selected(*types)
|
|
40
|
+
return "font-bold" if comparison_mode.type.in?(types)
|
|
41
|
+
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="dropdown dropdown-end">
|
|
2
|
+
<label
|
|
3
|
+
tabindex="0"
|
|
4
|
+
class="btn btn-ghost dark:hover:bg-neutral flex"
|
|
5
|
+
>
|
|
6
|
+
<%= header_icon %>
|
|
7
|
+
<span><%= title %></span>
|
|
8
|
+
</label>
|
|
9
|
+
<ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box">
|
|
10
|
+
<% options.each do |option| %>
|
|
11
|
+
<li>
|
|
12
|
+
<%= option %>
|
|
13
|
+
<li>
|
|
14
|
+
<% end %>
|
|
15
|
+
</ul>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="dropdown dropdown-end" data-controller='dropdown-label'>
|
|
2
|
+
<label
|
|
3
|
+
tabindex="0"
|
|
4
|
+
class="cursor-pointer flex <%= classes %>"
|
|
5
|
+
data-action='click->dropdown-label#removeHidden'
|
|
6
|
+
>
|
|
7
|
+
<span data-dropdown-label-target="label"><%= title %></span>
|
|
8
|
+
|
|
9
|
+
</label>
|
|
10
|
+
<ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" data-dropdown-label-target="close">
|
|
11
|
+
<% options.each do |option| %>
|
|
12
|
+
<li data-action="click->dropdown-label#setLabel">
|
|
13
|
+
<%= option %>
|
|
14
|
+
<li>
|
|
15
|
+
<% end %>
|
|
16
|
+
</ul>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lookout::DropdownLinkComponent < ViewComponent::Base
|
|
4
|
+
renders_many :options
|
|
5
|
+
renders_one :header
|
|
6
|
+
|
|
7
|
+
def initialize(title:, classes: nil)
|
|
8
|
+
@title = title
|
|
9
|
+
@classes = classes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def link_to(name, url, **options)
|
|
13
|
+
self.with_option_content view_context.link_to name, url, options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :title, :classes
|
|
19
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div class="dropdown dropdown-end">
|
|
2
|
+
<label
|
|
3
|
+
tabindex="0"
|
|
4
|
+
class="btn btn-sm btn-ghost dark:hover:bg-neutral flex"
|
|
5
|
+
>
|
|
6
|
+
<%= header_icon %>
|
|
7
|
+
<span><%= title %></span>
|
|
8
|
+
</label>
|
|
9
|
+
|
|
10
|
+
<ul class="w-72 dropdown-content z-[1] p-2 shadow bg-base-100 rounded-box" data-controller='toggle' data-toggle-enable-value="<%= advanced_filter_menu? %>">
|
|
11
|
+
<% if advanced_filter_menu? %>
|
|
12
|
+
<div id="advanced-filters" class="w-full text-sm leading-tight" >
|
|
13
|
+
<button class='w-full cursor-pointer block pl-4 pt-1 text-left hover:text-primary' data-action="click->toggle#trigger">+ Add filter</button>
|
|
14
|
+
<div class="divider my-1"></div>
|
|
15
|
+
<% filters.each do |_, filter| %>
|
|
16
|
+
<li class='flex flex-inline px-4 items-center' data-toggle-target='toggleable'>
|
|
17
|
+
<button title="Edit filter: <%= filter.title %>"
|
|
18
|
+
class="flex w-full justify-between link no-underline items-center group <% if filter.modal %>cursor-pointer<% else %>cursor-text<% end %> text-left"
|
|
19
|
+
onclick="<% if filter.modal %><%= filter.modal %>.showModal() <% end %>">
|
|
20
|
+
<span class="truncate w-48 ">
|
|
21
|
+
<%= filter.column.titleize %> <%= filter.predicate.titleize %>
|
|
22
|
+
<%= filter.values.to_sentence %>
|
|
23
|
+
</span>
|
|
24
|
+
<% if filter.modal %>
|
|
25
|
+
<span class="group-hover:text-primary hover:text-primary ">
|
|
26
|
+
<%= edit_icon %>
|
|
27
|
+
</span>
|
|
28
|
+
<% end %>
|
|
29
|
+
</button>
|
|
30
|
+
<a title="Remove filter: <%= filter.title %>"
|
|
31
|
+
class="hover:text-primary link no-underline pl-2" href="<%= filter.url %>">
|
|
32
|
+
<%= remove_icon %>
|
|
33
|
+
</a>
|
|
34
|
+
</li>
|
|
35
|
+
<div class="divider my-1" data-toggle-target='toggleable'></div>
|
|
36
|
+
<% end %>
|
|
37
|
+
<li data-toggle-target='toggleable'>
|
|
38
|
+
<a class="block mx-auto pl-4 pb-1 " href="<%= Lookout::Engine.app.url_helpers.root_path %>">Clear all filters</a>
|
|
39
|
+
</li>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
|
|
43
|
+
<ul id="core-filters" class="menu <%= 'hidden' if advanced_filter_menu? %> pt-0" data-toggle-target='toggleable'>
|
|
44
|
+
<% Lookout.config.filters.each do |label, filter_group| %>
|
|
45
|
+
<li><button onClick="<%= filter_group.modal_name %>.showModal()" class='link no-underline' data-action="click->toggle#trigger"><%= label %></button></li>
|
|
46
|
+
<% end %>
|
|
47
|
+
<li><button onClick="customPropertyFilterModal.showModal()" class='link no-underline' data-action="click->toggle#trigger">Property</button></li>
|
|
48
|
+
</ul>
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lookout::Filter::DropdownComponent < ViewComponent::Base
|
|
4
|
+
def initialize(filters:)
|
|
5
|
+
@filters = filters
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
attr_reader :filters
|
|
11
|
+
|
|
12
|
+
def header_icon
|
|
13
|
+
advanced_filter_menu? ? filters_icon : magnifier_icon
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def title
|
|
17
|
+
advanced_filter_menu? ? "#{filters.size} Filters" : 'Filter'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def advanced_filter_menu?
|
|
21
|
+
filter_categories.count >= ::Lookout::FilterParser::FILTER_MENU_MAX_SIZE
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def filter_categories
|
|
25
|
+
filters.values.map(&:values).flatten
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def magnifier_icon
|
|
29
|
+
%Q(
|
|
30
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-ml-1 mr-1 h-4 w-4 md:h-4 md:w-4"><path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"></path></svg>
|
|
31
|
+
).html_safe
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def filters_icon
|
|
35
|
+
%Q(
|
|
36
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-ml-1 mr-1 h-4 w-4"><path d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z"></path></svg>
|
|
37
|
+
).html_safe
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def edit_icon
|
|
41
|
+
%Q(
|
|
42
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4 ml-1 cursor-pointer"><path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"></path><path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"></path></svg>
|
|
43
|
+
).html_safe
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remove_icon
|
|
47
|
+
%Q(
|
|
48
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path></svg>
|
|
49
|
+
).html_safe
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<dialog id="<%= id %>" class="modal" data-controller="filter-modal">
|
|
2
|
+
<div class="modal-box h-5/6 max-w-5xl">
|
|
3
|
+
<h1 class="text-xl mb-4"><%= title %></h1>
|
|
4
|
+
<fieldset>
|
|
5
|
+
<%= modal_display %>
|
|
6
|
+
</fieldset>
|
|
7
|
+
<div class="mt-10">
|
|
8
|
+
<button class="btn btn-active btn-primary" type="submit">Apply</button>
|
|
9
|
+
<button class="btn btn-active btn-primary" type="reset">Reset</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<label class="modal-backdrop">
|
|
13
|
+
<button onclick="event.preventDefault(); <%=id %>.close();">Close</button>
|
|
14
|
+
</label>
|
|
15
|
+
</dialog>
|
|
16
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"><%= label %></div>
|
|
2
|
+
<fieldset class="grid grid-cols-11 mb-3" data-controller="predicate-select">
|
|
3
|
+
<div class="col-span-3 mr-2">
|
|
4
|
+
<div class="w-full">
|
|
5
|
+
<div class="relative inline-block text-left w-full">
|
|
6
|
+
<div class="w-full">
|
|
7
|
+
<% if @predicates.any? %>
|
|
8
|
+
<select id="<%= column %>_predicate" class='select border-0 select-primary bg-gray-900 ring-0 focus-within:ring-0 focus-within:ring-0 focus:ring-0 focus:outline-none inline-flex justify-between items-center w-full rounded-md ' data-action='change->predicate-select#handleChange'>
|
|
9
|
+
<% @predicates.each do |predicate| %>
|
|
10
|
+
<option value="<%= option_value(predicate) %>" <%= 'selected' if selected_predicate?(predicate) %>>
|
|
11
|
+
<%= predicate_label(predicate) %>
|
|
12
|
+
</option>
|
|
13
|
+
<% end %>
|
|
14
|
+
</select>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="col-span-8">
|
|
21
|
+
<div class="relative w-full">
|
|
22
|
+
<%= render ::Lookout::ComboboxComponent.new(name: column_name_with_predicate, multiple: multiple, column: column, url: url, value: values) %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</fieldset>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lookout::Filter::SelectComponent < ViewComponent::Base
|
|
4
|
+
def initialize(label:, column:, url:, predicates:, form:, multiple: true, input_html: {})
|
|
5
|
+
@label = label
|
|
6
|
+
@column = column
|
|
7
|
+
@url = url
|
|
8
|
+
@predicates = predicates
|
|
9
|
+
@form = form
|
|
10
|
+
@multiple = multiple
|
|
11
|
+
@input_html_options = input_html
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def selected_predicate?(predicate)
|
|
17
|
+
params.dig(:q, predicate_name(predicate)).present?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def option_value(predicate)
|
|
21
|
+
name = "q[#{predicate_name(predicate)}]"
|
|
22
|
+
name += "[]" if multiple
|
|
23
|
+
name
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def predicate_name(predicate)
|
|
27
|
+
"#{@column}_#{predicate}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def selected_predicate
|
|
31
|
+
@predicates.each do |predicate|
|
|
32
|
+
if params.dig(:q, predicate_name(predicate))
|
|
33
|
+
return predicate
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def column_name_with_predicate
|
|
41
|
+
if selected_predicate
|
|
42
|
+
option_value(selected_predicate)
|
|
43
|
+
else
|
|
44
|
+
option_value(@predicates.first)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def values
|
|
49
|
+
@predicates.each do |predicate|
|
|
50
|
+
option = params.dig(:q, predicate_name(predicate))
|
|
51
|
+
if option
|
|
52
|
+
return option
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
[]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def predicate_label(predicate)
|
|
60
|
+
Lookout::PredicateLabel[predicate]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attr_reader :label, :column, :url, :predicates, :form, :multiple
|
|
64
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="bg-base-200 py-2 px-4 mx-2 flex items-center text-base-content" data-controller="filter--item" data-filter--item-modal-value="<%= tag_item.modal %>">
|
|
2
|
+
<span data-action='click->filter--item#openModal'>
|
|
3
|
+
<%= tag_item.column.titleize %> <%= tag_item.predicate.titleize %>
|
|
4
|
+
<span class="font-bold">
|
|
5
|
+
<%= tag_item.label %>
|
|
6
|
+
</span>
|
|
7
|
+
</span>
|
|
8
|
+
<a class="hover:text-primary" href="<%= tag_item.url %>">
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4">
|
|
10
|
+
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path>
|
|
11
|
+
</svg>
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="rounded shadow bg-white cursor-pointer dark:bg-gray-800 flex h-8">
|
|
2
|
+
<button class="flex items-center px-1 sm:px-2 border-r border-gray-300 rounded-l dark:border-gray-500 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-900" type="button">
|
|
3
|
+
<svg class="feather h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
|
4
|
+
</button>
|
|
5
|
+
<button class="flex items-center px-1 sm:px-2 rounded-r dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-900" type="button">
|
|
6
|
+
<svg class="feather h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
|
7
|
+
</button>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<a href="<%= @url %>" class="relative px-4 md:px-6 w-1/2 my-4 w-auto group cursor-pointer" data-controller="frame-link" data-turbo-frame="chart">
|
|
2
|
+
<div>
|
|
3
|
+
<h5 class="text-sm font-bold uppercase whitespace-nowrap flex w-content border-transparent tooltip tooltip-bottom "
|
|
4
|
+
data-active-links-target="link" data-tip="<%= tooltip %>">
|
|
5
|
+
<%= @label %>
|
|
6
|
+
<div class="<%= klass %> ms-2"><%= arrow %></div>
|
|
7
|
+
</h5>
|
|
8
|
+
<div class="md:block flex gap-4">
|
|
9
|
+
<div>
|
|
10
|
+
<span class="flex items-center justify-between whitespace-nowrap">
|
|
11
|
+
<p class="font-bold text-xl"><%= formatted(value.current) %></p>
|
|
12
|
+
</span>
|
|
13
|
+
<% if compare_mode? %>
|
|
14
|
+
<p class="text-xs"><%= range_string %></p>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
<% if compare_mode? %>
|
|
18
|
+
<div>
|
|
19
|
+
<p class="font-bold text-xl text-gray-500"><%= formatted(value.compared_to) %></p>
|
|
20
|
+
<p class="text-xs text-gray-500"><%= compare_range_string %></p>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</a>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module Lookout
|
|
2
|
+
module Stats
|
|
3
|
+
class ComparableContainerComponent < ViewComponent::Base
|
|
4
|
+
include CompareMode
|
|
5
|
+
include Rangeable
|
|
6
|
+
|
|
7
|
+
def initialize(url, label, comparable, formatter = nil, selected = false, compare = false)
|
|
8
|
+
@url = url
|
|
9
|
+
@label = label
|
|
10
|
+
@comparable = comparable
|
|
11
|
+
@formatter = formatter
|
|
12
|
+
@selected = selected
|
|
13
|
+
@compare = compare
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def compare?
|
|
17
|
+
@compare
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def klass
|
|
21
|
+
if percentage.negative?
|
|
22
|
+
"text-red-400"
|
|
23
|
+
else
|
|
24
|
+
"text-green-400"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 〰 ↓ ↑
|
|
29
|
+
def arrow
|
|
30
|
+
if percentage.negative?
|
|
31
|
+
"↓"
|
|
32
|
+
elsif percentage.positive?
|
|
33
|
+
"↑"
|
|
34
|
+
else
|
|
35
|
+
"〰"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def percentage
|
|
40
|
+
begin
|
|
41
|
+
diff = value.current - value.compared_to
|
|
42
|
+
if diff.zero?
|
|
43
|
+
return 0
|
|
44
|
+
end
|
|
45
|
+
(value.current / diff).round(2) * 100
|
|
46
|
+
rescue ZeroDivisionError
|
|
47
|
+
0
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def number_to_duration(duration)
|
|
52
|
+
seconds =
|
|
53
|
+
case duration
|
|
54
|
+
when nil
|
|
55
|
+
nil
|
|
56
|
+
when ActiveSupport::Duration
|
|
57
|
+
duration.in_seconds
|
|
58
|
+
else
|
|
59
|
+
duration.to_f
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if seconds && seconds > 0
|
|
63
|
+
minutes = (seconds / 60).to_i
|
|
64
|
+
seconds = (seconds % 60).to_i
|
|
65
|
+
"#{minutes}m #{seconds}s"
|
|
66
|
+
else
|
|
67
|
+
"0m 0s"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def number_to_percentage(number, options = {})
|
|
72
|
+
precision = options.fetch(:precision, 2)
|
|
73
|
+
"#{number.round(precision)}%"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def compare_range_string
|
|
77
|
+
range_to_string(@comparable.compare_range)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def range_string
|
|
81
|
+
range_to_string(@comparable.range)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def value
|
|
85
|
+
@comparable.result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def tooltip
|
|
89
|
+
"#{formatted(value.current)} vs #{formatted(value.compared_to)} — #{number_to_percentage percentage} (#{arrow}) "
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def formatted(value)
|
|
93
|
+
public_send(@formatter, value)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def range_to_string(range)
|
|
99
|
+
[range[0], range[1]].map { |item| item.strftime('%m %B') }.join(' - ')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|