Giter VIP home page Giter VIP logo

rosling-graph's Introduction

Graphique Rosling

Hans Rosling

Page wikipedia

Video

Les données

Source

Gapminder

Préparer les données

fs

Documentation

Ouvrir un fichier CSV

const fs = require('fs')

const csv = fs.readFileSync(path, 'utf-8')

Extraire les cellules

const getRows = csv => csv
  .split('\n')
  .map(row => row.split(','))

const openCsv = path =>
  getRows(fs.readFileSync(path, 'utf-8'))

.split() sur MDN

Sauver un fichier JSON

const saveJson = (path, data) =>
  fs.writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8')

Joindre les tables

Trois de nos fichiers ont une colonne geo qui correspond au code ISO 3166-1 alpha-3.

life_expect.csv n'a pas de colonne geo mais une colonne country_code. Pour faire le lien nous allons utiliser [110m_countries.json] de [Natural earth] qui a les deux.

Extraire le code ISO et le code pays de 110m_countries.json

Fichier getCountries.js

const R = require('ramda')
const countriesFeatures = require('10m_countries.json').features

const getCountryNameAndIds = feature => ({
  country: R.path(['properties', 'name_fr'], feature),
  geo: R.path(['properties', 'iso_a3'], feature),
  code: R.path(['properties', 'iso_n3'], feature),
})

module.exports = countriesFeatures.map(getCountryNameAndIds)

Extraire les données des csv

life_expect.csv est aussi le fichier qui couvre le moins d'années de 1950 à 2015, nous ne pouvons donc utiliser que ces années là.

const years = Array.from(Array(66)).map((d, i) => i + 1950)

Pour chaque fichier CSV nous allons demander une liste de valeurs par pays pour ces années.

  • toJSON_life_expect.js
const R = require('ramda')
const { openCsv, saveJson, years } = require('./utils')

const rows = openCsv('csv/life_expect.csv')


const json = rows.map(([region, country_code, year, life_expect]) => ({
  code: country_code,
  year: Number(year),
  life_expect: Number(life_expect),
}))

/*
[
  { country_code: '900', year: 1950, lifeExpect: 45.78 },
  ...
]
*/

const getValueByYearAndCode = (year, code) => {
  const line = json.find(d => d.year === year && d.code === code)
  return line ? line.life_expect : undefined
}

const uniqCodes = R.uniq(json.map(R.prop('code')))

const data = uniqCodes.map(code => ({
  code,
  values: years.map(year => getValueByYearAndCode(year, code))
}))

saveJson('life_expect.json', data)
  • toJSON_pop.js
const R = require('ramda')
const { openCsv, saveJson, years } = require('./utils')

const rows = openCsv('csv/pop.csv')

const json = rows.map(([geo, name, time, population]) => ({
  geo,
  year: Number(time),
  pop: Number(population),
}))

/*
[
  { geo: 'afg', year: 1800, pop: 3280000 },
  ...
]
*/

const getValueByYearAndGeo = (year, geo) => {
  const line = json.find(d => d.year === year && d.geo === geo)
  return line ? line.pop : undefined
}

const uniqGeos = R.uniq(json.map(R.prop('geo')))

const data = uniqGeos.map(geo => ({
  geo,
  values: years.map(year => getValueByYearAndGeo(year, geo))
}))

saveJson('pop.json', data)
  • toJSON_gdp_p_capita.js
const R = require('ramda')
const { openCsv, saveJson, years } = require('./utils')

const json = openCsv('csv/gdp_p_capita.csv')

const head = R.head(json)
const rows = R.tail(json)

const yearIsInRange = year => years.includes(year)

const yearIndexes = head
  .map((label, index) => ({ label, index }))
  .filter(({ index }) => index !== 0 && index !== 1)
  .map(({ label, index }) => ({ year: Number(label), index }))
  .filter(({ year }) => yearIsInRange(year))
  .map(R.prop('index'))

const getRowValueByIndex = row => index =>
  R.prop(index, R.pick([index], row))

const data = rows.map(row => ({
  geo: getRowValueByIndex(row)(1),
  values: yearIndexes.map(getRowValueByIndex(row)).map(Number),
}))

saveJson('gdp_p_capita.json', data)
  • toJSON_regions.js
const R = require('ramda')
const { openCsv, saveJson } = require('./utils')

const rows = R.tail(openCsv('csv/regions.csv'))

const data = rows.map(([geo, region]) => ({ geo, region }))

saveJson('regions.json', data)

Nous avons maintenant:

Créer le fichier final

prepareData.js

const R = require('ramda')
const { saveJson, years } = require('./utils')
const countries = require('./getCountries')

const lifeExpectData = require('./life_expect.json')
const gdpCapitaData = require('./gdp_p_capita.json')
const popData = require('./pop.json')
const regionsData = require('./regions.json')

const getValuesByGeo = (data, key) => ({ geo }) => {
  const line = data.find(d => d.geo === geo)
  return line ? line[key] : undefined
}

