show france map with stats for each departement

This commit is contained in:
Christophe Robillard 2023-11-09 15:46:02 +01:00
parent 4a698f8264
commit ba876f5085
13 changed files with 585 additions and 0 deletions

View file

@ -0,0 +1,64 @@
@import "colors";
$dep-nothing: #E3E3FD; // blue-france-925
$dep-small: #CACAFB; // blue-france-850
$dep-medium: #8585F6; // blue-france-625
$dep-large: #313178; // blue-france-200
$dep-xlarge: #272747; // blue-france-125
#map-svg {
max-width: 100%;
height: auto;
}
#map-infos {
min-width: 328px;
}
.departement.nothing {
fill: $dep-nothing;
}
.departement.small {
fill: $dep-small;
}
.departement.medium {
fill: $dep-medium;
}
.departement.large {
fill: $dep-large;
}
.departement.xlarge {
fill: $dep-xlarge;
}
.legends {
.legend {
width: 60px;
height: 10px;
display: inline-block;
}
.nothing {
background-color: $dep-nothing;
}
.small {
background-color: $dep-small;
}
.medium {
background-color: $dep-medium;
}
.large {
background-color: $dep-large;
}
.xlarge {
background-color: $dep-xlarge;
}
}

View file

@ -0,0 +1,23 @@
class CarteController < ApplicationController
def show
@map_filter = MapFilter.new(params)
@map_filter.stats = stats
end
private
def stats
departements_sql = "select departement, count(procedures.id) as nb_demarches, sum(procedures.estimated_dossiers_count) as nb_dossiers from services inner join procedures on services.id = procedures.service_id where procedures.hidden_at is null and procedures.aasm_state in ('publiee', 'close', 'depubliee')"
departements_sql += " and procedures.published_at >= '#{@map_filter.year}-01-01' and procedures.published_at <= '#{@map_filter.year}-12-31'" if @map_filter.year.present?
departements_sql += " group by services.departement"
departements = ActiveRecord::Base.connection.execute(ActiveRecord::Base.sanitize_sql(departements_sql))
departements.to_a.reduce(Hash.new({ nb_demarches: 0, nb_dossiers: 0 })) do |h, v|
h.merge(
v["departement"] => {
'nb_demarches': v["nb_demarches"].presence || 0,
'nb_dossiers': v['nb_dossiers'].presence || 0
}
)
end
end
end

View file

@ -0,0 +1,19 @@
module CarteHelper
def svg_path(map_filter, departement, d)
tag.path(
class: "land departement departement#{departement} #{map_filter.css_class_for_departement(departement)}",
'stroke-width': ".5",
d: d,
data: {
departement: name_for_departement(departement),
demarches: map_filter.nb_demarches_for_departement(departement),
dossiers: map_filter.nb_dossiers_for_departement(departement),
action: "mouseenter->map-info#showInfo mouseout->map-info#hideInfo"
}
)
end
def name_for_departement(departement)
"#{departement.upcase} - #{APIGeoService.departement_name(departement.upcase)}"
end
end

View file

@ -0,0 +1,32 @@
import { Controller } from '@hotwired/stimulus';
import { hide, show } from '@utils';
export class MapInfoController extends Controller {
static targets = ['infos', 'departement', 'demarches', 'dossiers'];
declare readonly infosTarget: HTMLDivElement;
declare readonly departementTarget: HTMLDivElement;
declare readonly demarchesTarget: HTMLDivElement;
declare readonly dossiersTarget: HTMLDivElement;
showInfo(event: Event) {
const target = event.target as HTMLElement;
if (target && target.dataset && target.dataset.departement) {
target.setAttribute('stroke-width', '2.5');
this.departementTarget.innerHTML = target.dataset.departement;
this.demarchesTarget.innerHTML = Number(
target.dataset.demarches
).toLocaleString();
this.dossiersTarget.innerHTML = Number(
target.dataset.dossiers
).toLocaleString();
}
show(this.infosTarget);
}
hideInfo(event: Event) {
hide(this.infosTarget);
const target = event.target as HTMLElement;
target.removeAttribute('stroke-width');
}
}

84
app/models/map_filter.rb Normal file
View file

