From aee7cee0981beeeb43393e0b07b1d93b6514432d Mon Sep 17 00:00:00 2001 From: martintomas Date: Mon, 2 Oct 2023 13:39:13 +0200 Subject: [PATCH] feat: ASCOR bubble chart data --- app/assets/stylesheets/tpi/pages/ascor.scss | 26 +++++ app/controllers/tpi/ascor_controller.rb | 12 +++ app/models/ascor/assessment_result.rb | 3 + app/services/api/ascor/bubble_chart.rb | 63 +++++++++++ app/views/tpi/ascor/_bubble_chart.html.erb | 3 + app/views/tpi/ascor/_index_assessment.js.erb | 7 ++ app/views/tpi/ascor/index.html.erb | 31 ++++++ config/routes.rb | 1 + spec/services/api/ascor/bubble_chart_spec.rb | 105 +++++++++++++++++++ 9 files changed, 251 insertions(+) create mode 100644 app/services/api/ascor/bubble_chart.rb create mode 100644 app/views/tpi/ascor/_bubble_chart.html.erb create mode 100644 app/views/tpi/ascor/_index_assessment.js.erb create mode 100644 spec/services/api/ascor/bubble_chart_spec.rb diff --git a/app/assets/stylesheets/tpi/pages/ascor.scss b/app/assets/stylesheets/tpi/pages/ascor.scss index a8219ab25..3fcfbb8e9 100644 --- a/app/assets/stylesheets/tpi/pages/ascor.scss +++ b/app/assets/stylesheets/tpi/pages/ascor.scss @@ -114,6 +114,32 @@ } } + .bubble-chart-header { + margin-bottom: 20px; + + @include desktop { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__assessment-dropdown { + display: flex; + align-items: center; + gap: 10px; + } + } + + .bubble-chart-description { + margin-bottom: 40px; + font-size: 14px; + line-height: 1.75; + + @include desktop { + max-width: 60%; + } + } + section { margin-top: 70px; diff --git a/app/controllers/tpi/ascor_controller.rb b/app/controllers/tpi/ascor_controller.rb index 12131b114..b583bf9e1 100644 --- a/app/controllers/tpi/ascor_controller.rb +++ b/app/controllers/tpi/ascor_controller.rb @@ -2,6 +2,8 @@ module TPI class ASCORController < TPIController before_action :fetch_ascor_countries, only: [:index, :show] before_action :fetch_ascor_country, only: [:show] + before_action :fetch_assessment_date, only: [:index, :show, :index_assessment] + before_action :fetch_ascor_assessment_results, only: [:index, :index_assessment] def index @assessment_dates = ASCOR::Assessment.pluck(:assessment_date).uniq @@ -18,6 +20,8 @@ def show fixed_navbar("ASCOR Country #{@country.name}", admin_ascor_country_path(@country.id)) end + def index_assessment; end + def user_download render zip: { 'ASCOR_countries.xlsx' => Api::CSVToExcel.new(CSVExport::ASCOR::Countries.new.call).call, @@ -38,5 +42,13 @@ def fetch_ascor_countries def fetch_ascor_country @country = ASCOR::Country.friendly.find(params[:id]) end + + def fetch_assessment_date + @assessment_date = params[:assessment_date] || ASCOR::Assessment.maximum(:assessment_date) + end + + def fetch_ascor_assessment_results + @ascor_assessment_results = Api::ASCOR::BubbleChart.new(@assessment_date).call + end end end diff --git a/app/models/ascor/assessment_result.rb b/app/models/ascor/assessment_result.rb index 7576bee0f..3275d559f 100644 --- a/app/models/ascor/assessment_result.rb +++ b/app/models/ascor/assessment_result.rb @@ -16,4 +16,7 @@ class ASCOR::AssessmentResult < ApplicationRecord belongs_to :indicator, class_name: 'ASCOR::AssessmentIndicator', foreign_key: :indicator_id validates_uniqueness_of :indicator_id, scope: :assessment_id + + scope :of_type, ->(type) { includes(:indicator).where(ascor_assessment_indicators: {indicator_type: type}) } + scope :by_date, ->(date) { includes(:assessment).where(ascor_assessments: {assessment_date: date}) } end diff --git a/app/services/api/ascor/bubble_chart.rb b/app/services/api/ascor/bubble_chart.rb new file mode 100644 index 000000000..207ffcc45 --- /dev/null +++ b/app/services/api/ascor/bubble_chart.rb @@ -0,0 +1,63 @@ +module Api + module ASCOR + class BubbleChart + MARKET_CAP_QUERY = { + emissions_metric: 'Intensity per capita', + emissions_boundary: 'Production - excluding LULUCF' + }.freeze + + attr_accessor :assessment_date + + def initialize(assessment_date) + @assessment_date = assessment_date + end + + def call + ::ASCOR::AssessmentResult + .by_date(@assessment_date) + .of_type(:area) + .includes(assessment: :country) + .order(:indicator_id) + .map do |result| + { + pillar: pillars[result.indicator.code.split('.').first]&.first&.text, + area: result.indicator.text, + result: result.answer, + country_id: result.assessment.country_id, + country_name: result.assessment.country.name, + country_path: result.assessment.country.path, + market_cap_group: calculate_market_cap_group(result.assessment.country_id) + } + end + end + + private + + def calculate_market_cap_group(country_id) + recent_emission_level = recent_emission_levels[country_id]&.first&.recent_emission_level + return :small if recent_emission_level.blank? + + market_cap_groups.find { |range, _| range.include?(recent_emission_level) }&.last || :small + end + + def pillars + ::ASCOR::AssessmentIndicator.where(indicator_type: :pillar).group_by(&:code) + end + + def recent_emission_levels + @recent_emission_levels ||= ::ASCOR::Pathway.where(MARKET_CAP_QUERY).where(assessment_date: assessment_date) + .select(:country_id, :recent_emission_level) + .group_by(&:country_id) + end + + def market_cap_groups + @market_cap_groups ||= begin + min = ::ASCOR::Pathway.where(MARKET_CAP_QUERY).where(assessment_date: assessment_date).minimum(:recent_emission_level) + max = ::ASCOR::Pathway.where(MARKET_CAP_QUERY).where(assessment_date: assessment_date).maximum(:recent_emission_level) + step = (max.to_f - min.to_f) / 3 + {min..min + step => :small, min + step..min + (2 * step) => :medium, min + (2 * step)..max => :large} + end + end + end + end +end diff --git a/app/views/tpi/ascor/_bubble_chart.html.erb b/app/views/tpi/ascor/_bubble_chart.html.erb new file mode 100644 index 000000000..5e58746c1 --- /dev/null +++ b/app/views/tpi/ascor/_bubble_chart.html.erb @@ -0,0 +1,3 @@ + +<%#= react_component('charts/bank-bubble/Chart', { results: @ascor_assessment_results }) %> +<%#= react_component('charts/bank-bubble/CompaniesAccordion', { results: @ascor_assessment_results }) %> \ No newline at end of file diff --git a/app/views/tpi/ascor/_index_assessment.js.erb b/app/views/tpi/ascor/_index_assessment.js.erb new file mode 100644 index 000000000..807628e2c --- /dev/null +++ b/app/views/tpi/ascor/_index_assessment.js.erb @@ -0,0 +1,7 @@ +(function () { + document.getElementById('bubble-chart').innerHTML = "<%= j render('bubble_chart') %>"; + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('assessment_date', <%= @assessment_date %>); + window.history.replaceState({}, '', newUrl.href); + ReactRailsUJS.mountComponents('#assessment-charts'); +})(); diff --git a/app/views/tpi/ascor/index.html.erb b/app/views/tpi/ascor/index.html.erb index 53b9b4113..e416e0222 100644 --- a/app/views/tpi/ascor/index.html.erb +++ b/app/views/tpi/ascor/index.html.erb @@ -31,6 +31,37 @@ + + +
+
+

+ Lorem ipsum dolor sit amet consectetur. +

+ +
+
+ Assessment Date: +
+
+ <%= react_component('RemoteDropdown', { + name: 'assessment_date', + remote: true, + url: index_assessment_tpi_ascor_index_path, + data: @assessment_dates.map {|v| {label: v&.strftime('%d %B %Y'), value: v}}, + selected: @assessment_date + }) %> +
+
+
+
+ Lorem ipsum dolor sit amet consectetur. Viverra vestibulum eget vitae justo morbi eget. +
+
+ <%= render 'tpi/ascor/bubble_chart' %> +
+
+ <% if @methodology_publication.present? %>
diff --git a/config/routes.rb b/config/routes.rb index 5c0fa6121..d18c5e2a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,7 @@ resources :ascor, only: [:show, :index] do collection do + get :index_assessment get :user_download end end diff --git a/spec/services/api/ascor/bubble_chart_spec.rb b/spec/services/api/ascor/bubble_chart_spec.rb new file mode 100644 index 000000000..ec74fabe4 --- /dev/null +++ b/spec/services/api/ascor/bubble_chart_spec.rb @@ -0,0 +1,105 @@ +require 'rails_helper' + +RSpec.describe Api::ASCOR::BubbleChart do + subject { described_class.new(assessment_date).call } + + before_all do + usa = create(:ascor_country, id: 1, name: 'USA', iso: 'USA') + japan = create(:ascor_country, id: 2, name: 'Japan', iso: 'JPN') + + _indicator_pillar_1 = create(:ascor_assessment_indicator, id: 1, code: 'EP', indicator_type: :pillar, + text: 'Emissions Performance') + _indicator_pillar_2 = create(:ascor_assessment_indicator, id: 2, code: 'CP', indicator_type: :pillar, + text: 'Climate Performance') + indicator_area_1 = create(:ascor_assessment_indicator, id: 3, code: 'EP.1', indicator_type: :area, + text: 'Emissions Performance 1') + indicator_area_2 = create(:ascor_assessment_indicator, id: 4, code: 'EP.2', indicator_type: :area, + text: 'Emissions Performance 2') + indicator_area_3 = create(:ascor_assessment_indicator, id: 5, code: 'CP.1', indicator_type: :area, + text: 'Climate Performance 1') + + create :ascor_assessment, country: usa, assessment_date: Date.new(2019, 1, 1), results: [ + build(:ascor_assessment_result, indicator: indicator_area_1, answer: 'Yes'), + build(:ascor_assessment_result, indicator: indicator_area_2, answer: 'No'), + build(:ascor_assessment_result, indicator: indicator_area_3, answer: 'Yes') + ] + create :ascor_assessment, country: usa, assessment_date: Date.new(2019, 2, 1), results: [ + build(:ascor_assessment_result, indicator: indicator_area_1, answer: 'Yes'), + build(:ascor_assessment_result, indicator: indicator_area_2, answer: 'No'), + build(:ascor_assessment_result, indicator: indicator_area_3, answer: 'No') + ] + create :ascor_assessment, country: japan, assessment_date: Date.new(2019, 2, 1), results: [ + build(:ascor_assessment_result, indicator: indicator_area_1, answer: 'No'), + build(:ascor_assessment_result, indicator: indicator_area_2, answer: 'Yes'), + build(:ascor_assessment_result, indicator: indicator_area_3, answer: 'No') + ] + + create :ascor_pathway, country: usa, assessment_date: Date.new(2019, 1, 1), emissions_metric: 'Intensity per capita', + emissions_boundary: 'Production - excluding LULUCF', recent_emission_level: 100 + create :ascor_pathway, country: usa, assessment_date: Date.new(2019, 2, 1), emissions_metric: 'Intensity per capita', + emissions_boundary: 'Production - excluding LULUCF', recent_emission_level: 200 + create :ascor_pathway, country: japan, assessment_date: Date.new(2019, 2, 1), emissions_metric: 'Intensity per capita', + emissions_boundary: 'Production - excluding LULUCF', recent_emission_level: 300 + end + + let(:assessment_date) { Date.new(2019, 2, 1) } + + it 'returns the correct data' do + expect(subject).to eq( + [{ + pillar: 'Emissions Performance', + area: 'Emissions Performance 1', + result: 'Yes', + country_id: 1, + country_name: 'USA', + country_path: '/ascor/usa', + market_cap_group: :small + }, + { + pillar: 'Emissions Performance', + area: 'Emissions Performance 1', + result: 'No', + country_id: 2, + country_name: 'Japan', + country_path: '/ascor/japan', + market_cap_group: :large + }, + { + pillar: 'Emissions Performance', + area: 'Emissions Performance 2', + result: 'No', + country_id: 1, + country_name: 'USA', + country_path: '/ascor/usa', + market_cap_group: :small + }, + { + pillar: 'Emissions Performance', + area: 'Emissions Performance 2', + result: 'Yes', + country_id: 2, + country_name: 'Japan', + country_path: '/ascor/japan', + market_cap_group: :large + }, + { + pillar: 'Climate Performance', + area: 'Climate Performance 1', + result: 'No', + country_id: 1, + country_name: 'USA', + country_path: '/ascor/usa', + market_cap_group: :small + }, + { + pillar: 'Climate Performance', + area: 'Climate Performance 1', + result: 'No', + country_id: 2, + country_name: 'Japan', + country_path: '/ascor/japan', + market_cap_group: :large + }] + ) + end +end