const getGdpCapita = getValuesByGeo(gdpCapitaData, 'values')
const getPop = getValuesByGeo(popData, 'values')
const getRegion = getValuesByGeo(regionsData, 'region')
const getLifeExpect = ({ code }) => {
  const line = lifeExpectData.find(d => d.code === code)
  return line ? line.values : undefined
}

const data = countries.map(country => ({
  ...country,
  region: getRegion(country),
  gdpCapita: getGdpCapita(country),
  pop: getPop(country),
  lifeExpect: getLifeExpect(country),
}))

// enlever les pays avec données incomplètes

const hasNullValues = values =>
  values.reduce((result, value) => result ? result : R.isNil(value), false)

const valueIsNotComplete = values => {
  if (!values) {
    return true
  }
  if (values.length !== years.length) {
    return true
  }
  if (hasNullValues(values)) {
    return true
  }
  return false
}

const countryHasAllValues = country => {
  const noPop = R.not(valueIsNotComplete(country.pop))
  const noLife = R.not(valueIsNotComplete(country.lifeExpect))
  const noGdp = R.not(valueIsNotComplete(country.gdpCapita))
  return noPop && noLife && noGdp
}

saveJson('data.json', { years, countries: data.filter(countryHasAllValues) })

data.json

Le graphique

Mise en place

Les dépendances de développement:

npm install @babel/core http-server webpack webpack-cli  --save-dev

Les modules d3 dont nous avons besoin:

npm install d3-selection d3-scale --save

Créer les dossiers src et dist.

Dans package.json

{
  "scripts": {
    "serve": "http-server dist",
    "watch": "webpack --watch",
    "webpack": "webpack"
  },
}

Dans dist, créer un fichier index.html

<html>
  <head>
    <meta charset="utf-8">
    <title>Graphique Rosling</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="graph"></div>
    <input id="slider" type="range" min="1950" max="2015" value="1950" />
    <script src="bundle.js"></script>
  </body>
</html>

Nous allons écrire le code dans src

Les constantes

config.js

export const WIDTH = 500
export const HEIGHT = 200

Les éléments

index.js

import { select } from 'd3'

const svg = select('#graph').append('svg')
  .attr('viewBox', `0 0 ${WIDTH} ${HEIGHT}`)

const slider = document.getElementById('slider')

Nous allons aussi charger les données lors du chargement de la page. Copions data/data.json dans dist.

const getData = fetch('data.json')
  .then(res => res.json())

const drawGraph = ({ years, countries }) => {
  // dessiner le graphique ici
}

window.onload = () =>
  getData()
    .then(drawGraph(svg, slider))

Une fois les données chargée nous allons appeller la fonction drawGraph.

Les échelles et une fonction pour la couleur régionale

scales.js

import { scaleLinear, scaleLog, scalePow } from 'd3'
import { WIDTH, HEIGHT } from './config'

export const yearIndex = d3.scaleLinear().domain([1950, 2015]).range([0, 65])
export const xScale = scaleLog().domain([500, 140000]).range([0, WIDTH])
export const yScale = scaleLinear().domain([30, 85]).range([HEIGHT, 0])
export const rScale = scalePow().domain([25000, 1000000000]).range([2, 25])
export const getColorByRegion = ({ region }) => {
  switch(region) {
    case 'south_asia':return '#66c2a5'
    case 'europe_central_asia': return '#fc8d62'
    case 'middle_east_north_africa': return '#8da0cb'
    case 'sub_saharan_africa': return '#e78ac3'
    case 'america': return '#a6d854'
    default: return '#ffd92f'
  }
}

Les bulles

bubbles.js

import { getColorByRegion } from './scales'

export default (svg, countries) =>
  svg.selectAll('circle')
    // lier les données
    .data(countries)
    .enter()
    .append('circle')
    // une classe pour le CSS
    .attr('class', 'bubble')
    // la couleur en fonction de la région
    .attr('fill', getColorByRegion)
    .attr('stroke', getColorByRegion)

Import dans index.js

import createBubbles from './bubbles'

// ...

const drawGraph = ({ years, countries }) => {
  const bubbles = createBubbles(svg, countries)
}

Les événements

events.js

Nous bulles n'ont pas encore de positions / tailles elles vont être calculées en fonction de l'annéee

import {
  yearIndex,
  xScale,
  yScale,
  rScale,
} from './scales'

const updateBubblesByYearIndex = (bubbles, yearIndex) =>
    bubbles
      .attr('cx', d => xScale(d.gdpCapita[yearIndex]))
      .attr('cy', d => yScale(d.lifeExpect[yearIndex]))
      .attr('r', d => rScale(d.pop[yearIndex]))

export const setYear = (year, bubbles) => {
  const index = yearIndex(year)
  updateBubblesByYearIndex(bubbles, index)
}

Dans index.js

const drawGraph = ({ years, countries }) => {
  const bubbles = createBubbles(svg, countries)
  setYear(1950, bubbles)
}

Nous pouvons maintenant voir les bulles. Il nous faut mettre à jour les bulles quand la valeur de slider change.