@ -0,0 +1,84 @@
class MapFilter
# https://api.rubyonrails.org/v7.1.1/classes/ActiveModel/Errors.html
include ActiveModel::Conversion
extend ActiveModel::Translation
extend ActiveModel::Naming
LEGEND = {
nb_demarches: { 'nothing': -1, 'small': 20, 'medium': 50, 'large': 100, 'xlarge': 500 },
nb_dossiers: { 'nothing': -1, 'small': 500, 'medium': 2000, 'large': 10000, 'xlarge': 50000 }
}
attr_accessor :stats
attr_reader :errors
def initialize(params)
@params = params[:map_filter]&.permit(:kind, :year) || {}
@errors = ActiveModel::Errors.new(self)
end
def persisted?
false
end
def kind
@params[:kind]&.to_sym || :nb_demarches
end
def year
@params[:year].presence
end
def kind_buttons
LEGEND.keys.map do
{ label: I18n.t("kind.#{_1}", scope:), value: _1 }
end
end
def kind_legend_keys
LEGEND[kind].keys
end
def css_class_for_departement(departement)
if kind == :nb_demarches
kind_legend_keys.reverse.find do
nb_demarches_for_departement(departement) > LEGEND[kind][_1]
end
else
kind_legend_keys.reverse.find do
nb_dossiers_for_departement(departement) > LEGEND[kind][_1]
end
end
end
def nb_demarches_for_departement(departement)
stats[departement.upcase] ? stats[departement.upcase][:nb_demarches] : 0
end
def nb_dossiers_for_departement(departement)
stats[departement.upcase] ? stats[departement.upcase][:nb_dossiers] : 0
end
def legende_for(legende)
limit = LEGEND[kind][legende]
index = LEGEND[kind].keys.index(legende.to_sym)
next_limit = LEGEND[kind].to_a[index + 1]
if next_limit
I18n.t(:legend, min_thresold: limit + 1, max_thresold: next_limit[1], scope:)
else
"> #{limit}"
end
end
def detailed_title
add_on = I18n.t(:specific_year_add_on, year:, scope:) if year
I18n.t("detailed_title_for_#{kind}", add_on:, scope:)
end
private
def scope
'activemodel.attributes.map_filter'
end
end

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
en:
activemodel:
attributes:
map_filter:
year: Year
from_beginning: From the beginning
specific_year_add_on: "(in %{year})"
kind:
nb_demarches: Number of procedures
nb_dossiers: Number of files
detailed_title_for_nb_demarches: "Number of published or closed procedures %{add_on}"
detailed_title_for_nb_dossiers: "Number of submitted files %{add_on}"
legend: "Between %{min_thresold} and %{max_thresold}"

View file

@ -0,0 +1,14 @@
fr:
activemodel:
attributes:
map_filter:
year: Année
from_beginning: Depuis le début
specific_year_add_on: "(en %{year})"
kind:
nb_demarches: Nombre de démarches
nb_dossiers: Nombre de dossiers
detailed_title_for_nb_demarches: "Nombre de démarches publiées ou closes %{add_on}"
detailed_title_for_nb_dossiers: "Nombre de dossiers déposés %{add_on}"
legend: "Entre %{min_thresold} et %{max_thresold}"

View file

@ -0,0 +1,6 @@
en:
carte:
show:
title: Deployment by department
map_choice_title: Which map do you want ?

View file

@ -0,0 +1,5 @@
fr:
carte:
show:
title: Déploiement par département
map_choice_title: Quelle carte souhaitez vous ?

View file

@ -208,6 +208,8 @@ Rails.application.routes.draw do
get "mentions-legales", to: "static_pages#legal_notice"
get "declaration-accessibilite", to: "static_pages#accessibility_statement"
get "carte", to: "carte#show"
post "webhooks/sendinblue", to: "webhook#sendinblue"
post "webhooks/helpscout", to: "webhook#helpscout"
post "webhooks/helpscout_support_dev", to: "webhook#helpscout_support_dev"

View file

@ -0,0 +1,22 @@
describe CarteController do
describe '#show' do
let(:service) { create(:service, departement: '63') }
let(:service2) { create(:service, departement: '75') }
let(:service3) { create(:service, departement: '75') }
let!(:procedure) { create(:procedure, :published, service:, estimated_dossiers_count: 4) }
let!(:procedure2) { create(:procedure, :published, service: service2, estimated_dossiers_count: 20, published_at: Date.parse('2020-07-14')) }
let!(:procedure3) { create(:procedure, :published, service: service3, estimated_dossiers_count: 30, published_at: Date.parse('2021-07-14')) }
let(:subject) { assigns(:map_filter) }
it 'give stats for each departement' do
get :show
expect(subject.stats['63']).to eq({ nb_demarches: 1, nb_dossiers: 4 })
expect(subject.stats['75']).to eq({ nb_demarches: 2, nb_dossiers: 50 })
end
it 'give stats for each departement for a specific year' do
get :show, params: { map_filter: { year: 2020 } }
expect(subject.stats['75']).to eq({ nb_demarches: 1, nb_dossiers: 20 })
end
end
end

View file

@ -0,0 +1,23 @@
describe MapFilter do
let(:map_filter) do
mf = MapFilter.new(params)
mf.stats = { '63' => { nb_demarches: 51, nb_dossiers: 2001 } }
mf
end
describe 'css_class_for_departement' do
let(:params) { { kind: :nb_demarches } }
context 'for nb_demarches' do
it 'return class css' do
expect(map_filter.css_class_for_departement('63')).to eq :medium
end
end
context 'fr nb_dossiers' do
let(:params) { { kind: :nb_dossiers } }
it 'return class css' do
expect(map_filter.css_class_for_departement('63')).to eq :medium
end
end
end
end