diff --git a/app/admin/ascor/assessment_indicators.rb b/app/admin/ascor/assessment_indicators.rb
new file mode 100644
index 000000000..ff66e1727
--- /dev/null
+++ b/app/admin/ascor/assessment_indicators.rb
@@ -0,0 +1,58 @@
+ActiveAdmin.register ASCOR::AssessmentIndicator do
+ config.sort_order = 'id_asc'
+
+ menu label: 'Assessment Indicators', parent: 'ASCOR', priority: 4
+
+ actions :all, except: [:new, :create]
+
+ filter :code
+ filter :text
+ filter :indicator_type, as: :check_boxes, collection: ASCOR::AssessmentIndicator::INDICATOR_TYPES
+
+ data_export_sidebar 'ASCORAssessmentIndicators', display_name: 'ASCOR AssessmentIndicators'
+
+ permit_params :code, :indicator_type, :text
+
+ show do
+ attributes_table do
+ row :id
+ row :code
+ row :indicator_type
+ row :text
+ row :units_or_response_type
+ row :created_at
+ row :updated_at
+ end
+
+ active_admin_comments
+ end
+
+ form html: {'data-controller' => 'check-modified'} do |f|
+ f.semantic_errors(*f.object.errors.keys)
+
+ f.inputs do
+ f.input :indicator_type, as: :select, collection: ASCOR::AssessmentIndicator::INDICATOR_TYPES
+ f.input :code
+ f.input :text
+ f.input :units_or_response_type
+ end
+
+ f.actions
+ end
+
+ index do
+ id_column
+ column :indicator_type
+ column :code
+ column :text
+ actions
+ end
+
+ csv do
+ column :id
+ column :type, &:indicator_type
+ column :code
+ column :text
+ column :units_or_response_type
+ end
+end
diff --git a/app/admin/ascor/assessments.rb b/app/admin/ascor/assessments.rb
new file mode 100644
index 000000000..89e210b63
--- /dev/null
+++ b/app/admin/ascor/assessments.rb
@@ -0,0 +1,102 @@
+ActiveAdmin.register ASCOR::Assessment do
+ config.sort_order = 'assessment_date_desc'
+ includes :country
+
+ menu label: 'Assessments', parent: 'ASCOR', priority: 5
+
+ permit_params :country_id, :assessment_date, :publication_date, :notes,
+ results_attributes: [:id, :assessment_id, :indicator_id, :answer, :source, :year, :_destroy]
+
+ filter :country
+ filter :assessment_date, as: :select
+
+ data_export_sidebar 'ASCORAssessments', display_name: 'ASCOR Assessments'
+
+ index do
+ selectable_column
+ id_column
+ column :country, sortable: 'ascor_countries.name'
+ column :assessment_date
+ column :publication_date
+ column :notes
+
+ actions
+ end
+
+ show do
+ attributes_table do
+ row :id
+ row :country
+ row :assessment_date
+ row :publication_date
+ row :notes
+ row :created_at
+ row :updated_at
+ end
+
+ panel 'Assessment Results' do
+ table_for resource.results.includes(:indicator).order(:indicator_id) do
+ column(:indicator)
+ column(:answer)
+ column(:source)
+ column(:year)
+ end
+ end
+
+ active_admin_comments
+ end
+
+ form do |f|
+ f.semantic_errors(*f.object.errors.keys)
+
+ f.inputs do
+ f.input :country, as: :select, collection: ASCOR::Country.all.order(:name)
+ f.input :assessment_date, as: :datepicker
+ f.input :publication_date, as: :datepicker
+ f.input :notes
+ end
+
+ f.has_many :results, allow_destroy: true, heading: false do |ff|
+ ff.inputs 'Assessment Results' do
+ ff.input :indicator, as: :select, collection: ASCOR::AssessmentIndicator.all.order(:code)
+ ff.input :answer
+ ff.input :source
+ ff.input :year
+ end
+ end
+
+ f.actions
+ end
+
+ csv do
+ column :id
+ column :country do |resource|
+ resource.country.name
+ end
+ column :assessment_date
+ column :publication_date
+ ASCOR::AssessmentIndicator.where.not(indicator_type: 'pillar')
+ .where.not(code: %w[EP.1.a.i EP.1.a.ii]).order(:id).all.each do |indicator|
+ column "#{indicator.indicator_type} #{indicator.code}", humanize_name: false do |resource|
+ controller.assessment_results[[resource.id, indicator.id]]&.first&.answer
+ end
+ end
+ ASCOR::AssessmentIndicator.where(indicator_type: %w[indicator metric]).order(:id).all.each do |indicator|
+ column "source #{indicator.indicator_type} #{indicator.code}", humanize_name: false do |resource|
+ controller.assessment_results[[resource.id, indicator.id]]&.first&.source
+ end
+ end
+ ASCOR::AssessmentIndicator.where(indicator_type: 'metric').order(:id).all.each do |indicator|
+ column "year #{indicator.indicator_type} #{indicator.code}", humanize_name: false do |resource|
+ controller.assessment_results[[resource.id, indicator.id]]&.first&.year
+ end
+ end
+ column :notes
+ end
+
+ controller do
+ def assessment_results
+ @assessment_results ||= ASCOR::AssessmentResult.all.group_by { |r| [r.assessment_id, r.indicator_id] }
+ end
+ end
+end
diff --git a/app/admin/ascor/benchmarks.rb b/app/admin/ascor/benchmarks.rb
new file mode 100644
index 000000000..0cbc4a6c3
--- /dev/null
+++ b/app/admin/ascor/benchmarks.rb
@@ -0,0 +1,88 @@
+ActiveAdmin.register ASCOR::Benchmark do
+ config.sort_order = 'country_id_asc'
+ includes :country
+
+ menu label: 'Benchmarks', parent: 'ASCOR', priority: 2
+
+ permit_params :country_id, :publication_date, :emissions_metric, :emissions_boundary, :units, :benchmark_type, :emissions
+
+ filter :country, as: :select, collection: -> { ASCOR::Country.all.order(:name) }
+ filter :emissions_metric, as: :select, collection: -> { ASCOR::EmissionsMetric::VALUES }
+ filter :emissions_boundary, as: :select, collection: -> { ASCOR::EmissionsBoundary::VALUES }
+ filter :benchmark_type, as: :select, collection: -> { ASCOR::BenchmarkType::VALUES }
+
+ data_export_sidebar 'ASCORBenchmarks', display_name: 'ASCOR Benchmarks'
+
+ index do
+ selectable_column
+ id_column
+ column :country, sortable: 'ascor_countries.name'
+ column :emissions_metric
+ column :emissions_boundary
+ column :units
+ column :benchmark_type
+
+ actions
+ end
+
+ show do
+ attributes_table do
+ row :id
+ row :country
+ row :publication_date
+ row :emissions_metric
+ row :emissions_boundary
+ row :units
+ row :benchmark_type
+ row :created_at
+ row :updated_at
+ end
+
+ panel 'Benchmark emission values' do
+ render 'admin/cp/emissions_table', emissions: resource.emissions
+ end
+
+ active_admin_comments
+ end
+
+ form html: {'data-controller' => 'check-modified with-emission-table-form'} do |f|
+ f.semantic_errors(*f.object.errors.keys)
+
+ f.inputs do
+ f.input :country, as: :select, collection: ASCOR::Country.all.order(:name)
+ f.input :publication_date, as: :datepicker
+ f.input :emissions_metric, as: :select, collection: ASCOR::EmissionsMetric::VALUES
+ f.input :emissions_boundary, as: :select, collection: ASCOR::EmissionsBoundary::VALUES
+ f.input :units
+ f.input :benchmark_type, as: :select, collection: ASCOR::BenchmarkType::VALUES
+ f.input :emissions, as: :hidden, input_html: {value: f.object.emissions.to_json, id: 'input_emissions'}
+ end
+
+ div class: 'panel' do
+ h3 'Benchmark emission values'
+ div class: 'panel-contents padding-20' do
+ render 'admin/cp/emissions_table_edit', f: f
+ end
+ end
+
+ f.actions
+ end
+
+ csv do
+ year_columns = ASCOR::Benchmark.select(:emissions).flat_map(&:emissions_all_years).uniq.sort
+
+ column :id
+ column(:country) { |b| b.country.name }
+ column(:publication_date) { |b| b.publication_date.to_s(:year_month) }
+ column :emissions_metric
+ column :emissions_boundary
+ column :units
+ column :benchmark_type
+
+ year_columns.map do |year|
+ column year do |benchmark|
+ benchmark.emissions[year]
+ end
+ end
+ end
+end
diff --git a/app/admin/ascor/countries.rb b/app/admin/ascor/countries.rb
new file mode 100644
index 000000000..cf329134c
--- /dev/null
+++ b/app/admin/ascor/countries.rb
@@ -0,0 +1,65 @@
+ActiveAdmin.register ASCOR::Country do
+ config.batch_actions = false
+ config.sort_order = 'name_asc'
+
+ menu label: 'Countries', parent: 'ASCOR', priority: 1
+
+ permit_params :name, :iso, :region, :wb_lending_group, :fiscal_monitor_category, :type_of_party
+
+ filter :iso_contains, label: 'ISO'
+ filter :name_contains, label: 'Name'
+ filter :region, as: :check_boxes, collection: proc { Geography::REGIONS }
+
+ data_export_sidebar 'ASCORCountries', display_name: 'ASCOR Countries'
+
+ index do
+ selectable_column
+ id_column
+ column :name
+ column 'Country ISO code', :iso
+ column :region
+
+ actions
+ end
+
+ show do
+ attributes_table do
+ row :id
+ row :name
+ row 'Country ISO code', &:iso
+ row :region
+ row 'World Bank lending group', &:wb_lending_group
+ row 'International Monetary Fund fiscal monitor category', &:fiscal_monitor_category
+ row 'Type of Party to the United Nations Framework Convention on Climate Change', &:type_of_party
+ end
+
+ active_admin_comments
+ end
+
+ form do |f|
+ semantic_errors(*f.object.errors.attribute_names)
+
+ f.inputs do
+ f.input :name
+ f.input :iso, label: 'Country ISO code'
+ f.input :region, as: :select, collection: ASCOR::Country::REGIONS
+ f.input :wb_lending_group, as: :select, collection: ASCOR::Country::LENDING_GROUPS, label: 'World Bank lending group'
+ f.input :fiscal_monitor_category, as: :select, collection: ASCOR::Country::MONITOR_CATEGORIES,
+ label: 'International Monetary Fund fiscal monitor category'
+ f.input :type_of_party, as: :select, collection: ASCOR::Country::TYPE_OF_PARTY,
+ label: 'Type of Party to the United Nations Framework Convention on Climate Change'
+ end
+
+ f.actions
+ end
+
+ csv do
+ column :id
+ column :name
+ column 'Country ISO code', humanize_name: false, &:iso
+ column :region
+ column 'World Bank lending group', humanize_name: false, &:wb_lending_group
+ column 'International Monetary Fund fiscal monitor category', humanize_name: false, &:fiscal_monitor_category
+ column 'Type of Party to the United Nations Framework Convention on Climate Change', humanize_name: false, &:type_of_party
+ end
+end
diff --git a/app/admin/ascor/pathways.rb b/app/admin/ascor/pathways.rb
new file mode 100644
index 000000000..3b51289df
--- /dev/null
+++ b/app/admin/ascor/pathways.rb
@@ -0,0 +1,116 @@
+ActiveAdmin.register ASCOR::Pathway do
+ config.sort_order = 'id_asc'
+ includes :country
+
+ menu label: 'Pathways', parent: 'ASCOR', priority: 3
+
+ permit_params :country_id, :publication_date, :assessment_date, :emissions_metric, :emissions_boundary, :units,
+ :emissions, :last_historical_year, :trend_1_year, :trend_3_year, :trend_5_year, :trend_source, :trend_year,
+ :recent_emission_level, :recent_emission_source, :recent_emission_year
+
+ filter :country, as: :select, collection: -> { ASCOR::Country.all.order(:name) }
+ filter :assessment_date, as: :select, collection: -> { ASCOR::Pathway.pluck(:assessment_date).uniq }
+ filter :emissions_metric, as: :select, collection: -> { ASCOR::EmissionsMetric::VALUES }
+ filter :emissions_boundary, as: :select, collection: -> { ASCOR::EmissionsBoundary::VALUES }
+
+ data_export_sidebar 'ASCORPathways', display_name: 'ASCOR Pathways'
+
+ index do
+ selectable_column
+ id_column
+ column :country, sortable: 'ascor_countries.name'
+ column :assessment_date
+ column :emissions_metric
+ column :emissions_boundary
+ column :units
+
+ actions
+ end
+
+ show do
+ attributes_table do
+ row :id
+ row :country
+ row :assessment_date
+ row :publication_date
+ row :emissions_metric
+ row :emissions_boundary
+ row :units
+ row :last_historical_year
+ row 'metric EP1.a.i', &:recent_emission_level
+ row 'source EP1.a.i', &:recent_emission_source
+ row 'year EP1.a.i', &:recent_emission_year
+ row 'metric EP1.a.ii 1-year', &:trend_1_year
+ row 'metric EP1.a.ii 3-year', &:trend_3_year
+ row 'metric EP1.a.ii 5-year', &:trend_5_year
+ row 'source metric EP1.a.ii', &:trend_source
+ row 'year metric EP1.a.ii', &:trend_year
+ row :created_at
+ row :updated_at
+ end
+
+ panel 'Pathway emission values' do
+ render 'admin/cp/emissions_table', emissions: resource.emissions
+ end
+
+ active_admin_comments
+ end
+
+ form html: {'data-controller' => 'check-modified with-emission-table-form'} do |f|
+ f.semantic_errors(*f.object.errors.keys)
+
+ f.inputs do
+ f.input :country, as: :select, collection: ASCOR::Country.all.order(:name)
+ f.input :assessment_date, as: :datepicker
+ f.input :publication_date, as: :datepicker
+ f.input :emissions_metric, as: :select, collection: ASCOR::EmissionsMetric::VALUES
+ f.input :emissions_boundary, as: :select, collection: ASCOR::EmissionsBoundary::VALUES
+ f.input :units
+ f.input :last_historical_year
+ f.input :recent_emission_level, label: 'metric EP1.a.i'
+ f.input :recent_emission_source, label: 'source EP1.a.i'
+ f.input :recent_emission_year, label: 'year EP1.a.i'
+ f.input :trend_1_year, label: 'metric EP1.a.ii 1-year'
+ f.input :trend_3_year, label: 'metric EP1.a.ii 3-year'
+ f.input :trend_5_year, label: 'metric EP1.a.ii 5-year'
+ f.input :trend_source, label: 'source metric EP1.a.ii'
+ f.input :trend_year, label: 'year metric EP1.a.ii'
+ f.input :emissions, as: :hidden, input_html: {value: f.object.emissions.to_json, id: 'input_emissions'}
+ end
+
+ div class: 'panel' do
+ h3 'Pathway emission values'
+ div class: 'panel-contents padding-20' do
+ render 'admin/cp/emissions_table_edit', f: f
+ end
+ end
+
+ f.actions
+ end
+
+ csv do
+ year_columns = ASCOR::Pathway.select(:emissions).flat_map(&:emissions_all_years).uniq.sort
+
+ column :id
+ column(:country) { |b| b.country.name }
+ column(:assessment_date) { |b| b.assessment_date.strftime '%m/%d/%y' }
+ column(:publication_date) { |b| b.publication_date.to_s(:year_month) }
+ column :emissions_metric
+ column :emissions_boundary
+ column :units
+ column :last_historical_year
+ column 'metric EP1.a.i', humanize_name: false, &:recent_emission_level
+ column 'source EP1.a.i', humanize_name: false, &:recent_emission_source
+ column 'year EP1.a.i', humanize_name: false, &:recent_emission_year
+ column 'metric EP1.a.ii 1-year', humanize_name: false, &:trend_1_year
+ column 'metric EP1.a.ii 3-year', humanize_name: false, &:trend_3_year
+ column 'metric EP1.a.ii 5-year', humanize_name: false, &:trend_5_year
+ column 'source metric EP1.a.ii', humanize_name: false, &:trend_source
+ column 'year metric EP1.a.ii', humanize_name: false, &:trend_year
+ year_columns.map do |year|
+ column year do |resource|
+ resource.emissions[year]
+ end
+ end
+ end
+end
diff --git a/app/admin/data_uploads.rb b/app/admin/data_uploads.rb
index 4823898f2..9613f2ce7 100644
--- a/app/admin/data_uploads.rb
+++ b/app/admin/data_uploads.rb
@@ -1,5 +1,5 @@
ActiveAdmin.register DataUpload do
- menu parent: 'Administration', priority: 2
+ menu parent: 'Administration', priority: 3
decorate_with DataUploadDecorator
diff --git a/app/assets/images/tpi/ascor/circles_1.svg b/app/assets/images/tpi/ascor/circles_1.svg
new file mode 100644
index 000000000..8235f39cf
--- /dev/null
+++ b/app/assets/images/tpi/ascor/circles_1.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/circles_2.svg b/app/assets/images/tpi/ascor/circles_2.svg
new file mode 100644
index 000000000..18ecfa14a
--- /dev/null
+++ b/app/assets/images/tpi/ascor/circles_2.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/circles_3.svg b/app/assets/images/tpi/ascor/circles_3.svg
new file mode 100644
index 000000000..056420b12
--- /dev/null
+++ b/app/assets/images/tpi/ascor/circles_3.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/excempt.svg b/app/assets/images/tpi/ascor/excempt.svg
new file mode 100644
index 000000000..c99fdf445
--- /dev/null
+++ b/app/assets/images/tpi/ascor/excempt.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/logo.png b/app/assets/images/tpi/ascor/logo.png
new file mode 100644
index 000000000..26a4aeedb
Binary files /dev/null and b/app/assets/images/tpi/ascor/logo.png differ
diff --git a/app/assets/images/tpi/ascor/no.svg b/app/assets/images/tpi/ascor/no.svg
new file mode 100644
index 000000000..b8c1e2cf0
--- /dev/null
+++ b/app/assets/images/tpi/ascor/no.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/no_data.svg b/app/assets/images/tpi/ascor/no_data.svg
new file mode 100644
index 000000000..08ef2dbcd
--- /dev/null
+++ b/app/assets/images/tpi/ascor/no_data.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/assets/images/tpi/ascor/no_disclosure.svg b/app/assets/images/tpi/ascor/no_disclosure.svg
new file mode 100644
index 000000000..329cdbf62
--- /dev/null
+++ b/app/assets/images/tpi/ascor/no_disclosure.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/not_applicable.svg b/app/assets/images/tpi/ascor/not_applicable.svg
new file mode 100644
index 000000000..717566b52
--- /dev/null
+++ b/app/assets/images/tpi/ascor/not_applicable.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/partial.svg b/app/assets/images/tpi/ascor/partial.svg
new file mode 100644
index 000000000..58dad66db
--- /dev/null
+++ b/app/assets/images/tpi/ascor/partial.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/assets/images/tpi/ascor/yes.svg b/app/assets/images/tpi/ascor/yes.svg
new file mode 100644
index 000000000..973121c83
--- /dev/null
+++ b/app/assets/images/tpi/ascor/yes.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/assets/stylesheets/tpi.scss b/app/assets/stylesheets/tpi.scss
index 3c48f0964..d3605b988 100644
--- a/app/assets/stylesheets/tpi.scss
+++ b/app/assets/stylesheets/tpi.scss
@@ -26,8 +26,10 @@
@import "tpi/navbar";
@import "tpi/publications";
@import "tpi/bubble-chart";
+@import "tpi/bubble-chart-countries";
@import "tpi/charts";
@import "tpi/base-tooltip";
+@import "tpi/info-tooltip";
@import "tpi/filters";
@import "tpi/footer";
@import "tpi/dropdown-selector";
@@ -37,9 +39,12 @@
@import "tpi/fixed-navbar";
@import "tpi/nested-dropdown";
@import "tpi/banking-question-legend";
+@import "tpi/country-question-legend";
@import "tpi/latest-information";
@import "tpi/mq_beta_scores";
@import "tpi/mq-beta-modal";
+@import "tpi/selector";
+@import "tpi/emissions-chart";
@import "tpi/pages/*";
@import "tpi/shared/*";
diff --git a/app/assets/stylesheets/tpi/_bubble-chart-countries.scss b/app/assets/stylesheets/tpi/_bubble-chart-countries.scss
new file mode 100644
index 000000000..f1faf39ee
--- /dev/null
+++ b/app/assets/stylesheets/tpi/_bubble-chart-countries.scss
@@ -0,0 +1,195 @@
+@import "colors";
+@import "typography";
+$tape-height: 8px;
+$tape-color: rgba(25, 25, 25, 0.1);
+$cell-height: 80px;
+$cell-height-banks: 100px;
+$legend-image-width: 60px;
+
+.bubble-chart__container__grid {
+ display: none;
+ @include desktop {
+ grid-template-columns: 0.5fr 0.5fr 1.5fr 1fr 1fr 1fr;
+ padding: 0;
+ display: grid;
+ }
+}
+
+.bubble-chart__container__mobile {
+ display: block;
+ width: 100%;
+ padding: 10px;
+
+ .country-bubble-mobile {
+ width: 100%;
+ border: 1px solid $ascor-background-color;
+
+ > ul > li:last-of-type .country-bubble-mobile__item {
+ border-bottom: none;
+ }
+
+ &__item {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ font-size: 16px;
+ font-style: normal;
+
+ &.--pillar {
+ font-weight: 700;
+ font-family: $font-family-bold;
+ background: $ascor-background-color;
+ color: #fff;
+ padding: 20px 10px;
+ cursor: pointer;
+ border-bottom: 1px solid $grey-medium;
+ &.--open {
+ border-bottom: none;
+ }
+ }
+ &.--area {
+ color: #000;
+ padding: 10px;
+ font-family: $font-family-regular;
+ border-top: 1px solid $ascor-background-color;
+ cursor: pointer;
+ &.--open {
+ border-bottom: 1px solid $grey-medium;
+ }
+ }
+
+ .chevron-icon {
+ width: 12px;
+ }
+
+ &__result {
+ font-family: $font-family-regular;
+ font-size: 16px;
+
+ &__title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ padding: 10px;
+ border-bottom: 1px solid $grey-medium;
+ cursor: pointer;
+ min-height: 45px;
+ position: relative;
+
+ div {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ }
+ }
+
+ &:not(:last-of-type) .country-bubble-mobile__item__result__title {
+ border-bottom-style: dashed;
+ }
+
+ &.--open {
+ .country-bubble-mobile__item__result__title {
+ border-bottom: none;
+ position: absolute;
+ }
+ .country-bubble-mobile__item__result__countries {
+ border-bottom: 1px solid $grey-medium;
+ border-bottom-style: dashed;
+ }
+ }
+
+ &:last-of-type {
+ .country-bubble-mobile__item__result__countries {
+ border-bottom: none;
+ }
+ }
+
+ &__countries {
+ padding: 10px 0px 10px 42px;
+ min-height: 45px;
+ li:not(:last-of-type) {
+ padding-bottom: 10px;
+ }
+ }
+ }
+ }
+ }
+}
+
+.bubble-chart__cell-country {
+ position: relative;
+ height: $cell-height-banks;
+ display: flex;
+ align-items: center;
+ border-right: calc(#{$tape-height / 2}) dashed $tape-color;
+
+ & > *:first-child {
+ margin: auto;
+ z-index: 1;
+ }
+
+ &::before {
+ background-color: $tape-color;
+ content: "";
+ position: absolute;
+ top: calc(50% - #{$tape-height / 2});
+ height: $tape-height;
+ width: calc(100% + #{$tape-height / 2});
+ }
+}
+
+.bubble-chart_circle_country {
+ circle:hover {
+ stroke-width: 3;
+ stroke: $black !important;
+ }
+}
+
+.bubble-chart__level-country {
+ border-right: calc(#{$tape-height / 2}) dashed $tape-color;
+ position: relative;
+ padding-left: 20px;
+ height: 100%;
+}
+
+.bubble-chart__level-title-country {
+ height: 100%;
+ font-family: $font-family-bold;
+ font-size: 16px;
+ color: $black;
+ margin-bottom: 20px;
+}
+
+.bubble-chart__level-area-country {
+ font-family: $font-family-bold;
+ font-size: 16px;
+ background-color: $ascor-background-color;
+ color: white;
+ padding: 10px;
+ width: 100%;
+ display: flex;
+ align-items: center;
+
+ color: $black;
+ text-align: end;
+ margin-right: 14px;
+ grid-column: span 2;
+ height: 100%;
+ padding: 46px 0 46px;
+ gap: 16px;
+ background-color: white;
+
+ &__line {
+ border: 8px solid #e8e8e8;
+ border-right: none;
+ height: 100%;
+ flex: 1;
+ }
+
+ &__area {
+ text-align: end;
+ padding-right: 14px;
+ flex: 1;
+ }
+}
diff --git a/app/assets/stylesheets/tpi/_bubble-chart.scss b/app/assets/stylesheets/tpi/_bubble-chart.scss
index f5b1e6b70..67189583b 100644
--- a/app/assets/stylesheets/tpi/_bubble-chart.scss
+++ b/app/assets/stylesheets/tpi/_bubble-chart.scss
@@ -2,7 +2,7 @@
@import "typography";
$tape-height: 8px;
-$tape-color: rgba(25,25,25,0.1);
+$tape-color: rgba(25, 25, 25, 0.1);
$cell-height: 80px;
$cell-height-banks: 100px;
$legend-image-width: 60px;
@@ -21,7 +21,6 @@ $legend-image-width: 60px;
.last {
border-right: none;
}
-
}
&--banks {
diff --git a/app/assets/stylesheets/tpi/_colors.scss b/app/assets/stylesheets/tpi/_colors.scss
index 2439537ff..9a6c54e82 100644
--- a/app/assets/stylesheets/tpi/_colors.scss
+++ b/app/assets/stylesheets/tpi/_colors.scss
@@ -8,6 +8,7 @@ $hawkes-blue: #ECF1FE;
$yellow: #FFDD49;
$grey-light: #DDE7FD;
$grey-medium: #D8D8D8;
+$grey-lighter-medium: #E2E2E2;
$grey: #F5F8FE;
$grey-dark: #595B5D;
$grey-blue: #CFD7ED;
diff --git a/app/assets/stylesheets/tpi/_emissions-chart.scss b/app/assets/stylesheets/tpi/_emissions-chart.scss
new file mode 100644
index 000000000..46a8e8074
--- /dev/null
+++ b/app/assets/stylesheets/tpi/_emissions-chart.scss
@@ -0,0 +1,189 @@
+@import "variables";
+
+.emissions {
+ padding-bottom: 30px;
+ &__filters {
+ margin-bottom: 66px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ justify-content: space-between;
+
+ &.assessments {
+ margin-top: 10px;
+ margin-bottom: 0px;
+ }
+
+ &__emissions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ }
+
+ &__country-selector {
+ .button {
+ padding: 12px 16px;
+ }
+
+ &__countries {
+ height: 0;
+ overflow: hidden;
+
+ &.--countries-open {
+ overflow: visible;
+ }
+
+ &__wrapper {
+ z-index: 10;
+ position: relative;
+ top: 18px;
+ padding: 24px;
+ border: 1px solid $grey-dark;
+ background: #fff;
+ }
+
+ &__label {
+ color: $grey-dark;
+ font-family: $family-sans-serif;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 15px;
+ }
+
+ &__list {
+ max-height: 300px;
+ overflow-y: auto;
+ margin-block: 24px;
+ position: relative;
+
+ li {
+ display: flex;
+ gap: 20px;
+ align-items: center;
+ margin-bottom: 16px;
+
+ input {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+
+ &__button {
+ display: flex;
+ justify-content: flex-end;
+ }
+ }
+ }
+ }
+ &__chart {
+ > div,
+ .highcharts-container {
+ overflow: visible !important;
+
+ @include until($breakpoint-desktop) {
+ .highcharts-axis-title {
+ transform: translateY(-15px) !important;
+ left: 0 !important;
+ }
+ }
+ }
+
+ &__tooltip {
+ > span {
+ background-color: #fff;
+ padding: 24px;
+ border: 1px solid $grey-dark;
+ }
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ color: $dark;
+ font-family: $family-sans-serif;
+ font-size: 12px;
+ font-weight: 700;
+ margin-bottom: 12px;
+ }
+ &__item {
+ display: flex;
+ justify-content: space-between;
+ gap: 30px;
+ > div {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .--target {
+ color: $grey-dark;
+ font-weight: 400;
+ }
+ }
+ }
+
+ &__legend {
+ margin-top: 60px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ @include until($breakpoint-desktop) {
+ margin-top: 30px;
+ }
+
+ &__label {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding: 7px 12px;
+ border: 1px solid rgba(89, 91, 93, 0.5);
+ color: $grey-dark;
+ font-family: $family-sans-serif;
+ font-size: 12px;
+
+ @include until($breakpoint-desktop) {
+ // margin-top: 30px;
+ padding: 6px 10px;
+ font-size: 10px;
+ }
+
+ span {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ display: inline-block;
+
+ @include until($breakpoint-desktop) {
+ width: 12px;
+ height: 12px;
+ }
+ }
+ }
+ }
+ .highcharts-legend > div > div {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ width: 80vw;
+ margin-top: 44px;
+
+ @include until($breakpoint-desktop) {
+ margin-top: 24px;
+ }
+
+ .highcharts-legend-item.highcharts-line-series {
+ position: relative !important;
+ top: 0 !important;
+ left: 0 !important;
+
+ span {
+ position: relative !important;
+ top: 0 !important;
+ left: 0 !important;
+ }
+ }
+ .highcharts-legend-item-hidden {
+ opacity: 0.5 !important;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/tpi/_info-tooltip.scss b/app/assets/stylesheets/tpi/_info-tooltip.scss
new file mode 100644
index 000000000..62ad7a806
--- /dev/null
+++ b/app/assets/stylesheets/tpi/_info-tooltip.scss
@@ -0,0 +1,4 @@
+.info-tooltip {
+ position: relative;
+ max-width: 200px;
+}
diff --git a/app/assets/stylesheets/tpi/_mq_beta_scores.scss b/app/assets/stylesheets/tpi/_mq_beta_scores.scss
index 9c61a68de..d7078a942 100644
--- a/app/assets/stylesheets/tpi/_mq_beta_scores.scss
+++ b/app/assets/stylesheets/tpi/_mq_beta_scores.scss
@@ -41,6 +41,13 @@
}
}
+ &__beta-button {
+ &.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+ }
&__beta-button:before {
content: "";
diff --git a/app/assets/stylesheets/tpi/_nested-dropdown.scss b/app/assets/stylesheets/tpi/_nested-dropdown.scss
index d56d69ce4..ae10a5794 100644
--- a/app/assets/stylesheets/tpi/_nested-dropdown.scss
+++ b/app/assets/stylesheets/tpi/_nested-dropdown.scss
@@ -111,4 +111,13 @@
color: $white;
}
}
+
+ &--ascor:not(.nested-dropdown--open) {
+ background: $ascor-background-color;
+
+ .nested-dropdown__title {
+ border: 1px solid $white;
+ color: $white;
+ }
+ }
}
diff --git a/app/assets/stylesheets/tpi/_publications.scss b/app/assets/stylesheets/tpi/_publications.scss
index f8f9fd804..e303d03f2 100644
--- a/app/assets/stylesheets/tpi/_publications.scss
+++ b/app/assets/stylesheets/tpi/_publications.scss
@@ -114,7 +114,7 @@ $max-lines: 3;
}
}
-.tpi-sector__promoted-publications, .tpi-banks__promoted-publications {
+.tpi-sector__promoted-publications, .tpi-banks__promoted-publications, .ascor-page__promoted-publications {
margin-bottom: 30px;
.view-all-btn__container {
diff --git a/app/assets/stylesheets/tpi/_selector.scss b/app/assets/stylesheets/tpi/_selector.scss
new file mode 100644
index 000000000..7939a319a
--- /dev/null
+++ b/app/assets/stylesheets/tpi/_selector.scss
@@ -0,0 +1,80 @@
+@import "variables";
+
+.selector__wrapper {
+ .selector__container {
+ min-width: 200px;
+ max-width: 300px;
+
+ .selector__header {
+ border: 1px solid #595b5d;
+ padding: 8px 16px;
+ display: flex;
+ justify-content: space-between;
+ cursor: pointer;
+
+ .selector__value {
+ width: 100%;
+ margin-right: 16px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .chevron-icon {
+ width: 16px;
+ height: 14px;
+ }
+
+ .chevron-icon-rotated {
+ transform: rotate(180deg);
+ }
+ }
+
+ .selector__header--active {
+ border-bottom: none;
+ }
+
+ .selector__options-wrapper {
+ height: 0;
+ overflow: hidden;
+
+ &--open {
+ overflow: visible;
+ }
+
+ .selector__options {
+ z-index: 10;
+ position: relative;
+ border: 1px solid #595b5d;
+ background-color: #fff;
+ padding-block: 10px;
+
+ .selector__option {
+ padding-inline: 16px;
+ color: $ascor-color;
+ font-family: $family-sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+ padding-block: 10px;
+
+ &.selector__option--selected {
+ background-color: $ascor-background-color;
+ color: #fff;
+ font-size: 18px;
+ font-weight: 600;
+ }
+
+ &:hover {
+ background-color: $ascor-background-color;
+ color: #fff;
+ }
+ }
+ }
+ }
+ }
+
+ .selector__container--active {
+ }
+}
diff --git a/app/assets/stylesheets/tpi/_variables.scss b/app/assets/stylesheets/tpi/_variables.scss
index bc717dba7..9f71fa7ab 100644
--- a/app/assets/stylesheets/tpi/_variables.scss
+++ b/app/assets/stylesheets/tpi/_variables.scss
@@ -71,6 +71,9 @@ $navbar-dropdown-item-hover-background-color: $blue;
$navbar-dropdown-item-active-color: white;
$navbar-dropdown-item-active-background-color: $blue;
$navbar-bottom-box-shadow-size: 0;
+$ascor-background-color: #242638;
+$ascor-color: #191919;
+$ascor-green: #17B091;
// TAGS
$tag-background-color: $white;
diff --git a/app/assets/stylesheets/tpi/country-question-legend.scss b/app/assets/stylesheets/tpi/country-question-legend.scss
new file mode 100644
index 000000000..483f5d52e
--- /dev/null
+++ b/app/assets/stylesheets/tpi/country-question-legend.scss
@@ -0,0 +1,107 @@
+.country-question-legend {
+ border: 1px solid $grey-lighter-medium;
+ background-color: $white;
+ position: fixed;
+
+ bottom: 0;
+ right: 0;
+
+ font-size: 12px;
+
+ z-index: 10;
+
+ transition: opacity .5s ease-out;
+ opacity: 0;
+
+ @include desktop {
+ bottom: unset;
+ right: unset;
+ top: 50%;
+ left: 15px;
+ }
+
+ @media (min-width: 1530px) {
+ left: unset;
+ right: calc(#{$widescreen} + (100vw - #{$widescreen}) / 2);
+ }
+
+ &--active {
+ opacity: 1;
+ }
+
+ &__header {
+ display: none;
+
+ padding: 10px;
+ text-transform: uppercase;
+ border-bottom: 1px solid #CFD7ED;
+
+ @include desktop {
+ display: block;
+ }
+ }
+
+ &__content {
+ padding: 15px;
+ display: flex;
+ gap: 15px;
+
+ @include desktop {
+ flex-direction: column;
+ }
+
+ .country-question-legend-answer {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ &:before {
+ content: '';
+ display: block;
+ margin-top: -3px;
+
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 9px;
+ }
+
+ &--no:before {
+ background-image: image-url('tpi/ascor/no.svg');
+ background-size: 20px;
+ }
+
+ &--yes:before {
+ background-image: image-url('tpi/ascor/yes.svg');
+ background-size: 20px;
+ }
+
+ &--not-applicable:before {
+ background-image: image-url('tpi/ascor/not_applicable.svg');
+ background-size: 20px;
+ }
+
+ &--no-data:before {
+ background-image: image-url('tpi/ascor/no_data.svg');
+ background-size: 20px;
+ }
+
+ &--partial:before {
+ background-image: image-url('tpi/ascor/partial.svg');
+ background-size: 20px;
+ }
+
+ &--no-disclosure:before {
+ background-image: image-url('tpi/ascor/no_disclosure.svg');
+ background-size: 20px;
+ }
+
+ &--excempt:before {
+ background-image: image-url('tpi/ascor/excempt.svg');
+ background-size: 20px;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/tpi/pages/ascor.scss b/app/assets/stylesheets/tpi/pages/ascor.scss
new file mode 100644
index 000000000..3da31a69e
--- /dev/null
+++ b/app/assets/stylesheets/tpi/pages/ascor.scss
@@ -0,0 +1,571 @@
+$see-more-width: 100px;
+$see-more-width-tablet: 130px;
+
+.ascor-page {
+ .dropdown-selector-wrapper {
+ position: relative;
+ background-color: $ascor-background-color;
+
+ .dropdown-selector__container {
+ background-color: $ascor-background-color;
+
+ @include desktop {
+ padding: $container-top-padding $container-side-padding 40px $container-side-padding;
+ }
+ }
+
+ .dropdown-selector__container--active {
+ background-color: $white;
+ }
+
+ .dropdown-selector__wrapper {
+ background-color: $ascor-background-color;
+ }
+
+ .dropdown-selector__active-button {
+ background-color: $dark;
+ }
+
+ .dropdown-selector__option {
+ &:hover {
+ background-color: $ascor-background-color;
+ color: #fff;
+ }
+ }
+
+ @include desktop {
+ height: 335px;
+ }
+
+ .left-icon {
+ background-image: image-url('tpi/ascor/circles_1.svg');
+ width: 100%;
+ height: 100%;
+ background-repeat: no-repeat;
+ position: absolute;
+ background-position-y: 30px;
+ }
+
+ .right-icon {
+ background-image: image-url('tpi/ascor/circles_2.svg');
+ width: 100%;
+ height: 100%;
+ background-repeat: no-repeat;
+ position: absolute;
+ background-position-x: 100%;
+ }
+
+ .bottom-icon {
+ background-image: image-url('tpi/ascor/logo.png');
+ width: 100%;
+ height: 100%;
+ background-repeat: no-repeat;
+ position: absolute;
+ background-position: 50px 260px;
+ }
+
+ .ascor-header {
+ min-height: 100px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+
+ &.show-version {
+ justify-content: space-between;
+
+ @include mobile {
+ justify-content: flex-end;
+ }
+
+ @include tablet {
+ padding-left: 20px;
+ }
+
+ @include desktop {
+ padding-left: calc((960px - 608px)/2);
+ }
+
+ @include widescreen {
+ padding-left: calc((1152px - 608px)/2);
+ }
+ }
+
+ &__assessment-dropdown {
+ display: flex;
+ gap: 10px;
+ color: white;
+ margin-right: 20px;
+
+ .caption {
+ line-height: 35px;
+ }
+
+ @include tablet {
+ flex-direction: row;
+ align-items: center;
+ }
+ }
+
+ .button {
+ background-color: $ascor-background-color;
+ }
+
+ .button:hover {
+ background-color: $dark;
+ }
+
+ a {
+ color: white;
+ margin-right: 20px;
+ }
+
+ .links {
+ padding-right: 20px;
+ }
+
+ @include until($desktop) {
+ padding: 0 0.75rem;
+ font-size: $size-7;
+ height: 60px;
+ font-family: $font-family-regular;
+ }
+ }
+ }
+
+ .base-tooltip__default-trigger {
+ background-color: $ascor-green;
+
+ &:hover {
+ background-color: transparentize($ascor-green, 0.4);
+ }
+ }
+
+ .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: 500px;
+ }
+ }
+
+ .emissions-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;
+ }
+ }
+
+ .emissions-chart-description {
+ margin-bottom: 40px;
+ font-size: 14px;
+ line-height: 1.75;
+
+ @include desktop {
+ max-width: 500px;
+ }
+ }
+
+ section {
+ margin-top: 70px;
+
+ > p {
+ margin-top: 30px;
+ }
+
+ @include until($desktop) {
+ margin-top: 40px;
+ padding: 0 0.75rem;
+
+ h4 {
+ font-size: $size-5 !important;
+ }
+
+ > p {
+ margin-top: 15px;
+ font-size: $size-6;
+ color: $grey-dark;
+ }
+ }
+ }
+
+ #methodology {
+ display: flex;
+
+ .pages__content {
+ margin-bottom: 0;
+
+ > div {
+ margin-top: 30px;
+ }
+
+ @include until($desktop) {
+ h4 {
+ font-size: $size-5 !important;
+ }
+
+ > div {
+ margin-top: 15px;
+ font-size: $size-6;
+ color: $grey-dark;
+ }
+ }
+ }
+ }
+
+ .button {
+ background-color: $ascor-green;
+ color: $white;
+ border: 0;
+
+ &:hover {
+ color: $white;
+ background-color: transparentize($ascor-green, 0.4);
+ }
+ }
+
+ .country-assessment {
+ margin-top: 60px;
+ margin-bottom: 60px;
+
+ &__pillar {
+ outline: solid 1px $grey-lighter-medium;
+ margin-bottom: 1px;
+ padding: 30px 0 0 10px;
+
+ &.active {
+ background-color: transparentize($ascor-background-color, 0.96);
+ }
+
+ &__title {
+ color: $dark;
+ font-family: $family-sans-serif;
+ font-size: 24px !important;
+ line-height: 1.7rem;
+ font-weight: 400;
+ text-transform: uppercase;
+ margin: 5px 0 40px 0;
+ }
+
+ &__subtitle {
+ color: $grey-dark;
+ font-family: $family-sans-serif;
+ font-size: 12px;
+ }
+
+ @include tablet {
+ padding: 30px 0 0 40px;
+ }
+ }
+
+ &__area {
+ outline: solid 1px $grey-lighter-medium;
+ padding: 15px 20px 30px 20px;
+ position: relative;
+ background-color: white;
+ display: flex;
+
+ &__title {
+ font-family: $family-sans-serif;
+ font-size: 20px !important;
+ display: flex;
+ align-items: center;
+ gap: 25px;
+
+ div:first-child {
+ min-width: 80px;
+ }
+ }
+
+ @include tablet {
+ margin-left: 40px;
+ padding: 15px 20px;
+ }
+ }
+
+ &__icon {
+ &:before {
+ content: '';
+ display: block;
+
+ min-width: 40px;
+ min-height: 40px;
+ border-radius: 50%;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 40px;
+ }
+
+ &--no:before {
+ background-image: image-url('tpi/ascor/no.svg');
+ }
+
+ &--yes:before {
+ background-image: image-url('tpi/ascor/yes.svg');
+ }
+
+ &--not-applicable:before {
+ background-image: image-url('tpi/ascor/not_applicable.svg');
+ }
+
+ &--no-data:before {
+ background-image: image-url('tpi/ascor/no_data.svg');
+ }
+
+ &--partial:before {
+ background-image: image-url('tpi/ascor/partial.svg');
+ }
+
+ &--no-disclosure:before {
+ background-image: image-url('tpi/ascor/no_disclosure.svg');
+ }
+
+ &--excempt:before {
+ background-image: image-url('tpi/ascor/excempt.svg');
+ }
+ }
+
+ input.toggle {
+ display: none;
+ }
+
+ &__indicators {
+ display: none;
+ }
+
+ &__indicator {
+ margin-bottom: 1px;
+ outline: solid 1px $grey-lighter-medium;
+ padding: 15px 45px 15px 20px;
+ display: flex;
+
+ &__title {
+ font-family: $family-sans-serif;
+ font-weight: 700;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ gap: 25px;
+ flex-grow: 2;
+ }
+
+ &__source {
+ font-family: $family-sans-serif;
+ font-size: 14px !important;
+ line-height: 40px;
+
+ a {
+ color: $grey-dark;
+ text-decoration: underline;
+ }
+
+ a:hover {
+ color: $dark;
+ }
+ }
+
+ @include tablet {
+ margin-left: 40px;
+ }
+ }
+
+ &__break {
+ flex-basis: 100%;
+ height: 0;
+ }
+
+ &__metric-block {
+ margin-left: 40px;
+ padding: 30px 45px 0 45px;
+ border-left: solid 3px $grey-lighter-medium;
+
+ @include tablet {
+ margin-left: 80px;
+ }
+ }
+
+ &__metric {
+ display: flex;
+ padding-bottom: 30px;
+ position: relative;
+ flex-wrap: wrap;
+
+ &__title {
+ flex-grow:2
+ }
+
+ &__text {
+ background: $dark;
+ padding: 5px 10px;
+ color: $white;
+ font-weight: 600;
+ margin-top: 10px;
+ border: solid 1px $grey-lighter-medium;
+ text-align: center;
+ min-width: 80px;
+ font-size: small;
+ }
+
+ &__source {
+ font-family: $family-sans-serif;
+ font-size: 14px !important;
+
+ a {
+ color: $grey-dark;
+ text-decoration: underline;
+ }
+
+ a:hover {
+ color: $dark;
+ }
+ }
+ }
+
+ &__more {
+ cursor: pointer;
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-left: 10px;
+ padding-right: 10px;
+ height: 30px;
+ width: $see-more-width;
+ right: -1px;
+ bottom: -1px;
+ border: 1px solid #CFD7ED;
+ user-select: none;
+
+ @include tablet {
+ width: $see-more-width-tablet;
+ }
+
+ &:before {
+ color: $grey-dark;
+ content: 'See more';
+ display: block;
+ font-size: 12px;
+ margin-top: 3px;
+ }
+
+ &:after {
+ content: '';
+ display: block;
+
+ background-image: image-url('icons/chevron-gray.svg');
+ background-repeat: no-repeat;
+ background-position: right center;
+ background-size: 16px;
+
+ height: 16px;
+ width: 16px;
+ }
+ }
+
+ input.toggle:checked + .country-assessment__area-block {
+ .country-assessment__indicators {
+ display: block;
+ }
+
+ .country-assessment__more {
+ &:before {
+ content: 'See less';
+ }
+
+ &::after {
+ transform: rotate(-180deg);
+ }
+ }
+ }
+ }
+
+ .contacts {
+ width: 100%;
+ height: 380px;
+ background-color: $ascor-green;
+ position: relative;
+
+ &__icon {
+ background-image: image-url('tpi/ascor/circles_3.svg');
+ width: 100%;
+ height: 100%;
+ background-repeat: no-repeat;
+ position: absolute;
+ display: none;
+
+ @include tablet {
+ background-position-x: calc(50% - 250px);
+ display: block;
+ }
+
+ @include widescreen {
+ background-position-x: 200px;
+ }
+ }
+
+ &__info {
+ padding-top: 80px;
+ margin: 0 auto;
+ width: 400px;
+
+ h3 {
+ font-family: $family-sans-serif;
+ font-size: 24px !important;
+ font-weight: 700;
+ padding: 20px 0;
+ }
+
+ p {
+ font-size: 14px;
+ margin-bottom: 50px
+ }
+
+ .button {
+ background-color: $ascor-background-color;
+ color: $white;
+ display: inline-block;
+
+ &:hover {
+ background-color: $dark;
+ border: 0;
+ }
+ }
+
+ &__link {
+ font-weight: bold;
+ text-decoration: underline;
+ }
+
+ @include tablet {
+ margin: 0 0 0 50%;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/tpi/pages/home.scss b/app/assets/stylesheets/tpi/pages/home.scss
index 83ba30312..aa2ee8190 100644
--- a/app/assets/stylesheets/tpi/pages/home.scss
+++ b/app/assets/stylesheets/tpi/pages/home.scss
@@ -169,7 +169,7 @@
background-position: right -70px bottom 30px;
}
- &.sovereign-bonds {
+ &.ascor {
background-color: $blue;
color: $white;
background-image: image-url('tpi/home/countries.svg');
@@ -805,4 +805,4 @@
width: 100% !important;
padding: 0 5px;
}
-}
\ No newline at end of file
+}
diff --git a/app/controllers/tpi/ascor_controller.rb b/app/controllers/tpi/ascor_controller.rb
new file mode 100644
index 000000000..1b9cedcf2
--- /dev/null
+++ b/app/controllers/tpi/ascor_controller.rb
@@ -0,0 +1,79 @@
+module TPI
+ class ASCORController < TPIController
+ before_action :fetch_ascor_countries, only: [:index, :show]
+ before_action :fetch_ascor_country, only: [:show, :show_assessment]
+ before_action :fetch_assessment_date, only: [:index, :show, :index_assessment, :show_assessment]
+ before_action :fetch_emissions_assessment_date, only: [:index, :emissions_chart_data]
+ before_action :fetch_ascor_assessment_results, only: [:index, :index_assessment]
+
+ def index
+ @assessment_dates = ASCOR::Assessment.pluck(:assessment_date).uniq
+ @publications_and_articles = TPISector.find_by(slug: 'ascor')&.publications_and_articles || []
+ ascor_page = TPIPage.find_by(slug: 'ascor')
+ @methodology_description = Content.find_by(page: ascor_page, code: 'methodology_description')
+ @methodology_id = Content.find_by(page: ascor_page, code: 'methodology_publication_id')
+ @methodology_publication = Publication.find_by(id: @methodology_id&.text)
+
+ fixed_navbar('ASCOR Countries', admin_ascor_countries_path)
+ end
+
+ def show
+ @assessment = ASCOR::Assessment.find_by country: @country, assessment_date: @assessment_date
+ @recent_emissions = Api::ASCOR::RecentEmissions.new(@assessment_date, @country).call
+ fixed_navbar("ASCOR Country #{@country.name}", admin_ascor_country_path(@country.id))
+ end
+
+ def index_assessment; end
+
+ def index_emissions_assessment; end
+
+ def show_assessment; end
+
+ def emissions_chart_data
+ data = ::Api::ASCOR::EmissionsChart.new(
+ @emissions_assessment_date,
+ params[:emissions_metric],
+ params[:emissions_boundary],
+ params[:country_ids]
+ ).call
+
+ render json: data
+ end
+
+ def user_download
+ render zip: {
+ 'ASCOR_countries.xlsx' => Api::CSVToExcel.new(CSVExport::ASCOR::Countries.new.call).call,
+ 'ASCOR_indicators.xlsx' => Api::CSVToExcel.new(CSVExport::ASCOR::AssessmentIndicators.new.call).call,
+ 'ASCOR_assessments_results.xlsx' => Api::CSVToExcel.new(CSVExport::ASCOR::Assessments.new.call).call,
+ 'ASCOR_benchmarks.xlsx' => Api::CSVToExcel.new(CSVExport::ASCOR::Benchmarks.new.call).call,
+ 'ASCOR_assessments_results_trends_pathways.xlsx' => Api::CSVToExcel.new(CSVExport::ASCOR::Pathways.new.call).call
+ }, filename: "TPI ASCOR data - #{Time.now.strftime('%d%m%Y')}"
+ end
+
+ private
+
+ def fetch_ascor_countries
+ @countries = ASCOR::Country.all.order(:name)
+ @countries_json = [
+ {name: 'All countries', path: tpi_ascor_index_path},
+ *@countries.as_json(only: [:name], methods: [:path])
+ ]
+ end
+
+ 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_emissions_assessment_date
+ @emissions_assessment_date = params[:emissions_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/helpers/ascor_helper.rb b/app/helpers/ascor_helper.rb
new file mode 100644
index 000000000..bc7c81810
--- /dev/null
+++ b/app/helpers/ascor_helper.rb
@@ -0,0 +1,16 @@
+module ASCORHelper
+ def ascor_icon_for(indicator, assessment)
+ value = ascor_assessment_result_for(indicator, assessment).answer.to_s.downcase.tr(' ', '-')
+ return 'no-data' unless value.in?(%w[yes no partial no-data no-disclosure not-applicable excempt])
+
+ value
+ end
+
+ def ascor_sub_indicators_for(indicator, sub_indicators)
+ sub_indicators.select { |i| i.code.include? indicator.code }
+ end
+
+ def ascor_assessment_result_for(indicator, assessment)
+ assessment.results.find { |r| r.indicator_id == indicator.id }
+ end
+end
diff --git a/app/javascript/components/tpi/AscorDropdown.js b/app/javascript/components/tpi/AscorDropdown.js
new file mode 100644
index 000000000..98559bc04
--- /dev/null
+++ b/app/javascript/components/tpi/AscorDropdown.js
@@ -0,0 +1,179 @@
+import React, {
+ useState,
+ useMemo,
+ useRef,
+ useEffect,
+ useCallback,
+ Fragment
+} from 'react';
+import PropTypes from 'prop-types';
+import Fuse from 'fuse.js';
+import cx from 'classnames';
+import chevronIcon from 'images/icons/white-chevron-down.svg';
+import chevronIconBlack from 'images/icon_chevron_dark/chevron_down_black-1.svg';
+
+const ESCAPE_KEY = 27;
+const ENTER_KEY = 13;
+
+const AscorSelector = ({ banks, selectedOption }) => {
+ const [searchValue, setSearchValue] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const inputEl = useRef(null);
+ const searchContainer = useRef(null);
+
+ const fuse = (opt) => {
+ const config = {
+ shouldSort: true,
+ threshold: 0.3,
+ keys: ['name']
+ };
+ const fuzzy = new Fuse(opt, config);
+ const searchResults = fuzzy.search(searchValue);
+ return searchResults;
+ };
+
+ const searchResults = useMemo(() => (searchValue ? fuse(banks) : []), [searchValue]);
+
+ const options = useMemo(() => (searchValue
+ ? searchResults : banks),
+ [searchValue, banks]);
+
+ const input = () => (
+ setSearchValue(e.target.value)}
+ placeholder="Type or select bank"
+ />
+ );
+
+ const header = () => (
+ {selectedOption}
+ );
+
+ const handleOptionClick = (option) => {
+ setIsOpen(false);
+ window.location = option.path;
+ };
+
+ const handleCloseDropdown = () => {
+ setIsOpen(false);
+ setSearchValue('');
+ };
+
+ const handleOpenSearch = () => {
+ if (!isOpen) setIsOpen(true);
+ };
+
+ // hooks
+
+ useEffect(() => {
+ if (isOpen) { inputEl.current.focus(); }
+ }, [isOpen]);
+
+ const escFunction = useCallback((event) => {
+ if (event.keyCode === ESCAPE_KEY) {
+ handleCloseDropdown();
+ }
+ }, []);
+
+ const enterFunction = (event) => {
+ if (event.keyCode === ENTER_KEY) {
+ if (isOpen && searchResults.length) {
+ handleOptionClick(searchResults[0]);
+ }
+ }
+ };
+
+ const handleClickOutside = useCallback((event) => {
+ if (searchContainer.current && !searchContainer.current.contains(event.target)) {
+ handleCloseDropdown();
+ }
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('keydown', escFunction, false);
+
+ return () => {
+ document.removeEventListener('keydown', escFunction, false);
+ };
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside);
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('keydown', enterFunction);
+
+ return () => {
+ document.removeEventListener('keydown', enterFunction);
+ };
+ }, [searchResults]);
+
+ return (
+
+
+
+
+
+ Select a country
+
+
+
+ {isOpen ? input() : header()}
+
isOpen && handleCloseDropdown()}
+ className={cx('chevron-icon', { 'chevron-icon-rotated': isOpen })}
+ src={isOpen ? chevronIconBlack : chevronIcon}
+ alt="chevron"
+ />
+
+ {isOpen && (
+
+
+ {(options.length && options.map((option, i) => (
+
handleOptionClick(option)}
+ className="dropdown-selector__option"
+ key={`${option.name}-${i}`}
+ >
+ {option.name}
+
+ ))) || (searchValue.length && !options.length && (
+
No results found.
+ ))}
+
+
+ )}
+
+
+
+ );
+};
+
+AscorSelector.propTypes = {
+ banks: PropTypes.array.isRequired,
+ selectedOption: PropTypes.string.isRequired
+};
+
+export default AscorSelector;
diff --git a/app/javascript/components/tpi/AscorQuestionLegend.js b/app/javascript/components/tpi/AscorQuestionLegend.js
new file mode 100644
index 000000000..cfae07a62
--- /dev/null
+++ b/app/javascript/components/tpi/AscorQuestionLegend.js
@@ -0,0 +1,72 @@
+import React, { useState, useEffect } from 'react';
+import cx from 'classnames';
+
+const AscorQuestionLegend = () => {
+ const [isVisible, setVisible] = useState(false);
+
+ const isChecked = () => {
+ let anyChecked = false;
+
+ document.querySelectorAll('.country-assessment > .country-assessment__pillar').forEach((section) => {
+ let areaChecked = false;
+
+ section.querySelectorAll('input.toggle').forEach((input) => {
+ areaChecked = areaChecked || input.checked;
+ anyChecked = anyChecked || input.checked;
+ });
+ return areaChecked ? section.classList.add('active') : section.classList.remove('active');
+ });
+
+ setVisible(anyChecked);
+ };
+
+ useEffect(() => {
+ const eventListeners = [];
+ document.querySelectorAll('.country-assessment > .country-assessment__pillar > input.toggle').forEach((input) => {
+ const listener = input.addEventListener('click', isChecked);
+ eventListeners.push([input, listener]);
+ });
+
+ return () => {
+ eventListeners.forEach(([input, listener]) => {
+ input.removeEventListener('click', listener);
+ });
+ };
+ });
+
+ return (
+
+
+ Legend
+
+
+
+ No
+
+
+ Partial
+
+
+ Yes
+
+
+ No data
+
+
+ Not applicable
+
+
+ No disclosure
+
+
+ Excempt
+
+
+
+ );
+};
+
+AscorQuestionLegend.propTypes = {
+};
+
+export default AscorQuestionLegend;
diff --git a/app/javascript/components/tpi/AscorRecentEmissions.js b/app/javascript/components/tpi/AscorRecentEmissions.js
new file mode 100644
index 000000000..3f809a73f
--- /dev/null
+++ b/app/javascript/components/tpi/AscorRecentEmissions.js
@@ -0,0 +1,132 @@
+import PropTypes from 'prop-types';
+import Select from './Select';
+import React, {useState} from 'react';
+
+const AscorRecentEmissions = ({
+ emissions_metric_filter,
+ default_emissions_metric_filter,
+ emissions_boundary_filter,
+ default_emissions_boundary_filter,
+ trend_filters,
+ default_trend_filter,
+ data
+}) => {
+ const [filters, setFilters] = useState({
+ emissions_metric: default_emissions_metric_filter,
+ emissions_boundary: default_emissions_boundary_filter,
+ trends: default_trend_filter
+ });
+ const recentEmissions = data.filter((d) => d.emissions_metric === filters.emissions_metric && d.emissions_boundary === filters.emissions_boundary)[0] || {};
+ const trend = recentEmissions.trend || {};
+ const trendValue = trend.values?.filter((t) => t.filter === filters.trends)[0] || {};
+
+ const handleSelect = (opt) => {
+ setFilters({ ...filters, [opt.name]: opt.value });
+ };
+
+ return (
+ <>
+
+
+ i. What is the country's most recent emissions level?
+
+ { recentEmissions.source && (
+
+ )}
+
+
+ { recentEmissions.value !== null && (
+ <>
+
+
+ {recentEmissions.value} {recentEmissions.unit}
+
+ >
+ )}
+
+
+
+ ii. What is the country's most recent emissions trend?
+
+ { trend.source && (
+
+ )}
+
+
+ { trendValue.value && (
+ <>
+
+
+ {trendValue.value}
+
+ >
+ )}
+
+ >
+ );
+};
+
+AscorRecentEmissions.propTypes = {
+ emissions_metric_filter: PropTypes.arrayOf(PropTypes.string).isRequired,
+ default_emissions_metric_filter: PropTypes.string.isRequired,
+ emissions_boundary_filter: PropTypes.arrayOf(PropTypes.string).isRequired,
+ default_emissions_boundary_filter: PropTypes.string.isRequired,
+ trend_filters: PropTypes.arrayOf(PropTypes.string).isRequired,
+ default_trend_filter: PropTypes.string.isRequired,
+ data: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.number,
+ source: PropTypes.string,
+ year: PropTypes.number,
+ unit: PropTypes.string.isRequired,
+ emissions_metric: PropTypes.string.isRequired,
+ emissions_boundary: PropTypes.string.isRequired,
+ trend: PropTypes.shape({
+ source: PropTypes.string,
+ year: PropTypes.number,
+ values: PropTypes.arrayOf(
+ PropTypes.shape({
+ filter: PropTypes.string.isRequired,
+ value: PropTypes.string
+ })
+ ).isRequired
+ }).isRequired
+ })
+ ).isRequired
+};
+
+export default AscorRecentEmissions;
diff --git a/app/javascript/components/tpi/InfoModal.js b/app/javascript/components/tpi/InfoModal.js
new file mode 100644
index 000000000..00a84d862
--- /dev/null
+++ b/app/javascript/components/tpi/InfoModal.js
@@ -0,0 +1,61 @@
+import PropTypes from 'prop-types';
+import CustomModal from './Modal';
+import { OverlayProvider } from '@react-aria/overlays';
+import React, {useEffect, useState} from 'react';
+
+const InfoModal = ({ title, text, element }) => {
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ const eventListeners = [];
+ document.querySelectorAll(element).forEach((input) => {
+ const listener = input.addEventListener('click', handleOnRequestOpen);
+ eventListeners.push([input, listener]);
+ });
+
+ return () => {
+ eventListeners.forEach(([input, listener]) => {
+ input.removeEventListener('click', listener);
+ });
+ };
+ });
+
+ const handleOnRequestOpen = () => {
+ setVisible(true);
+ };
+
+ const handleOnRequestClose = () => {
+ setVisible(false);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+InfoModal.propTypes = {
+ title: PropTypes.string.isRequired,
+ text: PropTypes.string.isRequired,
+ element: PropTypes.string.isRequired
+};
+
+export default InfoModal;
diff --git a/app/javascript/components/tpi/InfoTooltip.js b/app/javascript/components/tpi/InfoTooltip.js
new file mode 100644
index 000000000..4629c7a7d
--- /dev/null
+++ b/app/javascript/components/tpi/InfoTooltip.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactTooltip from 'react-tooltip';
+
+const InfoTooltip = ({ trigger, content, html }) => (
+
+ {html ?
: {trigger}
}
+
+
+);
+
+InfoTooltip.propTypes = {
+ trigger: PropTypes.any,
+ content: PropTypes.any.isRequired,
+ html: PropTypes.bool
+};
+
+InfoTooltip.defaultProps = {
+ trigger: (
+ ?
+ ),
+ html: false
+};
+
+export default InfoTooltip;
diff --git a/app/javascript/components/tpi/RemoteDropdown.js b/app/javascript/components/tpi/RemoteDropdown.js
index b847bc7cf..e05ebb421 100644
--- a/app/javascript/components/tpi/RemoteDropdown.js
+++ b/app/javascript/components/tpi/RemoteDropdown.js
@@ -43,7 +43,7 @@ function RemoteDropdown({ url, theme, params, data, selected, name }) {
window.Rails.fire(Select.current, 'change');
setLabel(item.label);
};
- const chevron = !isOpen && theme === 'blue' ? chevronIconWhite : chevronIconBlack;
+ const chevron = !isOpen && (theme === 'blue' || theme === 'ascor') ? chevronIconWhite : chevronIconBlack;
return (
<>
diff --git a/app/javascript/components/tpi/Select.js b/app/javascript/components/tpi/Select.js
new file mode 100644
index 000000000..abca539e0
--- /dev/null
+++ b/app/javascript/components/tpi/Select.js
@@ -0,0 +1,228 @@
+import React, {
+ useState,
+ useMemo,
+ useRef,
+ useEffect,
+ useCallback
+} from 'react';
+import PropTypes from 'prop-types';
+import Fuse from 'fuse.js';
+import cx from 'classnames';
+import chevronIconBlack from 'images/icon_chevron_dark/chevron_down_black-1.svg';
+
+const ESCAPE_KEY = 27;
+const ENTER_KEY = 13;
+
+const Select = ({
+ options,
+ name,
+ onSelect,
+ value,
+ allowSearch,
+ placeholder,
+ label
+}) => {
+ const [searchValue, setSearchValue] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const inputEl = useRef(null);
+ const headerRef = useRef(null);
+ const searchContainer = useRef(null);
+
+ const fuse = useCallback(
+ (opt) => {
+ const config = {
+ shouldSort: true,
+ threshold: 0.3,
+ keys: ['label']
+ };
+ const fuzzy = new Fuse(opt, config);
+ const searchResults = fuzzy.search(searchValue);
+ return searchResults;
+ },
+ [searchValue]
+ );
+
+ const _options = useMemo(
+ () => options.map((option) => (option.label && option.value ? option : { label: option, value: option })),
+ [options]
+ );
+
+ const searchResults = useMemo(
+ () => (searchValue ? fuse(_options) : []),
+ [searchValue, fuse, _options]
+ );
+
+ const filteredOptions = useMemo(
+ () => (searchValue ? searchResults : _options),
+ [searchValue, searchResults, _options]
+ );
+
+ const handleOptionClick = (opt) => {
+ setIsOpen(false);
+ onSelect({ name, value: opt.value, label: opt.label });
+ };
+
+ const handleCloseDropdown = () => {
+ setIsOpen(false);
+ setSearchValue('');
+ };
+
+ const handleOpenSearch = () => {
+ setIsOpen((open) => !open);
+ };
+
+ // hooks
+
+ useEffect(() => {
+ if (inputEl.current && isOpen) {
+ inputEl.current.focus();
+ }
+ }, [isOpen]);
+
+ const escFunction = useCallback((event) => {
+ if (event.keyCode === ESCAPE_KEY) {
+ handleCloseDropdown();
+ }
+ }, []);
+
+ const enterFunction = (event) => {
+ if (event.keyCode === ENTER_KEY) {
+ if (isOpen && searchResults.length) {
+ handleOptionClick(searchResults[0]);
+ }
+ }
+ };
+
+ const handleClickOutside = useCallback((event) => {
+ if (
+ searchContainer.current
+ && !searchContainer.current.contains(event.target)
+ ) {
+ handleCloseDropdown();
+ }
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('keydown', escFunction, false);
+ document.addEventListener('mousedown', handleClickOutside);
+ document.addEventListener('keydown', enterFunction);
+
+ return () => {
+ document.removeEventListener('keydown', escFunction, false);
+ document.removeEventListener('mousedown', handleClickOutside);
+ document.removeEventListener('keydown', enterFunction);
+ };
+ }, []);
+
+ const handleListKeyDown = (event) => {
+ if (event.keyCode === ENTER_KEY) {
+ handleOpenSearch();
+ }
+ };
+
+ useEffect(() => {
+ headerRef.current?.addEventListener('keydown', handleListKeyDown);
+ return () => {
+ headerRef.current.removeEventListener('keydown', handleListKeyDown);
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {isOpen && allowSearch ? (
+
setSearchValue(e.target.value)}
+ placeholder="Type or select"
+ />
+ ) : (
+
+ {value || placeholder}
+
+ )}
+
isOpen && handleCloseDropdown()}
+ className={cx('chevron-icon', {
+ 'chevron-icon-rotated': isOpen
+ })}
+ src={chevronIconBlack}
+ alt="chevron"
+ title={isOpen ? 'Close dropdown' : 'Open dropdown'}
+ />
+
+
+
+
+
+ {(filteredOptions.length
+ && filteredOptions.map((option, i) => (
+ handleOptionClick(option)}
+ className={`selector__option ${
+ option.value === value && 'selector__option--selected'
+ }`}
+ key={`${option.label}-${i}`}
+ >
+ {option.label}
+
+ )))
+ || (searchValue.length && !_options.length && (
+ No results found.
+ ))}
+
+
+
+
+
+ );
+};
+
+Select.propTypes = {
+ options: PropTypes.oneOfType([
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ PropTypes.arrayOf(PropTypes.string).isRequired
+ ]).isRequired,
+ onSelect: PropTypes.func.isRequired,
+ value: PropTypes.string,
+ allowSearch: PropTypes.bool,
+ placeholder: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+};
+
+Select.defaultProps = {
+ value: '',
+ allowSearch: false,
+ placeholder: ''
+};
+
+export default Select;
diff --git a/app/javascript/components/tpi/charts/ascor-bubble/Chart.js b/app/javascript/components/tpi/charts/ascor-bubble/Chart.js
new file mode 100644
index 000000000..c24b6a9ef
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-bubble/Chart.js
@@ -0,0 +1,212 @@
+import React, { useEffect, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import SingleCell from './SingleCell';
+
+import { SCORE_RANGES, VALUES } from './constants';
+import { groupBy, keys, pickBy, values } from 'lodash';
+
+import ChartMobile from './chart-mobile';
+
+const DESKTOP_MIN_WIDTH = 992;
+
+const SCALE = 1.25;
+
+// radius of bubbles
+const COMPANIES_MARKET_CAP_GROUPS = {
+ large: 10 * SCALE,
+ medium: 5 * SCALE,
+ small: 3 * SCALE
+};
+
+const SINGLE_CELL_SVG_WIDTH = 120;
+const SINGLE_CELL_SVG_HEIGHT = 100;
+
+let tooltip = null;
+
+const BubbleChart = ({ results }) => {
+ const tooltipEl = '
';
+ useEffect(() => {
+ document.body.insertAdjacentHTML('beforeend', tooltipEl);
+ tooltip = document.getElementById('bubble-chart-tooltip');
+ }, []);
+ const ranges = keys(SCORE_RANGES);
+
+ const parsedData = useMemo(
+ () => values(values(groupBy(results, 'pillar'))).map((value) => ({
+ pillar: value[0].pillar,
+ values: values(groupBy(value, 'area')).map((areaValues) => {
+ const vValues = pickBy(
+ groupBy(areaValues, 'result'),
+ (_value, key) => key in VALUES
+ );
+ const v = {
+ ...VALUES,
+ ...vValues
+ };
+ return {
+ area: areaValues[0].area,
+ values: values(v)
+ };
+ })
+ })),
+ [results]
+ );
+
+ const [isMobile, setIsMobile] = React.useState(true);
+
+ const handleResize = () => {
+ if (window.innerWidth < DESKTOP_MIN_WIDTH) {
+ setIsMobile(true);
+ } else {
+ setIsMobile(false);
+ }
+ };
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ handleResize();
+ window.addEventListener('resize', handleResize);
+ }
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ };
+ }, []);
+
+ return (
+
+
+
Pillar
+
Area
+
+ {ranges.map((range) => (
+
+ ))}
+
+ {isMobile ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
+
+const ForceLayoutBubbleChart = (countriesBubbles, uniqueKey) => {
+ const handleBubbleClick = (country) => window.open(country.path, '_blank');
+
+ return (
+
+ );
+};
+
+const getTooltipText = ({ tooltipContent }) => {
+ if (tooltipContent) {
+ return `
+
+ `;
+ }
+ return '';
+};
+
+const showTooltip = (node, u) => {
+ const bubble = u._groups[0][node.index];
+
+ tooltip.innerHTML = getTooltipText(node);
+ tooltip.removeAttribute('hidden');
+ const bubbleBoundingRect = bubble.getBoundingClientRect();
+ const topOffset = bubbleBoundingRect.top - tooltip.offsetHeight + window.scrollY;
+ const leftOffset = bubbleBoundingRect.left
+ + (bubbleBoundingRect.width - tooltip.offsetWidth) / 2
+ + window.scrollX;
+
+ tooltip.style.left = `${leftOffset}px`;
+ tooltip.style.top = `${topOffset}px`;
+};
+
+const hideTooltip = () => {
+ tooltip.setAttribute('hidden', true);
+};
+
+const ChartRows = ({ data }) => data?.map((pillar, pillarIndex) => {
+ const pillarName = pillar.pillar;
+ const pillarSpan = pillar.values.length;
+ const pillarAcronym = pillarName
+ .split(' ')
+ .map((word) => word[0])
+ .join('');
+
+ return (
+ <>
+
+
+ {pillarIndex + 1}. {pillarName}
+
+
+
+
+ {pillar.values.map(({ area, values: areaValues }, areaIndex) => (
+ <>
+
+ {pillarAcronym} {areaIndex + 1}. {area}
+
+ {areaValues.map((areaValuesResult, i) => {
+ const countriesBubbles = areaValuesResult.map((result) => ({
+ value: COMPANIES_MARKET_CAP_GROUPS[result.market_cap_group],
+ tooltipContent: {
+ header: result.country_name,
+ value: result.result
+ },
+ path: result.country_path,
+ color: result.color,
+ result: result.result
+ }));
+
+ // Remove special characters from the key to be able to use d3-select as it uses querySelector
+ const cleanKey = area.replace(/[^a-zA-Z\-_:.]/g, '');
+ const uniqueKey = `${cleanKey}-${areaIndex}-${i}`;
+ return (
+
+ {ForceLayoutBubbleChart(countriesBubbles, uniqueKey)}
+
+ );
+ })}
+ >
+ ))}
+ >
+ );
+});
+
+BubbleChart.propTypes = {
+ results: PropTypes.arrayOf(
+ PropTypes.shape({
+ area: PropTypes.string.isRequired,
+ market_cap_group: PropTypes.string.isRequired,
+ country_id: PropTypes.number.isRequired,
+ country_path: PropTypes.string.isRequired,
+ country_name: PropTypes.string.isRequired,
+ result: PropTypes.string.isRequired,
+ pillar: PropTypes.string.isRequired
+ })
+ ).isRequired
+};
+export default BubbleChart;
diff --git a/app/javascript/components/tpi/charts/ascor-bubble/SingleCell.js b/app/javascript/components/tpi/charts/ascor-bubble/SingleCell.js
new file mode 100644
index 000000000..ce1b2bf0a
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-bubble/SingleCell.js
@@ -0,0 +1,91 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { range } from 'd3-array';
+import { select } from 'd3-selection';
+import * as d3 from 'd3-force';
+import { SCORE_RANGES } from './constants';
+
+const SingleCell = ({
+ width,
+ height,
+ handleNodeClick,
+ data,
+ uniqueKey,
+ showTooltip,
+ hideTooltip
+}) => {
+ const computizedKey = uniqueKey.split(' ').join('_');
+ const key = `${computizedKey.replace(/[&]/g, '_')}-${(
+ Math.random() * 100
+ ).toFixed()}`;
+
+ const nodes = range(data.length).map(function (index) {
+ return {
+ color: SCORE_RANGES[data[index].result],
+ tooltipContent: data[index].tooltipContent,
+ path: data[index].path,
+ radius: data[index].value,
+ value: data[index].result
+ };
+ });
+
+ const simulation = () => {
+ d3.forceSimulation(nodes)
+ .force('charge', d3.forceManyBody().strength(10))
+ .force('y', d3.forceY().strength(0.3).y(0))
+ .force(
+ 'collision',
+ d3.forceCollide().radius(function (d) {
+ return d.radius + 1;
+ })
+ )
+ .on('tick', ticked);
+ };
+
+ const ticked = () => {
+ const u = select(`#${key}`).select('g').selectAll('circle').data(nodes);
+
+ u.enter()
+ .append('circle')
+ .attr('r', (d) => d.radius)
+ .style('fill', (d) => d.color)
+ .merge(u)
+ .attr('cx', (d) => d.x)
+ .attr('cy', (d) => d.y)
+ .on('mouseover', (d) => showTooltip(d, u))
+ .on('mouseout', hideTooltip)
+ .on('click', (d) => handleNodeClick(d));
+
+ u.exit().remove();
+ };
+
+ simulation();
+
+ return (
+
+
+
+
+
+ );
+};
+
+SingleCell.propTypes = {
+ showTooltip: PropTypes.func.isRequired,
+ hideTooltip: PropTypes.func.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired,
+ handleNodeClick: PropTypes.func.isRequired,
+ data: PropTypes.oneOfType([PropTypes.number, PropTypes.array]).isRequired,
+ uniqueKey: PropTypes.string.isRequired
+};
+
+export default SingleCell;
diff --git a/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/ChartMobile.js b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/ChartMobile.js
new file mode 100644
index 000000000..d663829cd
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/ChartMobile.js
@@ -0,0 +1,172 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import chevronIcon from 'images/icons/white-chevron-down.svg';
+import chevronIconBlack from 'images/icon_chevron_dark/chevron_down_black-1.svg';
+import { SCORE_RANGES } from '../constants';
+
+const Item = ({ title, children, className, isOpen, onOpen, icon }) => (
+
+
+
{title}
+
+
+
+ {children}
+
+
+);
+
+Item.propTypes = {
+ title: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ children: PropTypes.node,
+ onOpen: PropTypes.func.isRequired,
+ isOpen: PropTypes.bool,
+ icon: PropTypes.string.isRequired
+};
+Item.defaultProps = {
+ className: '',
+ children: null,
+ isOpen: false
+};
+
+const ResultItem = ({ result, i }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ {
+ const color = Object.values(SCORE_RANGES)[i];
+ const title = `${result.length} countries`;
+
+ return (
+
+ setIsOpen((prevState) => !prevState)}
+ >
+
+ {!isOpen &&
{title} }
+
+
+ {isOpen && (
+
+ {result.length ? (
+ result.map((country) => (
+ {country.country_name}
+ ))
+ ) : (
+ No countries
+ )}
+
+ )}
+
+ );
+ }
+};
+
+const ChartMobile = ({ data }) => {
+ const [openPillars, setOpenPillars] = useState([]);
+ const [openAreas, setOpenAreas] = useState([]);
+
+ const handleOpenAreas = (key) => {
+ setOpenAreas((prevState) => {
+ if (prevState.includes(key)) {
+ return prevState.filter((area) => area !== key);
+ }
+ return [...prevState, key];
+ });
+ };
+
+ const handleOpenPillars = (key) => {
+ if (openPillars.includes(key)) {
+ setOpenPillars((prevState) => prevState.filter((pillar) => pillar !== key));
+ setOpenAreas((prevState) => prevState.filter((area) => !area.includes(`${key}.`)));
+ return;
+ }
+ setOpenPillars((prevState) => [...prevState, key]);
+ };
+
+ return (
+
+
+ {data.map((pillar) => (
+ - handleOpenPillars(pillar.pillar)}
+ icon={chevronIcon}
+ >
+
+ {pillar.values.map((area, areaIndex) => {
+ const pillarAcronym = pillar.pillar
+ .split(' ')
+ .map((word) => word[0])
+ .join('');
+ const title = `${pillarAcronym} ${areaIndex + 1}. ${area.area}`;
+ return (
+ - handleOpenAreas(`${pillar.pillar}.${area.area}`)}
+ icon={chevronIconBlack}
+ >
+
+ {area.values.map((result, i) => (
+
+ ))}
+
+
+ );
+ })}
+
+
+ ))}
+
+
+ );
+};
+
+ChartMobile.propTypes = {
+ data: PropTypes.arrayOf(
+ PropTypes.shape({
+ pillar: PropTypes.string,
+ values: PropTypes.arrayOf(
+ PropTypes.shape({
+ area: PropTypes.string,
+ values: PropTypes.array
+ })
+ )
+ })
+ )
+};
+
+ChartMobile.defaultProps = {
+ data: []
+};
+
+export default ChartMobile;
diff --git a/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/index.js b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/index.js
new file mode 100644
index 000000000..dc8cbf4b0
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/index.js
@@ -0,0 +1,3 @@
+import ChartMobile from './ChartMobile';
+
+export default ChartMobile;
diff --git a/app/javascript/components/tpi/charts/ascor-bubble/constants.js b/app/javascript/components/tpi/charts/ascor-bubble/constants.js
new file mode 100644
index 000000000..f31d301d6
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-bubble/constants.js
@@ -0,0 +1,7 @@
+export const SCORE_RANGES = {
+ No: '#F26E6E',
+ Partial: '#F9A400',
+ Yes: '#17B091'
+};
+
+export const VALUES = { No: [], Partial: [], Yes: [] };
diff --git a/app/javascript/components/tpi/charts/ascor-bubble/index.js b/app/javascript/components/tpi/charts/ascor-bubble/index.js
new file mode 100644
index 000000000..9ec02597f
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-bubble/index.js
@@ -0,0 +1,3 @@
+import Chart from './Chart';
+
+export { Chart };
diff --git a/app/javascript/components/tpi/charts/ascor-emissions/Chart.js b/app/javascript/components/tpi/charts/ascor-emissions/Chart.js
new file mode 100644
index 000000000..21ff57afb
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-emissions/Chart.js
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types';
+
+import React from 'react';
+
+import Highcharts from 'highcharts';
+import HighchartsReact from 'highcharts-react-official';
+
+import { options } from './options';
+
+const EmissionsChart = ({ chartData }) => {
+ const { data, metadata } = chartData;
+
+ return (
+
+
+
+ );
+};
+
+export default EmissionsChart;
+
+EmissionsChart.propTypes = {
+ chartData: PropTypes.shape({
+ data: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ data: PropTypes.arrayOf(PropTypes.object).isRequired,
+ zoneAxis: PropTypes.string,
+ zones: PropTypes.arrayOf(PropTypes.object)
+ })
+ ),
+ metadata: PropTypes.shape({
+ unit: PropTypes.string.isRequired
+ })
+ })
+};
+
+EmissionsChart.defaultProps = {
+ chartData: {
+ data: [],
+ metadata: {
+ unit: ''
+ }
+ }
+};
diff --git a/app/javascript/components/tpi/charts/ascor-emissions/CountrySelector.js b/app/javascript/components/tpi/charts/ascor-emissions/CountrySelector.js
new file mode 100644
index 000000000..124bf9ea8
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-emissions/CountrySelector.js
@@ -0,0 +1,105 @@
+import React, { useState } from 'react';
+
+import PropTypes from 'prop-types';
+
+const CountrySelector = ({
+ countries,
+ selectedCountries: defaultSelectedCountries,
+ maxSelectedCountries,
+ onSaveCountries
+}) => {
+ const [countriesOpen, setCountriesOpen] = useState(false);
+ const [selectedCountries, setSelectedCountries] = useState(
+ defaultSelectedCountries
+ );
+
+ const _countries = countries.sort((a, b) => (a.name < b.name ? -1 : 1));
+
+ const handleSelectedCountry = (event) => {
+ const { checked, value } = event.target;
+
+ if (checked && selectedCountries.length >= maxSelectedCountries) {
+ return;
+ }
+
+ const parsedValue = parseInt(value, 10);
+
+ if (checked) {
+ setSelectedCountries([...selectedCountries, parsedValue]);
+ } else {
+ setSelectedCountries(
+ selectedCountries.filter((id) => id !== parsedValue)
+ );
+ }
+ };
+
+ const handleSaveCountries = () => {
+ setCountriesOpen(false);
+ onSaveCountries(selectedCountries);
+ };
+
+ return (
+
+
setCountriesOpen((open) => !open)}
+ type="button"
+ >
+ Choose countries to show
+
+
+
+
+ Add up to 10 countries simultaneously
+
+
+
+
+ Save
+
+
+
+
+
+ );
+};
+
+CountrySelector.propTypes = {
+ countries: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ iso: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ selectedCountries: PropTypes.arrayOf(PropTypes.number),
+ maxSelectedCountries: PropTypes.number.isRequired,
+ onSaveCountries: PropTypes.func.isRequired
+};
+
+CountrySelector.defaultProps = {
+ selectedCountries: []
+};
+
+export default CountrySelector;
diff --git a/app/javascript/components/tpi/charts/ascor-emissions/Emissions.js b/app/javascript/components/tpi/charts/ascor-emissions/Emissions.js
new file mode 100644
index 000000000..a3304fc98
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-emissions/Emissions.js
@@ -0,0 +1,96 @@
+import React, { useMemo, useState } from 'react';
+
+import PropTypes from 'prop-types';
+import Filters from './Filters';
+import EmissionsChart from './Chart';
+import { useChartData } from '../hooks';
+
+const initialData = {
+ data: [],
+ metadata: {
+ unit: ''
+ }
+};
+
+const Emissions = ({
+ emissions_metric_filter,
+ default_emissions_metric_filter,
+ emissions_boundary_filter,
+ default_emissions_boundary_filter,
+ countries,
+ default_countries,
+ emissions_data_url
+}) => {
+ const [filters, setFilters] = useState({
+ emissions_metric: default_emissions_metric_filter,
+ emissions_boundary: default_emissions_boundary_filter,
+ country_ids: default_countries
+ });
+
+ const onChangeFilters = (filter) => {
+ setFilters((_filters) => ({ ..._filters, ...filter }));
+ };
+
+ const { data } = useChartData(emissions_data_url, filters);
+
+ const chartData = useMemo(
+ () => (Array.isArray(data)
+ ? initialData
+ : {
+ ...data,
+ data: Object.entries(data.data).map(
+ ([countryId, { emissions, last_historical_year }]) => ({
+ name: countries.find(
+ (country) => country.id === Number(countryId)
+ ).name,
+ custom: { unit: data.metadata.unit },
+ data: Object.entries(emissions).map(([year, value]) => ({
+ x: Number(year),
+ y: value
+ })),
+ zoneAxis: 'x',
+ zones: [
+ {
+ value: last_historical_year
+ },
+ {
+ dashStyle: 'dash'
+ }
+ ]
+ })
+ )
+ }),
+ [countries, data]
+ );
+
+ return (
+
+
+
+
+ );
+};
+
+Emissions.propTypes = {
+ emissions_metric_filter: PropTypes.arrayOf(PropTypes.string).isRequired,
+ default_emissions_metric_filter: PropTypes.string.isRequired,
+ emissions_boundary_filter: PropTypes.arrayOf(PropTypes.string).isRequired,
+ default_emissions_boundary_filter: PropTypes.string.isRequired,
+ countries: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ iso: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ default_countries: PropTypes.arrayOf(PropTypes.number).isRequired,
+ emissions_data_url: PropTypes.string.isRequired
+};
+
+export default Emissions;
diff --git a/app/javascript/components/tpi/charts/ascor-emissions/Filters.js b/app/javascript/components/tpi/charts/ascor-emissions/Filters.js
new file mode 100644
index 000000000..d3db7a4f8
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-emissions/Filters.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Select from '../../Select';
+import CountrySelector from './CountrySelector';
+
+const MAX_SELECTED_COUNTRIES = 10;
+
+const Filters = ({
+ metrics,
+ boundaries,
+ countries,
+ filters: { emissions_metric, emissions_boundary, country_ids },
+ onChangeFilters
+}) => {
+ const handleSelect = (opt) => {
+ onChangeFilters({ [opt.name]: opt.value });
+ };
+
+ const handleSelectCountry = (_countries) => {
+ onChangeFilters({ country_ids: _countries });
+ };
+
+ return (
+
+ );
+};
+
+Filters.propTypes = {
+ metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
+ boundaries: PropTypes.arrayOf(PropTypes.string).isRequired,
+ countries: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ iso: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ filters: PropTypes.shape({
+ emissions_metric: PropTypes.string.isRequired,
+ emissions_boundary: PropTypes.string.isRequired,
+ country_ids: PropTypes.arrayOf(PropTypes.number).isRequired
+ }).isRequired,
+ onChangeFilters: PropTypes.func.isRequired
+};
+
+export default Filters;
diff --git a/app/javascript/components/tpi/charts/ascor-emissions/index.js b/app/javascript/components/tpi/charts/ascor-emissions/index.js
new file mode 100644
index 000000000..0c0b97a9d
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-emissions/index.js
@@ -0,0 +1,3 @@
+import Emissions from './Emissions';
+
+export default Emissions;
diff --git a/app/javascript/components/tpi/charts/ascor-emissions/mockedData.js b/app/javascript/components/tpi/charts/ascor-emissions/mockedData.js
new file mode 100644
index 000000000..86655b1b9
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-emissions/mockedData.js
@@ -0,0 +1,224 @@
+export default {
+ data: {
+ 1: {
+ emissions: {
+ 2005: 22,
+ 2006: 22,
+ 2007: 22,
+ 2008: 22,
+ 2009: 20,
+ 2010: 20,
+ 2011: 19,
+ 2012: 19,
+ 2013: 20,
+ 2014: 19,
+ 2015: 18,
+ 2016: 17,
+ 2017: 17,
+ 2018: 17,
+ 2019: 17,
+ 2020: 15,
+ 2021: 15,
+ 2022: 14,
+ 2023: 15,
+ 2024: 15,
+ 2025: 15,
+ 2026: 15,
+ 2027: 13,
+ 2028: 13,
+ 2029: 13,
+ 2030: 13
+ },
+ last_historical_year: 2020
+ },
+ 2: {
+ emissions: {
+ 2005: 12,
+ 2006: 12,
+ 2007: 10,
+ 2008: 10,
+ 2009: 10,
+ 2010: 10,
+ 2011: 10,
+ 2012: 10,
+ 2013: 10,
+ 2014: 9,
+ 2015: 9,
+ 2016: 9,
+ 2017: 9,
+ 2018: 9,
+ 2019: 9,
+ 2020: 9,
+ 2021: 9,
+ 2022: 9,
+ 2023: 9,
+ 2024: 9,
+ 2025: 7,
+ 2026: 7,
+ 2027: 7,
+ 2028: 7,
+ 2029: 7,
+ 2030: 7
+ },
+ last_historical_year: 2020
+ },
+ 3: {
+ emissions: {
+ 2005: 22,
+ 2006: 22,
+ 2007: 22,
+ 2008: 22,
+ 2009: 22,
+ 2010: 20,
+ 2011: 19,
+ 2012: 19,
+ 2013: 20,
+ 2014: 20,
+ 2015: 18,
+ 2016: 17,
+ 2017: 17,
+ 2018: 17,
+ 2019: 17,
+ 2020: 17,
+ 2021: 16,
+ 2022: 16,
+ 2023: 16,
+ 2024: 16,
+ 2025: 16,
+ 2026: 15,
+ 2027: 15,
+ 2028: 15,
+ 2029: 15,
+ 2030: 15
+ },
+ last_historical_year: 2020
+ },
+ 4: {
+ emissions: {
+ 2005: 10,
+ 2006: 12,
+ 2007: 14,
+ 2008: 16,
+ 2009: 18,
+ 2010: 20,
+ 2011: 19,
+ 2012: 19,
+ 2013: 20,
+ 2014: 19,
+ 2015: 18,
+ 2016: 17,
+ 2017: 17,
+ 2018: 16,
+ 2019: 16,
+ 2020: 15,
+ 2021: 15,
+ 2022: 14,
+ 2023: 14,
+ 2024: 14,
+ 2025: 14,
+ 2026: 13,
+ 2027: 13,
+ 2028: 13,
+ 2029: 13,
+ 2030: 13
+ },
+ last_historical_year: 2020
+ },
+ 14: {
+ emissions: {
+ 2005: 10,
+ 2006: 12,
+ 2007: 14,
+ 2008: 16,
+ 2009: 18,
+ 2010: 20,
+ 2011: 19,
+ 2012: 19,
+ 2013: 20,
+ 2014: 19,
+ 2015: 18,
+ 2016: 17,
+ 2017: 17,
+ 2018: 16,
+ 2019: 16,
+ 2020: 15,
+ 2021: 15,
+ 2022: 14,
+ 2023: 14,
+ 2024: 14,
+ 2025: 14,
+ 2026: 13,
+ 2027: 13,
+ 2028: 13,
+ 2029: 13,
+ 2030: 13
+ },
+ last_historical_year: 2020
+ },
+ 21: {
+ emissions: {
+ 2005: 10,
+ 2006: 12,
+ 2007: 14,
+ 2008: 16,
+ 2009: 18,
+ 2010: 20,
+ 2011: 19,
+ 2012: 19,
+ 2013: 20,
+ 2014: 19,
+ 2015: 18,
+ 2016: 17,
+ 2017: 17,
+ 2018: 16,
+ 2019: 16,
+ 2020: 15,
+ 2021: 15,
+ 2022: 14,
+ 2023: 14,
+ 2024: 14,
+ 2025: 14,
+ 2026: 13,
+ 2027: 13,
+ 2028: 13,
+ 2029: 13,
+ 2030: 13
+ },
+ last_historical_year: 2020
+ },
+ 22: {
+ emissions: {
+ 2005: 13,
+ 2006: 12,
+ 2007: 11,
+ 2008: 12,
+ 2009: 13,
+ 2010: 11,
+ 2011: 14,
+ 2012: 14,
+ 2013: 12,
+ 2014: 13,
+ 2015: 13,
+ 2016: 12,
+ 2017: 12,
+ 2018: 11,
+ 2019: 11,
+ 2020: 11,
+ 2021: 10,
+ 2022: 9,
+ 2023: 9,
+ 2024: 9,
+ 2025: 8,
+ 2026: 8,
+ 2027: 7,
+ 2028: 7,
+ 2029: 8,
+ 2030: 7
+ },
+ last_historical_year: 2020
+ }
+ },
+ metadata: {
+ unit: 'MtCO2e'
+ }
+};
diff --git a/app/javascript/components/tpi/charts/ascor-emissions/options.js b/app/javascript/components/tpi/charts/ascor-emissions/options.js
new file mode 100644
index 000000000..9f2ac1acc
--- /dev/null
+++ b/app/javascript/components/tpi/charts/ascor-emissions/options.js
@@ -0,0 +1,167 @@
+export const colors = [
+ '#17B091',
+ '#F26E6E',
+ '#5454C4',
+ '#B75038',
+ '#FFDD49',
+ '#00A8FF',
+ '#F602B4',
+ '#191919'
+];
+
+const tooltipLegendLine = (
+ dashStyle,
+ color
+) => `
+${
+ dashStyle === 'dash'
+ ? ` `
+ : ` `
+}
+ `;
+
+export const options = {
+ title: { text: '' },
+ colors,
+ yAxis: {
+ lineColor: '#595B5D',
+ lineWidth: 1,
+ visible: true,
+ tickColor: '#595B5D',
+ tickAmount: 8,
+ labels: {
+ padding: 10,
+ style: {
+ color: '#595B5D',
+ fontSize: '12px'
+ }
+ },
+ title: {
+ useHTML: true,
+ align: 'high'
+ }
+ },
+ credits: {
+ enabled: false
+ },
+ xAxis: {
+ lineColor: '#595B5D',
+ lineWidth: 1,
+ tickColor: '#595B5D',
+ tickInterval: 5,
+ labels: {
+ style: {
+ color: '#0A4BDC',
+ fontSize: '14px'
+ },
+ overflow: 'allow'
+ }
+ },
+ legend: {
+ layout: 'horizontal',
+ align: 'left',
+ verticalAlign: 'bottom',
+ padding: 50,
+ itemDistance: 12,
+ symbolHeight: 0,
+ symbolWidth: 0,
+ useHTML: true,
+ itemMarginBottom: 10,
+ labelFormatter() {
+ return ` ${this.name}
`;
+ },
+ className: 'emissions__chart__legend'
+ },
+ tooltip: {
+ shared: true,
+ headerFormat: ``,
+ pointFormatter() {
+ return ``;
+ },
+ style: {
+ color: '#191919',
+ fontSize: '14px'
+ },
+ borderWidth: 0,
+ crosshairs: true,
+ padding: 0,
+ shadow: false,
+ className: 'emissions__chart__tooltip',
+ useHTML: true
+ },
+ chart: {
+ height: 550,
+ showAxes: true
+ },
+ plotOptions: {
+ series: {
+ marker: {
+ enabled: false,
+ states: {
+ hover: {
+ enabled: false
+ }
+ }
+ }
+ }
+ },
+ series: [],
+ responsive: {
+ rules: [
+ {
+ condition: {
+ maxWidth: 992
+ },
+ chartOptions: {
+ legend: {
+ align: 'left',
+ padding: 0,
+ itemDistance: 10,
+ alignColumns: false,
+ margin: 0,
+ itemMarginTop: 0
+ },
+ yAxis: {
+ labels: {
+ align: 'center',
+ distance: 5,
+ padding: 0,
+ style: {
+ fontSize: '10px'
+ }
+ },
+ title: {
+ reserveSpace: false,
+ rotation: 0,
+ style: {
+ fontSize: '10px'
+ }
+ }
+ },
+ xAxis: {
+ labels: {
+ style: {
+ fontSize: '10px'
+ }
+ }
+ },
+ chart: {
+ height: 350
+ }
+ }
+ }
+ ]
+ }
+};
diff --git a/app/javascript/components/tpi/charts/hooks.js b/app/javascript/components/tpi/charts/hooks.js
index 199836d61..8e57f5965 100644
--- a/app/javascript/components/tpi/charts/hooks.js
+++ b/app/javascript/components/tpi/charts/hooks.js
@@ -1,12 +1,17 @@
+import { isEmpty } from 'lodash';
import { useEffect, useState } from 'react';
-export function useChartData(dataUrl) {
+export function useChartData(dataUrl, params = {}) {
const [data, setData] = useState([]);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
+ const url = isEmpty(params)
+ ? dataUrl
+ : `${dataUrl}?${new URLSearchParams(params)}`;
+
useEffect(() => {
- fetch(dataUrl)
+ fetch(url)
.then((r) => r.json())
.then((chartData) => {
setLoading(false);
@@ -16,7 +21,7 @@ export function useChartData(dataUrl) {
setLoading(false);
setError('Error while loading the data');
});
- }, [dataUrl]);
+ }, [url]);
return {
data,
diff --git a/app/models/ascor.rb b/app/models/ascor.rb
new file mode 100644
index 000000000..6ecbd743b
--- /dev/null
+++ b/app/models/ascor.rb
@@ -0,0 +1,5 @@
+module ASCOR
+ def self.table_name_prefix
+ 'ascor_'
+ end
+end
diff --git a/app/models/ascor/assessment.rb b/app/models/ascor/assessment.rb
new file mode 100644
index 000000000..743dd5dea
--- /dev/null
+++ b/app/models/ascor/assessment.rb
@@ -0,0 +1,22 @@
+# == Schema Information
+#
+# Table name: ascor_assessments
+#
+# id :bigint not null, primary key
+# country_id :bigint not null
+# assessment_date :date
+# publication_date :date
+# created_at :datetime not null
+# updated_at :datetime not null
+# notes :text
+#
+class ASCOR::Assessment < ApplicationRecord
+ belongs_to :country, class_name: 'ASCOR::Country', foreign_key: :country_id
+
+ has_many :results, class_name: 'ASCOR::AssessmentResult', foreign_key: :assessment_id, dependent: :destroy,
+ inverse_of: :assessment
+
+ validates_presence_of :assessment_date
+
+ accepts_nested_attributes_for :results, allow_destroy: true
+end
diff --git a/app/models/ascor/assessment_indicator.rb b/app/models/ascor/assessment_indicator.rb
new file mode 100644
index 000000000..909b58642
--- /dev/null
+++ b/app/models/ascor/assessment_indicator.rb
@@ -0,0 +1,24 @@
+# == Schema Information
+#
+# Table name: ascor_assessment_indicators
+#
+# id :bigint not null, primary key
+# indicator_type :string
+# code :string
+# text :text
+# created_at :datetime not null
+# updated_at :datetime not null
+# units_or_response_type :string
+#
+class ASCOR::AssessmentIndicator < ApplicationRecord
+ INDICATOR_TYPES = %w[pillar area indicator metric].freeze
+ enum indicator_type: array_to_enum_hash(INDICATOR_TYPES)
+
+ has_many :results, class_name: 'ASCOR::AssessmentResult', foreign_key: :indicator_id, dependent: :destroy
+
+ validates_presence_of :indicator_type, :code, :text
+
+ def to_s
+ "#{indicator_type} #{code}"
+ end
+end
diff --git a/app/models/ascor/assessment_result.rb b/app/models/ascor/assessment_result.rb
new file mode 100644
index 000000000..3275d559f
--- /dev/null
+++ b/app/models/ascor/assessment_result.rb
@@ -0,0 +1,22 @@
+# == Schema Information
+#
+# Table name: ascor_assessment_results
+#
+# id :bigint not null, primary key
+# assessment_id :bigint not null
+# indicator_id :bigint not null
+# answer :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# source :string
+# year :integer
+#
+class ASCOR::AssessmentResult < ApplicationRecord
+ belongs_to :assessment, class_name: 'ASCOR::Assessment', foreign_key: :assessment_id
+ 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/models/ascor/benchmark.rb b/app/models/ascor/benchmark.rb
new file mode 100644
index 000000000..922d72003
--- /dev/null
+++ b/app/models/ascor/benchmark.rb
@@ -0,0 +1,25 @@
+# == Schema Information
+#
+# Table name: ascor_benchmarks
+#
+# id :bigint not null, primary key
+# country_id :bigint not null
+# publication_date :date
+# emissions_metric :string
+# emissions_boundary :string
+# units :string
+# benchmark_type :string
+# emissions :jsonb
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+class ASCOR::Benchmark < ApplicationRecord
+ include HasEmissions
+
+ belongs_to :country, class_name: 'ASCOR::Country', foreign_key: :country_id
+
+ validates_presence_of :emissions_metric, :emissions_boundary, :units, :benchmark_type
+ validates :emissions_metric, inclusion: {in: ASCOR::EmissionsMetric::VALUES}, allow_nil: true
+ validates :emissions_boundary, inclusion: {in: ASCOR::EmissionsBoundary::VALUES}, allow_nil: true
+ validates :benchmark_type, inclusion: {in: ASCOR::BenchmarkType::VALUES}, allow_nil: true
+end
diff --git a/app/models/ascor/benchmark_type.rb b/app/models/ascor/benchmark_type.rb
new file mode 100644
index 000000000..d7e124829
--- /dev/null
+++ b/app/models/ascor/benchmark_type.rb
@@ -0,0 +1,6 @@
+class ASCOR::BenchmarkType
+ VALUES = [
+ 'National 1.5C benchmark',
+ 'Fair share 1.5C allocation'
+ ].freeze
+end
diff --git a/app/models/ascor/country.rb b/app/models/ascor/country.rb
new file mode 100644
index 000000000..1c4ae3f5d
--- /dev/null
+++ b/app/models/ascor/country.rb
@@ -0,0 +1,59 @@
+# == Schema Information
+#
+# Table name: ascor_countries
+#
+# id :bigint not null, primary key
+# name :string
+# slug :string
+# iso :string
+# region :string
+# wb_lending_group :string
+# fiscal_monitor_category :string
+# created_at :datetime not null
+# updated_at :datetime not null
+# type_of_party :string
+#
+class ASCOR::Country < ApplicationRecord
+ extend FriendlyId
+
+ REGIONS = [
+ 'Africa',
+ 'Asia',
+ 'Europe',
+ 'Latin America and Caribbean',
+ 'North America',
+ 'Oceania'
+ ].freeze
+ LENDING_GROUPS = [
+ 'High-income economies',
+ 'Upper-middle-income economies',
+ 'Lower-middle-income economies'
+ ].freeze
+ MONITOR_CATEGORIES = [
+ 'Advanced economies',
+ 'Emerging market economies',
+ 'Low-income developing countries'
+ ].freeze
+ TYPE_OF_PARTY = [
+ 'Annex I',
+ 'Non-Annex I'
+ ].freeze
+ DEFAULT_COUNTRIES = %w[USA CAN GBR FRA DEU ITA JPN RUS].freeze
+
+ friendly_id :name, use: [:slugged, :history], routes: :default
+
+ has_many :benchmarks, class_name: 'ASCOR::Benchmark', foreign_key: :country_id, dependent: :destroy
+ has_many :pathways, class_name: 'ASCOR::Pathway', foreign_key: :country_id, dependent: :destroy
+ has_many :assessments, class_name: 'ASCOR::Assessment', foreign_key: :country_id, dependent: :destroy
+
+ validates_presence_of :name, :slug, :iso, :region, :wb_lending_group, :fiscal_monitor_category
+ validates_uniqueness_of :name, :slug, :iso
+ validates :region, inclusion: {in: REGIONS}, allow_nil: true
+ validates :wb_lending_group, inclusion: {in: LENDING_GROUPS}, allow_nil: true
+ validates :fiscal_monitor_category, inclusion: {in: MONITOR_CATEGORIES}, allow_nil: true
+ validates :type_of_party, inclusion: {in: TYPE_OF_PARTY}, allow_nil: true
+
+ def path
+ Rails.application.routes.url_helpers.tpi_ascor_path slug
+ end
+end
diff --git a/app/models/ascor/emissions_boundary.rb b/app/models/ascor/emissions_boundary.rb
new file mode 100644
index 000000000..fc2d5e362
--- /dev/null
+++ b/app/models/ascor/emissions_boundary.rb
@@ -0,0 +1,7 @@
+class ASCOR::EmissionsBoundary
+ VALUES = [
+ 'Production - excluding LULUCF',
+ 'Production - only LULUCF',
+ 'Consumption - excluding LULUCF'
+ ].freeze
+end
diff --git a/app/models/ascor/emissions_metric.rb b/app/models/ascor/emissions_metric.rb
new file mode 100644
index 000000000..1dfe670c7
--- /dev/null
+++ b/app/models/ascor/emissions_metric.rb
@@ -0,0 +1,7 @@
+class ASCOR::EmissionsMetric
+ VALUES = [
+ 'Absolute',
+ 'Intensity per capita',
+ 'Intensity per GDP-PPP'
+ ].freeze
+end
diff --git a/app/models/ascor/pathway.rb b/app/models/ascor/pathway.rb
new file mode 100644
index 000000000..e85b56d15
--- /dev/null
+++ b/app/models/ascor/pathway.rb
@@ -0,0 +1,33 @@
+# == Schema Information
+#
+# Table name: ascor_pathways
+#
+# id :bigint not null, primary key
+# country_id :bigint not null
+# emissions_metric :string
+# emissions_boundary :string
+# units :string
+# assessment_date :date
+# publication_date :date
+# last_historical_year :integer
+# trend_1_year :string
+# trend_3_year :string
+# trend_5_year :string
+# emissions :jsonb
+# created_at :datetime not null
+# updated_at :datetime not null
+# trend_source :string
+# trend_year :integer
+# recent_emission_level :float
+# recent_emission_source :string
+# recent_emission_year :integer
+#
+class ASCOR::Pathway < ApplicationRecord
+ include HasEmissions
+
+ belongs_to :country, class_name: 'ASCOR::Country', foreign_key: :country_id
+
+ validates_presence_of :emissions_metric, :emissions_boundary, :units, :assessment_date
+ validates :emissions_metric, inclusion: {in: ASCOR::EmissionsMetric::VALUES}, allow_nil: true
+ validates :emissions_boundary, inclusion: {in: ASCOR::EmissionsBoundary::VALUES}, allow_nil: true
+end
diff --git a/app/models/company.rb b/app/models/company.rb
index c3a0f0709..7c710fcc4 100644
--- a/app/models/company.rb
+++ b/app/models/company.rb
@@ -75,6 +75,10 @@ def latest_mq_assessment
latest_mq_assessment_only_beta_methodologies || latest_mq_assessment_without_beta_methodologies
end
+ def beta_mq_assessments?
+ mq_assessments.only_beta_methodologies.exists?
+ end
+
def should_generate_new_friendly_id?
name_changed? || super
end
diff --git a/app/models/data_upload.rb b/app/models/data_upload.rb
index 0e107c16d..9fda519d1 100644
--- a/app/models/data_upload.rb
+++ b/app/models/data_upload.rb
@@ -15,6 +15,11 @@ class DataUpload < ApplicationRecord
DEV_UPLOADERS = %w[Documents].freeze
UPLOADERS = {
+ 'ASCOR Countries' => 'ASCORCountries',
+ 'ASCOR Benchmarks' => 'ASCORBenchmarks',
+ 'ASCOR Pathways' => 'ASCORPathways',
+ 'ASCOR Assessment Indicators' => 'ASCORAssessmentIndicators',
+ 'ASCOR Assessments' => 'ASCORAssessments',
'Banks' => 'Banks',
'Bank Assessment Indicators' => 'BankAssessmentIndicators',
'Bank Assessments' => 'BankAssessments',
diff --git a/app/services/api/ascor/bubble_chart.rb b/app/services/api/ascor/bubble_chart.rb
new file mode 100644
index 000000000..dfae7d345
--- /dev/null
+++ b/app/services/api/ascor/bubble_chart.rb
@@ -0,0 +1,64 @@
+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 :medium if recent_emission_level.blank?
+
+ market_cap_groups.find { |range, _| range.include?(recent_emission_level) }&.last || :medium
+ end
+
+ def pillars
+ @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
+ values = ::ASCOR::Pathway.where(MARKET_CAP_QUERY).where(assessment_date: assessment_date)
+ .where.not(recent_emission_level: nil).pluck(:recent_emission_level).sort
+ {values.first..values[(values.size * 1.0 / 3).ceil - 1] => :small,
+ values[(values.size * 1.0 / 3).ceil - 1]..values[(values.size * 2.0 / 3).ceil - 1] => :medium,
+ values[(values.size * 2.0 / 3).ceil - 1]..values.last => :large}
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/api/ascor/emissions_chart.rb b/app/services/api/ascor/emissions_chart.rb
new file mode 100644
index 000000000..f502ab86e
--- /dev/null
+++ b/app/services/api/ascor/emissions_chart.rb
@@ -0,0 +1,51 @@
+module Api
+ module ASCOR
+ class EmissionsChart
+ attr_accessor :assessment_date, :emissions_metric, :emissions_boundary, :country_ids
+
+ def initialize(assessment_date, emissions_metric, emissions_boundary, country_ids)
+ @assessment_date = assessment_date
+ @emissions_metric = emissions_metric || 'Absolute'
+ @emissions_boundary = emissions_boundary || 'Production - excluding LULUCF'
+ @country_ids = country_ids.is_a?(String) ? country_ids.split(',') : Array.wrap(country_ids)
+ end
+
+ def call
+ {data: collect_data, metadata: collect_metadata}
+ end
+
+ private
+
+ def collect_data
+ countries.each_with_object({}) do |country, result|
+ pathway = pathways[country.id]&.first
+ result[country.id] = {
+ emissions: pathway&.emissions || {},
+ last_historical_year: pathway&.last_historical_year
+ }
+ end
+ end
+
+ def collect_metadata
+ {unit: pathways.values.flatten.first&.units}
+ end
+
+ def countries
+ @countries ||= if country_ids.blank?
+ ::ASCOR::Country.where(iso: ::ASCOR::Country::DEFAULT_COUNTRIES)
+ else
+ ::ASCOR::Country.where(id: country_ids)
+ end
+ end
+
+ def pathways
+ @pathways ||= ::ASCOR::Pathway.where(
+ country: countries,
+ emissions_metric: emissions_metric,
+ emissions_boundary: emissions_boundary,
+ assessment_date: assessment_date
+ ).group_by(&:country_id)
+ end
+ end
+ end
+end
diff --git a/app/services/api/ascor/recent_emissions.rb b/app/services/api/ascor/recent_emissions.rb
new file mode 100644
index 000000000..fc6a60ba0
--- /dev/null
+++ b/app/services/api/ascor/recent_emissions.rb
@@ -0,0 +1,40 @@
+module Api
+ module ASCOR
+ class RecentEmissions
+ attr_accessor :assessment_date, :country
+
+ def initialize(assessment_date, country)
+ @assessment_date = assessment_date
+ @country = country
+ end
+
+ def call
+ pathways.map do |pathway|
+ {
+ value: pathway.recent_emission_level,
+ source: pathway.recent_emission_source,
+ year: pathway.recent_emission_year,
+ emissions_metric: pathway.emissions_metric,
+ emissions_boundary: pathway.emissions_boundary,
+ unit: pathway.units,
+ trend: {
+ source: pathway.trend_source,
+ year: pathway.trend_year,
+ values: [
+ {filter: '1 year trend', value: pathway.trend_1_year},
+ {filter: '3 years trend', value: pathway.trend_3_year},
+ {filter: '5 years trend', value: pathway.trend_5_year}
+ ]
+ }
+ }
+ end
+ end
+
+ private
+
+ def pathways
+ @pathways ||= ::ASCOR::Pathway.where(assessment_date: assessment_date, country: country)
+ end
+ end
+ end
+end
diff --git a/app/services/csv_export/ascor/assessment_indicators.rb b/app/services/csv_export/ascor/assessment_indicators.rb
new file mode 100644
index 000000000..f93559ea4
--- /dev/null
+++ b/app/services/csv_export/ascor/assessment_indicators.rb
@@ -0,0 +1,30 @@
+module CSVExport
+ module ASCOR
+ class AssessmentIndicators
+ HEADERS = ['Id', 'Type', 'Code', 'Text', 'Units or response type'].freeze
+
+ def call
+ # BOM UTF-8
+ CSV.generate("\xEF\xBB\xBF") do |csv|
+ csv << HEADERS
+
+ assessment_indicators.each do |indicator|
+ csv << [
+ indicator.id,
+ indicator.indicator_type,
+ indicator.code,
+ indicator.text,
+ indicator.units_or_response_type
+ ]
+ end
+ end
+ end
+
+ private
+
+ def assessment_indicators
+ @assessment_indicators ||= ::ASCOR::AssessmentIndicator.order(:id)
+ end
+ end
+ end
+end
diff --git a/app/services/csv_export/ascor/assessments.rb b/app/services/csv_export/ascor/assessments.rb
new file mode 100644
index 000000000..460ddabd6
--- /dev/null
+++ b/app/services/csv_export/ascor/assessments.rb
@@ -0,0 +1,72 @@
+module CSVExport
+ module ASCOR
+ class Assessments
+ def call
+ CSV.generate("\xEF\xBB\xBF") do |csv|
+ csv << headers
+
+ assessments.each do |assessment|
+ csv << [
+ assessment.id,
+ assessment.assessment_date,
+ assessment.publication_date,
+ assessment.country_id,
+ assessment.country.name,
+ *answer_values_for(assessment),
+ *source_values_for(assessment),
+ *year_values_for(assessment),
+ assessment.notes
+ ]
+ end
+ end
+ end
+
+ private
+
+ def headers
+ result = ['Id', 'Assessment date', 'Publication date', 'Country Id', 'Country']
+ result += assessment_indicators.reject { |i| i.indicator_type == 'pillar' || i.code.in?(%w[EP.1.a.i EP.1.a.ii]) }
+ .map { |i| "#{i.indicator_type} #{i.code}" }
+ result += assessment_indicators.select { |i| i.indicator_type.in?(%w[indicator metric]) }
+ .map { |i| "source #{i.indicator_type} #{i.code}" }
+ result += assessment_indicators.select { |i| i.indicator_type == 'metric' }
+ .map { |i| "year #{i.indicator_type} #{i.code}" }
+ result += ['Notes']
+ result
+ end
+
+ def answer_values_for(assessment)
+ assessment_indicators.reject { |i| i.indicator_type == 'pillar' || i.code.in?(%w[EP.1.a.i EP.1.a.ii]) }
+ .map do |indicator|
+ assessment_results[[assessment.id, indicator.id]]&.first&.answer
+ end
+ end
+
+ def source_values_for(assessment)
+ assessment_indicators.select { |i| i.indicator_type.in?(%w[indicator metric]) }
+ .map do |indicator|
+ assessment_results[[assessment.id, indicator.id]]&.first&.source
+ end
+ end
+
+ def year_values_for(assessment)
+ assessment_indicators.select { |i| i.indicator_type == 'metric' }
+ .map do |indicator|
+ assessment_results[[assessment.id, indicator.id]]&.first&.year
+ end
+ end
+
+ def assessments
+ @assessments ||= ::ASCOR::Assessment.joins(:country).includes(:country).order(:assessment_date, 'ascor_countries.name')
+ end
+
+ def assessment_results
+ @assessment_results ||= ::ASCOR::AssessmentResult.all.group_by { |r| [r.assessment_id, r.indicator_id] }
+ end
+
+ def assessment_indicators
+ @assessment_indicators ||= ::ASCOR::AssessmentIndicator.order(:id)
+ end
+ end
+ end
+end
diff --git a/app/services/csv_export/ascor/benchmarks.rb b/app/services/csv_export/ascor/benchmarks.rb
new file mode 100644
index 000000000..2ce163a71
--- /dev/null
+++ b/app/services/csv_export/ascor/benchmarks.rb
@@ -0,0 +1,46 @@
+module CSVExport
+ module ASCOR
+ class Benchmarks
+ HEADERS = [
+ 'Id',
+ 'Country',
+ 'Publication date',
+ 'Emissions metric',
+ 'Emissions boundary',
+ 'Units',
+ 'Benchmark type'
+ ].freeze
+
+ def call
+ CSV.generate("\xEF\xBB\xBF") do |csv|
+ csv << (HEADERS + year_columns)
+
+ benchmarks.each do |benchmark|
+ csv << [
+ benchmark.id,
+ benchmark.country.name,
+ benchmark.publication_date,
+ benchmark.emissions_metric,
+ benchmark.emissions_boundary,
+ benchmark.units,
+ benchmark.benchmark_type,
+ year_columns.map do |year|
+ benchmark.emissions[year]
+ end
+ ].flatten
+ end
+ end
+ end
+
+ private
+
+ def year_columns
+ @year_columns ||= benchmarks.flat_map(&:emissions_all_years).uniq.sort
+ end
+
+ def benchmarks
+ @benchmarks ||= ::ASCOR::Benchmark.joins(:country).includes(:country).order('ascor_countries.name')
+ end
+ end
+ end
+end
diff --git a/app/services/csv_export/ascor/countries.rb b/app/services/csv_export/ascor/countries.rb
new file mode 100644
index 000000000..8c5b6ae61
--- /dev/null
+++ b/app/services/csv_export/ascor/countries.rb
@@ -0,0 +1,39 @@
+module CSVExport
+ module ASCOR
+ class Countries
+ HEADERS = [
+ 'Id',
+ 'Name',
+ 'Country ISO code',
+ 'Region',
+ 'World Bank lending group',
+ 'International Monetary Fund fiscal monitor category',
+ 'Type of Party to the United Nations Framework Convention on Climate Change'
+ ].freeze
+
+ def call
+ CSV.generate("\xEF\xBB\xBF") do |csv|
+ csv << HEADERS
+
+ countries.each do |country|
+ csv << [
+ country.id,
+ country.name,
+ country.iso,
+ country.region,
+ country.wb_lending_group,
+ country.fiscal_monitor_category,
+ country.type_of_party
+ ]
+ end
+ end
+ end
+
+ private
+
+ def countries
+ @countries ||= ::ASCOR::Country.order(:name)
+ end
+ end
+ end
+end
diff --git a/app/services/csv_export/ascor/pathways.rb b/app/services/csv_export/ascor/pathways.rb
new file mode 100644
index 000000000..d54a4e253
--- /dev/null
+++ b/app/services/csv_export/ascor/pathways.rb
@@ -0,0 +1,65 @@
+module CSVExport
+ module ASCOR
+ class Pathways
+ HEADERS = [
+ 'Id',
+ 'Country',
+ 'Emissions metric',
+ 'Emissions boundary',
+ 'Units',
+ 'Assessment date',
+ 'Publication date',
+ 'Last historical year',
+ 'metric EP1.a.i',
+ 'source metric EP1.a.i',
+ 'year metric EP1.a.i',
+ 'metric EP1.a.ii 1-year',
+ 'metric EP1.a.ii 3-year',
+ 'metric EP1.a.ii 5-year',
+ 'source metric EP1.a.ii',
+ 'year metric EP1.a.ii'
+ ].freeze
+
+ def call
+ CSV.generate("\xEF\xBB\xBF") do |csv|
+ csv << (HEADERS + year_columns)
+
+ pathways.each do |pathway|
+ csv << [
+ pathway.id,
+ pathway.country.name,
+ pathway.emissions_metric,
+ pathway.emissions_boundary,
+ pathway.units,
+ pathway.assessment_date,
+ pathway.publication_date,
+ pathway.last_historical_year,
+ pathway.recent_emission_level,
+ pathway.recent_emission_source,
+ pathway.recent_emission_year,
+ pathway.trend_1_year,
+ pathway.trend_3_year,
+ pathway.trend_5_year,
+ pathway.trend_source,
+ pathway.trend_year,
+ year_columns.map do |year|
+ pathway.emissions[year]
+ end
+ ].flatten
+ end
+ end
+ end
+
+ private
+
+ def year_columns
+ @year_columns ||= pathways.flat_map(&:emissions_all_years).uniq.sort
+ end
+
+ def pathways
+ @pathways ||= ::ASCOR::Pathway.joins(:country).includes(:country)
+ .order(:assessment_date, 'ascor_countries.name')
+ end
+ end
+ end
+end
diff --git a/app/services/csv_import/ascor_assessment_indicators.rb b/app/services/csv_import/ascor_assessment_indicators.rb
new file mode 100644
index 000000000..b45ad20d6
--- /dev/null
+++ b/app/services/csv_import/ascor_assessment_indicators.rb
@@ -0,0 +1,41 @@
+module CSVImport
+ class ASCORAssessmentIndicators < BaseImporter
+ include Helpers
+
+ def import
+ import_each_csv_row(csv) do |row|
+ indicator = prepare_indicator(row)
+
+ indicator.indicator_type = row[:type].downcase
+ indicator.code = row[:code]
+ indicator.text = row[:text]
+ indicator.units_or_response_type = row[:units_or_response_type] if row.header?(:units_or_response_type)
+
+ was_new_record = indicator.new_record?
+ any_changes = indicator.changed?
+
+ indicator.save!
+
+ update_import_results(was_new_record, any_changes)
+ end
+ end
+
+ private
+
+ def resource_klass
+ ASCOR::AssessmentIndicator
+ end
+
+ def required_headers
+ [:id, :code, :type, :text]
+ end
+
+ def prepare_indicator(row)
+ find_record_by(:id, row) ||
+ resource_klass.find_or_initialize_by(
+ code: row[:code],
+ indicator_type: row[:type]
+ )
+ end
+ end
+end
diff --git a/app/services/csv_import/ascor_assessments.rb b/app/services/csv_import/ascor_assessments.rb
new file mode 100644
index 000000000..c9c6e4740
--- /dev/null
+++ b/app/services/csv_import/ascor_assessments.rb
@@ -0,0 +1,70 @@
+module CSVImport
+ class ASCORAssessments < BaseImporter
+ include Helpers
+
+ def import
+ import_each_csv_row(csv) do |row|
+ assessment = prepare_assessment(row)
+
+ assessment.country = countries[row[:country]].first if row.header?(:country)
+ assessment.assessment_date = assessment_date(row) if row.header?(:assessment_date)
+ assessment.publication_date = publication_date(row) if row.header?(:publication_date)
+ assessment.notes = row[:notes] if row.header?(:notes)
+
+ was_new_record = assessment.new_record?
+
+ assessment.save!
+ save_assessment_results! assessment, row
+
+ update_import_results(was_new_record, !was_new_record)
+ end
+ end
+
+ private
+
+ def header_converters
+ converter = lambda { |header| header.squish.tr(' ', '_').downcase.underscore.to_sym }
+ [converter]
+ end
+
+ def resource_klass
+ ASCOR::Assessment
+ end
+
+ def required_headers
+ [:id]
+ end
+
+ def prepare_assessment(row)
+ find_record_by(:id, row) ||
+ ASCOR::Assessment.find_or_initialize_by(
+ country: countries[row[:country]].first,
+ assessment_date: assessment_date(row)
+ )
+ end
+
+ def save_assessment_results!(assessment, row)
+ ASCOR::AssessmentIndicator.all.each do |indicator|
+ value_key = "#{indicator.indicator_type}_#{indicator.code.underscore}".to_sym
+ result = ASCOR::AssessmentResult.find_or_initialize_by(assessment: assessment, indicator: indicator)
+ result.answer = row[value_key] if row.header?(value_key)
+ [:source, :year].each do |attr|
+ result.public_send("#{attr}=", row["#{attr}_#{value_key}".to_sym]) if row.header?("#{attr}_#{value_key}".to_sym)
+ end
+ result.save!
+ end
+ end
+
+ def countries
+ @countries ||= ASCOR::Country.all.group_by(&:name)
+ end
+
+ def assessment_date(row)
+ CSVImport::DateUtils.safe_parse!(row[:assessment_date], ['%Y-%m-%d', '%m/%d/%y']) if row[:assessment_date]
+ end
+
+ def publication_date(row)
+ CSVImport::DateUtils.safe_parse!(row[:publication_date], ['%Y-%m'])
+ end
+ end
+end
diff --git a/app/services/csv_import/ascor_benchmarks.rb b/app/services/csv_import/ascor_benchmarks.rb
new file mode 100644
index 000000000..2be256985
--- /dev/null
+++ b/app/services/csv_import/ascor_benchmarks.rb
@@ -0,0 +1,54 @@
+module CSVImport
+ class ASCORBenchmarks < BaseImporter
+ include Helpers
+
+ def import
+ import_each_csv_row(csv) do |row|
+ benchmark = prepare_benchmark(row)
+
+ benchmark.country = countries[row[:country]].first if row.header?(:country)
+ benchmark.publication_date = parse_date(row[:publication_date]) if row.header?(:publication_date)
+ benchmark.emissions_metric = row[:emissions_metric] if row.header?(:emissions_metric)
+ benchmark.emissions_boundary = row[:emissions_boundary] if row.header?(:emissions_boundary)
+ benchmark.units = row[:units] if row.header?(:units)
+ benchmark.benchmark_type = row[:benchmark_type] if row.header?(:benchmark_type)
+ benchmark.emissions = parse_emissions(row, thousands_separator: ',') if emission_headers?(row)
+
+ was_new_record = benchmark.new_record?
+ any_changes = benchmark.changed?
+
+ benchmark.save!
+
+ update_import_results(was_new_record, any_changes)
+ end
+ end
+
+ private
+
+ def resource_klass
+ ASCOR::Benchmark
+ end
+
+ def required_headers
+ [:id]
+ end
+
+ def prepare_benchmark(row)
+ find_record_by(:id, row) ||
+ ASCOR::Benchmark.find_or_initialize_by(
+ country: countries[row[:country]].first,
+ emissions_metric: row[:emissions_metric],
+ emissions_boundary: row[:emissions_boundary],
+ benchmark_type: row[:benchmark_type]
+ )
+ end
+
+ def countries
+ @countries ||= ASCOR::Country.all.group_by(&:name)
+ end
+
+ def parse_date(date)
+ CSVImport::DateUtils.safe_parse!(date, ['%Y-%m', '%Y-%m-%d'])
+ end
+ end
+end
diff --git a/app/services/csv_import/ascor_countries.rb b/app/services/csv_import/ascor_countries.rb
new file mode 100644
index 000000000..24cf8f6fd
--- /dev/null
+++ b/app/services/csv_import/ascor_countries.rb
@@ -0,0 +1,45 @@
+module CSVImport
+ class ASCORCountries < BaseImporter
+ include Helpers
+
+ def import
+ import_each_csv_row(csv) do |row|
+ country = prepare_country row
+
+ country.name = row[:name] if row.header?(:name)
+ country.iso = row[:country_iso_code] if row.header?(:country_iso_code)
+ country.region = row[:region] if row.header?(:region)
+ country.wb_lending_group = row[:world_bank_lending_group] if row.header?(:world_bank_lending_group)
+ if row.header?(:international_monetary_fund_fiscal_monitor_category)
+ country.fiscal_monitor_category = row[:international_monetary_fund_fiscal_monitor_category]
+ end
+ if row.header?(:type_of_party_to_the_united_nations_framework_convention_on_climate_change)
+ country.type_of_party = row[:type_of_party_to_the_united_nations_framework_convention_on_climate_change]
+ end
+
+ was_new_record = country.new_record?
+ any_changes = country.changed?
+
+ country.save!
+
+ update_import_results(was_new_record, any_changes)
+ end
+ end
+
+ private
+
+ def resource_klass
+ ASCOR::Country
+ end
+
+ def required_headers
+ [:id]
+ end
+
+ def prepare_country(row)
+ find_record_by(:id, row) ||
+ find_record_by(:iso, row, column_name: :country_iso_code) ||
+ resource_klass.new
+ end
+ end
+end
diff --git a/app/services/csv_import/ascor_pathways.rb b/app/services/csv_import/ascor_pathways.rb
new file mode 100644
index 000000000..3322d7b97
--- /dev/null
+++ b/app/services/csv_import/ascor_pathways.rb
@@ -0,0 +1,69 @@
+module CSVImport
+ class ASCORPathways < BaseImporter
+ include Helpers
+
+ def import
+ import_each_csv_row(csv) do |row|
+ pathway = prepare_pathway(row)
+
+ pathway.country = countries[row[:country]].first if row.header?(:country)
+ pathway.emissions_metric = row[:emissions_metric] if row.header?(:emissions_metric)
+ pathway.emissions_boundary = row[:emissions_boundary] if row.header?(:emissions_boundary)
+ pathway.units = row[:units] if row.header?(:units)
+ pathway.assessment_date = assessment_date(row) if row.header?(:assessment_date)
+ pathway.publication_date = publication_date(row) if row.header?(:publication_date)
+ pathway.last_historical_year = row[:last_historical_year] if row.header?(:last_historical_year)
+ pathway.trend_1_year = row[:metric_ep1aii_1year] if row.header?(:metric_ep1aii_1year)
+ pathway.trend_3_year = row[:metric_ep1aii_3year] if row.header?(:metric_ep1aii_3year)
+ pathway.trend_5_year = row[:metric_ep1aii_5year] if row.header?(:metric_ep1aii_5year)
+ pathway.trend_source = row[:source_metric_ep1aii] if row.header?(:source_metric_ep1aii)
+ pathway.trend_year = row[:year_metric_ep1aii] if row.header?(:year_metric_ep1aii)
+ if row.header?(:metric_ep1ai)
+ pathway.recent_emission_level = string_to_float(row[:metric_ep1ai], thousands_separator: ',')
+ end
+ pathway.recent_emission_source = row[:source_metric_ep1ai] if row.header?(:source_metric_ep1ai)
+ pathway.recent_emission_year = row[:year_metric_ep1ai] if row.header?(:year_metric_ep1ai)
+ pathway.emissions = parse_emissions(row, thousands_separator: ',') if emission_headers?(row)
+
+ was_new_record = pathway.new_record?
+ any_changes = pathway.changed?
+
+ pathway.save!
+
+ update_import_results(was_new_record, any_changes)
+ end
+ end
+
+ private
+
+ def resource_klass
+ ASCOR::Pathway
+ end
+
+ def required_headers
+ [:id]
+ end
+
+ def prepare_pathway(row)
+ find_record_by(:id, row) ||
+ ASCOR::Pathway.find_or_initialize_by(
+ country: countries[row[:country]].first,
+ emissions_metric: row[:emissions_metric],
+ emissions_boundary: row[:emissions_boundary],
+ assessment_date: assessment_date(row)
+ )
+ end
+
+ def countries
+ @countries ||= ASCOR::Country.all.group_by(&:name)
+ end
+
+ def assessment_date(row)
+ CSVImport::DateUtils.safe_parse!(row[:assessment_date], ['%Y-%m-%d', '%m/%d/%y']) if row[:assessment_date]
+ end
+
+ def publication_date(row)
+ CSVImport::DateUtils.safe_parse!(row[:publication_date], ['%Y-%m'])
+ end
+ end
+end
diff --git a/app/services/csv_import/base_importer.rb b/app/services/csv_import/base_importer.rb
index e0812a66c..3d5903a26 100644
--- a/app/services/csv_import/base_importer.rb
+++ b/app/services/csv_import/base_importer.rb
@@ -51,8 +51,8 @@ def csv
@csv ||= parse_csv
end
- def find_record_by(attr_name, row)
- resource_klass.find_by(attr_name.to_sym => row[attr_name]&.strip)
+ def find_record_by(attr_name, row, column_name: nil)
+ resource_klass.find_by(attr_name.to_sym => row[column_name || attr_name]&.strip)
end
def prepare_overridden_resource(row)
diff --git a/app/services/csv_import/helpers/emissions.rb b/app/services/csv_import/helpers/emissions.rb
index 8e93ea2ad..2abae9907 100644
--- a/app/services/csv_import/helpers/emissions.rb
+++ b/app/services/csv_import/helpers/emissions.rb
@@ -3,17 +3,24 @@ module Helpers
module Emissions
EMISSION_YEAR_PATTERN = /^\d{4}$/.freeze
- def parse_emissions(row)
+ def parse_emissions(row, thousands_separator: '')
row.headers.grep(EMISSION_YEAR_PATTERN).reduce({}) do |acc, year|
- next acc unless row[year].present?
+ next acc if row[year].blank? || row[year] == 'NA'
- acc.merge(year.to_s.to_i => row[year].to_f)
+ acc.merge(year.to_s.to_i => string_to_float(row[year], thousands_separator: thousands_separator))
end
end
def emission_headers?(row)
row.headers.grep(EMISSION_YEAR_PATTERN).any?
end
+
+ def string_to_float(string, thousands_separator: ',')
+ return nil if string.blank?
+ return string.to_f unless string.is_a?(String)
+
+ string.delete(thousands_separator).delete("\t").delete(' ').to_f
+ end
end
end
end
diff --git a/app/services/seed/tpi_data.rb b/app/services/seed/tpi_data.rb
index 2eec3df20..ba67a6ea5 100644
--- a/app/services/seed/tpi_data.rb
+++ b/app/services/seed/tpi_data.rb
@@ -59,6 +59,26 @@ def call
TimedLogger.log('Create Publications') do
create_publications
end
+
+ TimedLogger.log('Import ASCOR Countries') do
+ run_importer CSVImport::ASCORCountries.new(seed_file('ascor_countries.csv'))
+ end
+
+ TimedLogger.log('Import ASCOR Benchmarks') do
+ run_importer CSVImport::ASCORBenchmarks.new(seed_file('ascor_benchmarks.csv'))
+ end
+
+ TimedLogger.log('Import ASCOR Pathways') do
+ run_importer CSVImport::ASCORPathways.new(seed_file('ascor_pathways.csv'))
+ end
+
+ TimedLogger.log('Import ASCOR Assessment Indicators') do
+ run_importer CSVImport::ASCORAssessmentIndicators.new(seed_file('ascor_assessment_indicators.csv'))
+ end
+
+ TimedLogger.log('Import ASCOR Assessments') do
+ run_importer CSVImport::ASCORAssessments.new(seed_file('ascor_assessments.csv'))
+ end
end
def import_sector_clusters
@@ -190,6 +210,7 @@ def import_sectors
end
end
TPISector.find_or_create_by!(name: 'Banks', show_in_tpi_tool: false)
+ TPISector.find_or_create_by!(name: 'ASCOR', show_in_tpi_tool: false)
end
end
end
diff --git a/app/views/layouts/tpi/_banner.html.erb b/app/views/layouts/tpi/_banner.html.erb
index 6d60362ab..53b57fe2d 100644
--- a/app/views/layouts/tpi/_banner.html.erb
+++ b/app/views/layouts/tpi/_banner.html.erb
@@ -10,26 +10,7 @@
/>
<% end %>
-
+ <%= content_for?(:hosted_by) ? content_for(:hosted_by) : render('layouts/tpi/hosted_by') %>
diff --git a/app/views/layouts/tpi/_header.html.erb b/app/views/layouts/tpi/_header.html.erb
index 2ba23f620..d912fbc09 100644
--- a/app/views/layouts/tpi/_header.html.erb
+++ b/app/views/layouts/tpi/_header.html.erb
@@ -16,8 +16,8 @@
path: tpi_banks_path
},
{
- title: 'Sovereign bonds issuers',
- path: tpi_publications_path(tags: 'ASCOR')
+ title: 'ASCOR',
+ path: tpi_ascor_index_path
}
],
active: active_menu_page?([
diff --git a/app/views/layouts/tpi/_hosted_by.html.erb b/app/views/layouts/tpi/_hosted_by.html.erb
new file mode 100644
index 000000000..404c1b97b
--- /dev/null
+++ b/app/views/layouts/tpi/_hosted_by.html.erb
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/app/views/tpi/ascor/_assessment.html.erb b/app/views/tpi/ascor/_assessment.html.erb
new file mode 100644
index 000000000..1db2dd630
--- /dev/null
+++ b/app/views/tpi/ascor/_assessment.html.erb
@@ -0,0 +1,69 @@
+<% pillars = ASCOR::AssessmentIndicator.pillar.order(:id) %>
+<% areas = ASCOR::AssessmentIndicator.area.order(:id) %>
+<% indicators = ASCOR::AssessmentIndicator.indicator.order(:id) %>
+<% metrics = ASCOR::AssessmentIndicator.metric.order(:id) %>
+
+<%= react_component('AscorQuestionLegend') %>
+<% pillars.each_with_index do |pillar, i| %>
+
+
<%= "Pillar #{i + 1}" %>
+
<%= pillar.text %>
+
+ <% ascor_sub_indicators_for(pillar, areas).each do |area| %>
+
+
+
+
+
<%= "Area #{area.code}" %>
+
<%= area.text %>
+
+
+
+
+
+ <% ascor_sub_indicators_for(area, indicators).each do |indicator| %>
+
+
+
+ <%= "#{indicator.code.split('.').last}. #{indicator.text}" %>
+
+ <% if ascor_assessment_result_for(indicator, @assessment).source.present? %>
+
+ <%= link_to 'Source', ascor_assessment_result_for(indicator, @assessment).source %>
+
+ <% end %>
+
+
+ <% ascor_sub_indicators_for(indicator, metrics).each do |metric| %>
+ <% next if metric.code == 'EP.1.a.ii' # skipped because EP.1.a.i and EP.1.a.ii are rendered via same React component %>
+
+
+ <% if metric.code == 'EP.1.a.i' %>
+ <%= render 'tpi/ascor/metrics_ep1a', recent_emissions: @recent_emissions %>
+ <% else %>
+
+
+ <%= "#{metric.code.split('.').last}. #{metric.text}" %>
+
+ <% if ascor_assessment_result_for(metric, @assessment).source.present? %>
+
+ <%= link_to "Source (#{ascor_assessment_result_for(metric, @assessment).year})", ascor_assessment_result_for(metric, @assessment).source %>
+
+ <% end %>
+ <% if ascor_assessment_result_for(metric, @assessment).answer.present? %>
+
+
+ <%= ascor_assessment_result_for(metric, @assessment).answer %>
+
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+
+
+ <% end %>
+
+<% end %>
\ No newline at end of file
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..93395cd03
--- /dev/null
+++ b/app/views/tpi/ascor/_bubble_chart.html.erb
@@ -0,0 +1,7 @@
+<%= react_component('charts/ascor-bubble/Chart', { results: @ascor_assessment_results }) %>
+<%= react_component('InfoModal', {
+ title: "ASCOR country assessment results",
+ text: "Countries are assessed across the ASCOR framework’s three pillars and thirteen topic areas.
" \
+ "The area-level result is Yes if all indicators within the area are assessed as Yes, Partial if some of the indicators within the area are assessed as Yes, and No if all of the indicators within the area are assessed as No.
",
+ element: "#bubble-chart-info"
+}) %>
\ No newline at end of file
diff --git a/app/views/tpi/ascor/_contact.html.erb b/app/views/tpi/ascor/_contact.html.erb
new file mode 100644
index 000000000..a06418e1f
--- /dev/null
+++ b/app/views/tpi/ascor/_contact.html.erb
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/app/views/tpi/ascor/_emissions_chart.html.erb b/app/views/tpi/ascor/_emissions_chart.html.erb
new file mode 100644
index 000000000..e4e69777a
--- /dev/null
+++ b/app/views/tpi/ascor/_emissions_chart.html.erb
@@ -0,0 +1,16 @@
+<%= react_component('charts/ascor-emissions', {
+ emissions_metric_filter: ASCOR::EmissionsMetric::VALUES,
+ default_emissions_metric_filter: 'Absolute',
+ emissions_boundary_filter: ASCOR::EmissionsBoundary::VALUES,
+ default_emissions_boundary_filter: 'Production - excluding LULUCF',
+ countries: ASCOR::Country.all.sort_by(&:name).map { |c| { id: c.id, iso: c.iso, name: c.name } },
+ default_countries: ASCOR::Country.where(iso: ASCOR::Country::DEFAULT_COUNTRIES).map(&:id),
+ emissions_data_url: emissions_chart_data_tpi_ascor_index_path
+}) %>
+<%= react_component('InfoModal', {
+ title: "Country emission pathways",
+ text: "Country emission pathways are assessed in several ways to account for a variety of factors and uncertainties. The emission metrics considered include the following options: production and consumption-based emissions; exclusion of LULUCF emissions and LULUCF emissions alone; and emissions on an absolute and intensity basis (per capita and per PPP-adjusted GDP).
" \
+ "Targeted future pathways are included only for absolute production-based emissions excluding LULUCF as this is the basis on which 2030 targets are assessed.
" \
+ "Pathways end in 2030 because long-term net zero targets are often stated on a different emission boundary from the one considered in the 2030 target assessments (e.g. including only CO₂ emissions rather than all Kyoto greenhouse gases).
",
+ element: "#emissions-chart-info"
+}) %>
\ 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..9196d3334
--- /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('#bubble-chart');
+})();
diff --git a/app/views/tpi/ascor/_index_emissions_assessment.js.erb b/app/views/tpi/ascor/_index_emissions_assessment.js.erb
new file mode 100644
index 000000000..8a922fca4
--- /dev/null
+++ b/app/views/tpi/ascor/_index_emissions_assessment.js.erb
@@ -0,0 +1,7 @@
+(function () {
+ document.getElementById('emissions-chart').innerHTML = "<%= j render('emissions_chart') %>";
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.set('emissions_assessment_date', <%= @assessment_date %>);
+ window.history.replaceState({}, '', newUrl.href);
+ ReactRailsUJS.mountComponents('#emissions-charts');
+})();
diff --git a/app/views/tpi/ascor/_metrics_ep1a.html.erb b/app/views/tpi/ascor/_metrics_ep1a.html.erb
new file mode 100644
index 000000000..e240fc3b6
--- /dev/null
+++ b/app/views/tpi/ascor/_metrics_ep1a.html.erb
@@ -0,0 +1,9 @@
+<%= react_component('AscorRecentEmissions', {
+ emissions_metric_filter: ASCOR::EmissionsMetric::VALUES,
+ default_emissions_metric_filter: 'Absolute',
+ emissions_boundary_filter: ASCOR::EmissionsBoundary::VALUES,
+ default_emissions_boundary_filter: 'Production - excluding LULUCF',
+ trend_filters: ['1 year trend', '3 years trend', '5 years trend'],
+ default_trend_filter: '1 year trend',
+ data: recent_emissions
+}) %>
\ No newline at end of file
diff --git a/app/views/tpi/ascor/_show_assessment.js.erb b/app/views/tpi/ascor/_show_assessment.js.erb
new file mode 100644
index 000000000..1cccca213
--- /dev/null
+++ b/app/views/tpi/ascor/_show_assessment.js.erb
@@ -0,0 +1,7 @@
+(function () {
+ document.getElementById('assessment').innerHTML = "<%= j render('assessment') %>";
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.set('assessment_date', <%= @assessment_date %>);
+ window.history.replaceState({}, '', newUrl.href);
+ ReactRailsUJS.mountComponents('#assessment');
+})();
\ No newline at end of file
diff --git a/app/views/tpi/ascor/index.html.erb b/app/views/tpi/ascor/index.html.erb
new file mode 100644
index 000000000..a6879d5af
--- /dev/null
+++ b/app/views/tpi/ascor/index.html.erb
@@ -0,0 +1,134 @@
+<% content_for :page_title, "ASCOR Tool - Transition Pathway Initiative" %>
+<% content_for :hosted_by do %> <% end %>
+
+
+
+
+
+
+
+ <%= react_component("AscorDropdown", { banks: @countries_json, selectedOption: 'All countries' }) %>
+
+
+
+
+
+
+
+ Countries are assessed across the ASCOR framework’s three pillars and thirteen topic areas.
+
+
+ <%= render 'tpi/ascor/bubble_chart' %>
+
+
+
+
+
+
+ Country emissions assessed using different metrics, focusing on absolute production-based emissions, excluding LULUCF, to align with 2030 targets.
+
+
+ <%= render 'tpi/ascor/emissions_chart' %>
+
+
+
+ <% if @methodology_publication.present? %>
+
+
+
+
Methodology
+
+
+ <%= @methodology_description&.text&.html_safe %>
+
+
+
+ <%= render 'tpi/publications/list', publications_and_articles: [@methodology_publication] %>
+
+
+
+ <% end %>
+
+
+
+
+
+
+
+ <% if @publications_and_articles.any? %>
+ <%= render "tpi/publications/promoted", publications_and_articles: @publications_and_articles, count: @publications_and_articles.count %>
+ <% else %>
+
+ There are currently no articles related to ASCOR available, but check all our other publications and news.
+
+ <% end %>
+
+
+
+
+ <%= render 'contact' %>
+
\ No newline at end of file
diff --git a/app/views/tpi/ascor/show.html.erb b/app/views/tpi/ascor/show.html.erb
new file mode 100644
index 000000000..c38d0940f
--- /dev/null
+++ b/app/views/tpi/ascor/show.html.erb
@@ -0,0 +1,43 @@
+<% content_for :page_title, "#{@country.name} - Transition Pathway Initiative" %>
+<% content_for :hosted_by do %> <% end %>
+
+
+
+
+
+
+
+ <%= react_component("AscorDropdown", { banks: @countries_json, selectedOption: @country.name }) %>
+
+
+
+
+ <% if @assessment.present? %>
+
+ <%= render 'assessment', assessment: @assessment %>
+
+ <% end %>
+
+ <%= render 'contact' %>
+
\ No newline at end of file
diff --git a/app/views/tpi/companies/show.html.erb b/app/views/tpi/companies/show.html.erb
index 3ad25bccf..90cf136bc 100644
--- a/app/views/tpi/companies/show.html.erb
+++ b/app/views/tpi/companies/show.html.erb
@@ -15,7 +15,7 @@