const drawGraph = ({ years, countries }) => {
  const bubbles = createBubbles(svg, countries)
  setYear(1950, bubbles)
  slider.addEventListener('input', e => setYear(e.target.value, bubbles))
}

Style

dist/style.css

html, body, #graph, #slider {
  margin: 0;
  padding: 0;
}
body {
  font-family: Arial, Helvetica, sans-serif
}
#graph, #slider {
  width: 100%;
}
.bubble {
  fill-opacity: 0.4;
  stroke-opacity: 0.8;
  stroke-width: 0.5;
}

Afficher l'année

Dans index.js

const yearDisplay = svg.append('text')
  .attr('id', 'year')
  .attr('x', WIDTH - 20)
  .attr('y', HEIGHT - 20)
  .attr('text-anchor', 'end')
  .text(null)

// ...

const drawGraph = ({ years, countries }) => {
  const bubbles = createBubbles(svg, countries)
  setYear(1950, bubbles, yearDisplay)
  slider.addEventListener('input', e => setYear(e.target.value, bubbles, yearDisplay))
}

Dans events.js

const updateYearDisplayByYear = (yearDisplay, year) =>
  yearDisplay.text(year)

export const setYear = (year, bubbles, yearDisplay) => {
  const index = yearIndex(year)
  updateBubblesByYearIndex(bubbles, index)
  updateYearDisplayByYear(yearDisplay, year)
}

Dans dist/style.css

#year {
  font-size: 100;
  opacity: 0.2;
}

Afficher le nom du pays en survolant une bulle

bubbles.js

import { select } from 'd3-selection'
import { getColorByRegion } from './scales'

function onBubbleMouseOver(svg) {
  return function(d) {
    const current = select(this)
    current.attr('fill-opacity', 1)
    svg.append('text')
      .attr('id', 'country-name')
      .attr('text-anchor', 'middle')
      .attr('x', parseFloat(current.attr('cx')))
      .attr('y', parseFloat(current.attr('cy') - 5))
      .text(d.country)
  }
}

function onBubbleMouseOut(d) {
  select(this).attr('stroke', getColorByRegion)
  select('#country-name').remove()
}

export default (svg, countries) =>
  svg.selectAll('circle')
    .data(countries)
    .enter()
    .append('circle')
    .attr('class', 'bubble')
    .attr('fill', getColorByRegion)
    .attr('stroke', getColorByRegion)
    .on('mouseover', onBubbleMouseOver(svg))
    .on('mouseout', onBubbleMouseOut)

Dans dist/style.css

#country-name {
  text-anchor: 'middle';
  font-size: 5;
}

Légende

axis.js

import { WIDTH, HEIGHT } from './config'
import { xScale, yScale } from './scales'

const TOP = 10
const BOTTOM = HEIGHT - 10
const LEFT = 10
const RIGHT = WIDTH - 10

export default (svg) => {
  const xAxis = svg.append('g')
  const yAxis = svg.append('g')

  xAxis.append('line')
    .attr('x1', LEFT)
    .attr('x2', RIGHT)
    .attr('y1', BOTTOM)
    .attr('y2', BOTTOM)
    .attr('stroke', 'black')
    .attr('opacity', 0.5)
    .attr('stroke-width', 0.5)
  
  xAxis.selectAll('text')
    .data([1000, 4000, 16000, 64000])
    .enter()
    .append('text')
    .attr('class', 'axis-label')
    .attr('x', xScale)
    .attr('y', BOTTOM + 10)
    .attr('text-anchor', 'middle')
    .text(d => d)
  
  xAxis.append('text')
    .attr('class', 'axis-label')
    .attr('x', RIGHT)
    .attr('y', BOTTOM - 3)
    .attr('text-anchor', 'end')
    .text('PNB par habitant')

  yAxis.append('line')
    .attr('x1', LEFT)
    .attr('x2', LEFT)
    .attr('y1', TOP)
    .attr('y2', BOTTOM)
    .attr('stroke', 'black')
    .attr('opacity', 0.5)
    .attr('stroke-width', 0.5)

  yAxis.append('text')
    .attr('class', 'axis-label')
    .attr('x', LEFT + 5)
    .attr('y', TOP)
    .attr('transform', `rotate(90, ${LEFT + 5}, ${TOP})`)
    .text('Espérance de vie')

  yAxis.selectAll('text')
    .data([40, 60, 80])
    .enter()
    .append('text')
    .attr('class', 'axis-label')
    .attr('x', LEFT - 2)
    .attr('y', d => yScale(d) - 5)
    .attr('text-anchor', 'end')
    .text(d => d)
}

Dans index.js

import addAxis from './axis'

// ...

const drawGraph = ({ years, countries }) => {
  addAxis(svg)
  const bubbles = createBubbles(svg, countries)
  setYear(1950, bubbles, yearDisplay)
  slider.addEventListener('input', e => setYear(e.target.value, bubbles, yearDisplay))
}

Dans style.css

.axis-label {
  font-size: 5;
}

rosling-graph's People

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.