block by curran 05bd927371a3ccf8bf6039bf1b30e448

Syrian Refugees by Settlement Type

Full Screen

A stacked area chart showing Syrian refugees, by settlement type.

Data from the UN Refugee Agency Data API.

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <title>Syria Situation Prototypes</title>
    <style>
      .axis .tick text, .legend text, .tooltip text {
        fill: #585858;
        font-family: sans-serif;
        font-size: 12pt;
      }
      .axis .tick line{
        stroke: #ddd;
      }
      .axis .domain {
        display: none;
      }
    </style>
  </head>
  <body>
    <svg width="960" height="300"></svg>
    <script>
      const svg = d3.select('svg');
      const width = svg.attr('width');
      const height = svg.attr('height');
      const margin = { top: 28, bottom: 20.5, left: 10, right: 54.5 };

      //const inCampEndpoint = 'https://data2.unhcr.org/api/population/get/timeseries?widget_id=41115&sv_id=4&population_collection=25&frequency=day&fromDate=2011-01-01';
      //const urbanRuralEndpoint = 'https://data2.unhcr.org/api/population/get/timeseries?widget_id=41116&sv_id=4&population_collection=26&frequency=day&fromDate=2011-01-01';
      const inCampEndpoint = 'inCamp.json';
      const urbanRuralEndpoint = 'urbanRural.json';
      
      const dateFromTimestamp = unix_timestamp => new Date(unix_timestamp * 1000);
      const bisectDate = d3.bisector(d => d.date).left;

      function getInterpolatedValue (timeseries, date, value){
        const i = bisectDate(timeseries, date, 0, timeseries.length - 1);
        if (i > 0) {
          const a = timeseries[i - 1];
          const b = timeseries[i];
          const t = (date - a.date) / (b.date - a.date);
          return value(a) * (1 - t) + value(b) * t;
        }
        return value(timeseries[i]);
      }

      d3.queue()
        .defer(d3.json, inCampEndpoint)
        .defer(d3.json, urbanRuralEndpoint)
        .awaitAll((error, results) => {
          if (error) throw error;

          // Parse dates from timestamps for all timeseries.
          results.forEach(result => {
            result.data.timeseries.forEach(d => {
              d.date = dateFromTimestamp(d.unix_timestamp);
            });
          });

          // Because each timeseries has different date intervals,
          // we impute values using linear interpolation.
          // First we need the union of all dates from both timeseries.
          const dates = d3
            .set(
              results.reduce((a, b) => a.data.timeseries.concat(b.data.timeseries)),
              d => d.unix_timestamp
            )
            .values() // Returns strings
            .map(d => +d) // Parse into numbers for sorting
            .sort()
            .map(dateFromTimestamp);

          // Transform data into a structure usable by d3.stack.
          const keys = results.map(result => result.title_language_en);
          const conciseKeys = keys.map(key => key.replace(' by date', ''));
          const data = dates.map(date => {

            // Create a new row object with the date.
            const d = { date: date };

            // Assign values to the new row object for each key.
            // Value for `key` here will be e.g. "In-camp by date" or "Urban/rural by date".
            keys.forEach((key, j) => {
              const timeseries = results[j].data.timeseries;
              d[conciseKeys[j]] = getInterpolatedValue(timeseries, date, d => d.individuals);
            });

            return d;
          });

          render(data, conciseKeys, results);
        });

      const innerWidth = width - margin.right - margin.left;
      const innerHeight = height - margin.top - margin.bottom;
      const xScale = d3.scaleTime().range([0, innerWidth]);
      const yScale = d3.scaleLinear().range([innerHeight, 0]);

      // Colors and opacity derived from //data2.unhcr.org
      const colorScale = d3.scaleOrdinal()
        .range(['rgb(60, 141, 188)', 'rgb(48, 48, 48)']);
      const areaFillOpacity = 0.2;

      const xAxis = d3.axisBottom()
        .scale(xScale);
      const yAxis = d3.axisRight()
        .tickSize(-innerWidth)
        .tickPadding(5)
        .scale(yScale)
        .ticks(5)
        .tickFormat(d3.format('.2s'));

      const g = svg.append('g')
          .attr('transform', `translate(${margin.left},${margin.top})`);
      const xAxisG = g.append('g')
          .attr('class', 'axis')
          .attr('transform', `translate(0,${innerHeight})`);
      const yAxisG = g.append('g')
          .attr('class', 'axis')
          .attr('transform', `translate(${innerWidth},0)`);

      // This layer will contain the marks (stacked area layers).
      const marksG = g.append('g');

      // This layer will contain the tooltip.
      const tooltipG = g.append('g')
          .attr('class', 'tooltip');

      // This rectangle intercepts mouse events to drive the tooltip.
      const eventRect = g.append('rect')
          .attr('fill', 'none')
          .attr('pointer-events', 'all');

      const colorLegendG = svg.append('g')
          .attr('class', 'legend')
          .attr('transform', `translate(${margin.left},13)`);

      const area = d3.area()
        .x(d => xScale(d.data.date))
        .y0(d => yScale(d[0]))
        .y1(d => yScale(d[1]));

      const stack = d3.stack();

      function render(data, keys, results) {
        const stackedData = stack.keys(keys)(data);

        xScale.domain(d3.extent(data, d => d.date));
        yScale
          .domain([0, d3.max(stackedData[stackedData.length - 1], d => d[1])]);
        colorScale.domain(keys);

        const paths = marksG.selectAll('path').data(stackedData);
        paths
          .enter().append('path')
            .attr('fill-opacity', areaFillOpacity)
          .merge(paths)
            .attr('d', area)
            .attr('fill', d => colorScale(d.key))
            .attr('stroke', d => colorScale(d.key));

        xAxisG.call(xAxis);
        yAxisG.call(yAxis);
        renderColorLegend();

        const tooltipDatum = () => {
          const coords = d3.mouse(eventRect.node());
          const date = xScale.invert(coords[0]);
          const index = bisectDate(data, date);
          return data[Math.min(index, data.length - 1)];
        }

        eventRect
            .attr('width', innerWidth)
            .attr('height', innerHeight)
            .on('mousemove', () => {
              renderTooltip([tooltipDatum()], keys);
            })
            .on('mouseout', () => {
              renderTooltip([]);
            });
      }

      function renderColorLegend() {
        const entrySpacing = 130;
        const entryRectWidth = 50;
        const entryRectHeight = 20;
        const entryTextPadding = 4;
        const entries = colorLegendG.selectAll('g').data(colorScale.domain());
        entries
          .enter().append('text')
            .attr('alignment-baseline', 'middle')
            .attr('x', (d, i) => i * entrySpacing + entryRectWidth + entryTextPadding)
            .attr('y', 0)
            .text(d => d);
        entries
          .enter().append('rect')
            .attr('x', (d, i) => Math.round(i * entrySpacing) - .5) // .5 for crisp edges.
            .attr('y', Math.round(-entryRectHeight / 2) - .5)
            .attr('width', entryRectWidth)
            .attr('height', entryRectHeight)
            .attr('fill-opacity', areaFillOpacity)
            .attr('fill', colorScale)
            .attr('stroke', colorScale);
      }

      const dateFormat = d3.timeFormat('%B %d, %Y');
      const commaFormat = d3.format(',');
      const numberFormat = n => commaFormat(Math.round(n));
      const lines = keys => d => {
        const total = `Total: ${numberFormat(d3.sum(keys.map(key => d[key])))}`;
        const keyValues = keys.map(key => `${key}: ${numberFormat(d[key])}`);
        return [dateFormat(d.date)].concat(total).concat(keyValues);
      }
      const lineHeight = 20;
      const tooltipRectWidth = 180;
      const tooltipRectHeight = 90;

      function renderTooltip(hoveredData, keys) {

        // Show a line representing the hovered date.
        const line = tooltipG.selectAll('line').data(hoveredData);
        line.exit().remove();
        line
          .enter().append('line')
          .merge(line)
            .attr('x1', d => xScale(d.date))
            .attr('y1', 0)
            .attr('x2', d => xScale(d.date))
            .attr('y2', innerHeight)
            .attr('stroke', 'black')
            .attr('stroke-opacity', 0.5);

        // Create a group element to contain the tooltip.
        let g = tooltipG.selectAll('g').data(hoveredData);
        g.exit().remove();
        g = g
          .enter().append('g')
          .merge(g)
            .attr('transform', d => {
              let x = xScale(d.date);
              if ( (x + tooltipRectWidth) > innerWidth) {
                x -= tooltipRectWidth;
              }
              const y = lineHeight;
              return `translate(${x},${y})`
            });

        // The background rectangle.
        const rect = g.selectAll('rect').data([1]);
        rect
          .enter().append('rect')
            .attr('y', -20)
            .attr('width', tooltipRectWidth)
            .attr('height', tooltipRectHeight)
            .attr('fill', 'white')
            .attr('stroke', 'gray')
            .attr('rx', 10);

        // The text (multiple lines).
        const text = g.selectAll('text').data(lines(keys));
        text
          .enter().append('text')
          .merge(text)
            .attr('x', 10)
            .attr('y', (d, i) => i * lineHeight)
            .text(d => d);
      }

    </script>
  </body>
</html>