javascriptd3.jsdata-visualizationbar-chartarea-chart

Append a line to an end of a path in area chart d3


I'm trying to append a line to an end of an area chart path. The most difficult part my area is animated. I have a clipPath which width being transformed from width: 0 to width: 960 and the line at the end goes along with it so shoud be synchronised. Also the text on top of that line needs to be updated while it goes along.

Desired output:

enter image description here

My initial idea was to build a chart area and add a clipPath and then add a bar chart inside of area chart so I can update my text based on the bar appended, however bars are not inside my area chart. What am I doing wrong to place bars inside area chart or is there a better solution to this?

// Area chart width and height
const width1 = 1000,
  height1 = 100;

// Define x and y scale for area chart
const xScale1 = d3.scaleTime().range([0, width1]);
const yScale1 = d3.scaleLinear().range([height1, 0]);

// Define x and y range for bar chart
let xScale2 = d3.scaleBand().range([0, width1]);
let yScale2 = d3.scaleLinear().range([height1, 0]);

// Add SVG to #areachart
const svg1 = d3
  .select('#areachart')
  .append('svg')
  .attr('viewBox', `0 0 ${width1} ${height1}`)
  .attr('transform', 'translate(' + 0 + ',' + -50 + ')');

const g1 = svg1.append('g');

// Fetch data
d3.json(
    'https://api.coronavirus.data.gov.uk/v1/data?filters=areaName=United%2520Kingdom;areaType=overview&structure=%7B%22areaType%22:%22areaType%22,%22areaName%22:%22areaName%22,%22areaCode%22:%22areaCode%22,%22date%22:%22date%22,%22newCasesByPublishDate%22:%22newCasesByPublishDate%22,%22cumCasesByPublishDate%22:%22cumCasesByPublishDate%22%7D&format=json'
  )
  .then(function(data) {
    console.log('DATES SLICED ----->', data.data.slice(30, 281));

    //Define xScale1 & yScale1 domain after data loaded
    yScale1.domain([
      0,
      d3.max(data.data, function(d) {
        return +d.cumCasesByPublishDate;
      }),
    ]);

    xScale1.domain(
      d3.extent(data.data, function(d) {
        return new Date(d.date);
      })
    );

    // Area  generator
    const area = d3
      .area()
      .curve(d3.curveStepAfter)
      .x((d) => xScale1(new Date(d.date)))
      .y1((d) => yScale1(+d.cumCasesByPublishDate))
      .y0(yScale1(0));

    g1.append('path')
      .datum(data.data.slice(30, 200))
      .attr('d', area)
      .classed('placeholder-layer', true)
      .style('fill', '#dadada')
      .style('opacity', '0.3');

    // clipPath for areachart fill animation
    const clip = g1.append('clipPath').attr('id', 'clip');
    const clipRect = clip.append('rect').attr('width', 0).attr('height', 750);

    g1.append('path')
      .datum(data.data.slice(30, 200))
      .attr('d', area)
      .attr('clip-path', 'url(#clip)')
      .classed('overlay-layer', true)
      .style('fill', 'yellow')
      .style('opacity', '0.3');

    g1.append('line').attr('stroke-width', 960).style('stroke', 'yellow');

    clipRect
      .transition()
      .duration(10000)
      .ease(d3.easeLinear)
      .attr('width', 960);

    //x and y domain for bar chart
    xScale2.domain(data.data.slice(30, 200).map((d) => new Date(d.date)));
    yScale2.domain([
      0,
      d3.max(data.data, function(d) {
        return +d.cumCasesByPublishDate;
      }),
    ]);

    g1.selectAll('rect')
      .data(data.data.slice(30, 200))
      .enter()
      .append('rect')
      .style('fill', 'red')
      .attr('width', xScale2.bandwidth() * 10)
      .attr('height', (d) => yScale2(+d.cumCasesByPublishDate))
      .attr('x', 0)
      .attr('y', function(d) {
        return yScale2(+d.cumCasesByPublishDate);
      })
      .transition()
      .delay(function(d, i) {
        return i * 30;
      })
      .attr('x', function(d) {
        return xScale2(new Date(d.date));
      })
      .duration(100);
  })
  // if there's an error, log it
  .catch((error) => console.log(error));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<section id="map">
  <div id="visualisation-container">
    <div id="visualisation"></div>
    <div id="areachart"></div>
  </div>
</section>


Solution

  • I'd just use a line, no bars, and use transition.tween with d3.interpolateDate to make the text change.

    // Area chart width and height
    const width1 = 800,
      height1 = 250,
      marginBottom = 50
    
    // Define x and y scale for area chart
    const xScale1 = d3.scaleTime().range([0, width1]);
    const yScale1 = d3.scaleLinear().range([height1, marginBottom]);
    
    // Define x and y range for bar chart
    let xScale2 = d3.scaleBand().range([0, width1]);
    let yScale2 = d3.scaleLinear().range([height1, marginBottom]);
    
    // Add SVG to #areachart
    const svg1 = d3
      .select('#areachart')
      .append('svg')
      .attr('width', width1)
      .attr('height', height1);
    
    const g1 = svg1.append('g');
    
    // Fetch data
    d3.json(
        'https://api.coronavirus.data.gov.uk/v1/data?filters=areaName=United%2520Kingdom;areaType=overview&structure=%7B%22areaType%22:%22areaType%22,%22areaName%22:%22areaName%22,%22areaCode%22:%22areaCode%22,%22date%22:%22date%22,%22newCasesByPublishDate%22:%22newCasesByPublishDate%22,%22cumCasesByPublishDate%22:%22cumCasesByPublishDate%22%7D&format=json'
      )
      .then(data => {
        data.data.forEach(d => {
          d.cumCasesByPublishDate = +d.cumCasesByPublishDate;
          d.date = new Date(d.date);
        });
        return data.data.slice(30, 200);
      })
      .then(function(data) {
        //Define xScale1 & yScale1 domain after data loaded
        yScale1.domain([
          0,
          d3.max(data, d => d.cumCasesByPublishDate),
        ]);
    
        xScale1.domain(
          d3.extent(data, d => d.date)
        );
    
        // Area  generator
        const area = d3
          .area()
          .curve(d3.curveStepAfter)
          .x((d) => xScale1(d.date))
          .y1((d) => yScale1(d.cumCasesByPublishDate))
          .y0(yScale1(0));
    
        g1.append('path')
          .datum(data)
          .attr('d', area)
          .classed('placeholder-layer', true)
          .style('fill', '#dadada')
          .style('opacity', '0.3');
    
        // clipPath for areachart fill animation
        const clip = g1.append('clipPath').attr('id', 'clip');
        const clipRect = clip.append('rect').attr('width', 0).attr('height', 750);
    
        g1.append('path')
          .datum(data)
          .attr('d', area)
          .attr('clip-path', 'url(#clip)')
          .classed('overlay-layer', true)
          .style('fill', 'yellow')
          .style('opacity', '0.3');
    
        const format = d3.timeFormat("%B %d, %Y");
        const duration = 10000;
        g1.append('line')
          .attr('stroke-width', 5)
          .style('stroke', 'black')
          .attr('x1', xScale1.range()[0])
          .attr('x2', xScale1.range()[0])
          .attr('y1', yScale1.range()[0])
          .attr('y2', yScale1.range()[1])
          .transition()
          .duration(duration)
          .ease(d3.easeLinear)
          .attr('x1', xScale1.range()[1])
          .attr('x2', xScale1.range()[1])
    
        g1.append('text')
          .attr('x', xScale1.range()[0])
          .attr('y', marginBottom / 2)
          .attr('text-anchor', 'middle')
          .transition()
          .duration(duration)
          .ease(d3.easeLinear)
          .attr('x', xScale1.range()[1])
          .tween('text', function() {
            const i = d3.interpolateDate(xScale1.domain()[0], xScale1.domain()[1]);
            return (t) => d3.select(this).text(format(i(t)));
          })
      })
      // if there's an error, log it
      .catch((error) => console.log(error));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <section id="map">
      <div id="visualisation-container">
        <div id="visualisation"></div>
        <div id="areachart"></div>
      </div>
    </section>