4

I'm getting stuck with the D3.js v4's animation of both line & area:

  • It's ok to do the animation separately for line & area
  • When 2 animations are combined, even at the same transition duration, they do not occur together.
  • For the reason of styling, I cannot drop the line away.

See the illustration below: enter image description here

To make thing like above, I do 2 big steps:

  1. Do animation for line via setting the properties stroke-dasharrow and stroke-dashoffset. (Inspired from Visual Cinnamon)
  2. Do animation for area via tweaking parameters for d3.area() function (Inspired from other Stackoverlfow post)

The result is rather disappointing because line and area underneath do not appear in parallel.

My target is to mimic the Highchart library, see an example here, and its illustration below:

enter image description here

It seems the Highchart library uses a different animation technique, because during DOM inspection, there is no sign of any change for the DOM paths along the animation.

Appreciated if anyone could suggest me some ideas to experiment with.

My code sample is below:

let animationDuration = 5000;
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
    width = 480 - margin.left - margin.right,
    height = 250 - margin.top - margin.bottom;

// parse the date / time
var parseTime = d3.timeParse("%d-%b-%y");

// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);

// define the area
var area = function (datum, boolean) {
    return d3.area()
        .y0(height)
        .y1(function (d) { return boolean ? y(d.close) : y(d.close); })
        .x(function (d) { return boolean ? x(d.date) : 0; })
        (datum);
}

// define the line
var valueline = d3.line()
    .x(function (d) { return x(d.date); })
    .y(function (d) { return y(d.close); });

// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

      var data = d3.csvParse(d3.select("pre#data").text());

      data.reverse();

    // format the data
    data.forEach(function (d) {
        d.date = parseTime(d.date);
        d.close = +d.close;
    });

    // scale the range of the data
    x.domain(d3.extent(data, function (d) { return d.date; }));
    y.domain([0, d3.max(data, function (d) { return d.close; })]);

    // add the area
    svg.append("path")
        .data([data])
        .attr("class", "area")
        .attr("d", d => area(d, false))
        .attr("fill", "lightsteelblue")
        .transition()
        .duration(animationDuration)
        .attr("d", d => area(d, true));

    // add the valueline path.
    svg.append("path")
        .data([data])
        .attr("class", "line")
        .attr("d", valueline)
        .style("stroke-dasharray", d => {
            let path = document.querySelector(".line");
            const totalLength = path.getTotalLength();

            return `${totalLength} ${totalLength}`;
        })
        .style("stroke-dashoffset", d => {
            let path = document.querySelector(".line");
            const totalLength = path.getTotalLength();
            return `${totalLength}`;
        })
        .transition()
        .duration(animationDuration)
        .style("stroke-dashoffset", 0);

    // add the X Axis
    svg.append("g")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x));

    // add the Y Axis
    svg.append("g")
        .call(d3.axisLeft(y));
.line {
        fill: none;
        stroke: steelblue;
        stroke-width: 2px;
    }
    
pre#data {display:none;}
<script src="https://d3js.org/d3.v4.min.js"></script>
<pre id="data">
date,close
1-May-12,58.13
30-Apr-12,53.98
27-Apr-12,67.00
26-Apr-12,89.70
25-Apr-12,99.00
24-Apr-12,130.28
23-Apr-12,166.70
20-Apr-12,234.98
19-Apr-12,345.44
18-Apr-12,443.34
17-Apr-12,543.70
16-Apr-12,580.13
13-Apr-12,605.23
12-Apr-12,622.77
11-Apr-12,626.20
10-Apr-12,628.44
9-Apr-12,636.23
5-Apr-12,633.68
4-Apr-12,624.31
3-Apr-12,629.32
2-Apr-12,618.63
30-Mar-12,599.55
29-Mar-12,609.86
28-Mar-12,617.62
27-Mar-12,614.48
26-Mar-12,606.98
</pre>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
TrungNguyen
  • 309
  • 5
  • 9

1 Answers1

5

There is a way to animate both the line and the area, using a custom interpolator.

However, since your goal is to mimic that Highcharts animation you linked, there is a way easier alternative: use a <clipPath>.

In my proposed solution we create the area and the line the regular way. However, we reference a clipping path...

.attr("clip-path", "url(#clip)");

...in both area and line. The clipping path is created with 0 width:

var clip = svg.append("clipPath")
    .attr("id", "clip");
var clipRect = clip.append("rect")
  .attr("width", 0)

Then, after that, it's just a matter of applying the transition to the clipping path:

clipRect.transition()
    .duration(5000)
    .ease(d3.easeLinear)
    .attr("width", someValue)

Here is a demo:

var svg = d3.select("svg");
var data = d3.range(30).map(d => Math.random() * 150);
var clip = svg.append("clipPath")
  .attr("id", "clip");
var clipRect = clip.append("rect")
  .attr("width", 0)
  .attr("height", 150)
var lineGenerator = d3.line()
  .x((_, i) => i * 10)
  .y(d => d)
  .curve(d3.curveMonotoneX)
var areaGenerator = d3.area()
  .x((_, i) => i * 10)
  .y1(d => d)
  .y0(150)
  .curve(d3.curveMonotoneX)
svg.append("path")
  .attr("d", areaGenerator(data))
  .attr("class", "area")
  .attr("clip-path", "url(#clip)");
svg.append("path")
  .attr("d", lineGenerator(data))
  .attr("class", "line")
  .attr("clip-path", "url(#clip)");
clipRect.transition()
  .duration(5000)
  .ease(d3.easeLinear)
  .attr("width", 300)
.line {
  fill: none;
  stroke: #222;
  stroke-width: 2px;
}

.area {
  fill: limegreen;
  stroke: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

And here is your code with those changes:

let animationDuration = 5000;
// set the dimensions and margins of the graph
var margin = {
    top: 20,
    right: 20,
    bottom: 30,
    left: 50
  },
  width = 480 - margin.left - margin.right,
  height = 250 - margin.top - margin.bottom;

// parse the date / time
var parseTime = d3.timeParse("%d-%b-%y");

// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);

// define the area
var area = function(datum, boolean) {
  return d3.area()
    .y0(height)
    .y1(function(d) {
      return boolean ? y(d.close) : y(d.close);
    })
    .x(function(d) {
      return boolean ? x(d.date) : 0;
    })
    (datum);
}

// define the line
var valueline = d3.line()
  .x(function(d) {
    return x(d.date);
  })
  .y(function(d) {
    return y(d.close);
  });

// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
    "translate(" + margin.left + "," + margin.top + ")");

var clip = svg.append("clipPath")
  .attr("id", "clip");
var clipRect = clip.append("rect")
  .attr("width", 0)
  .attr("height", height);

var data = d3.csvParse(d3.select("pre#data").text());

data.reverse();

// format the data
data.forEach(function(d) {
  d.date = parseTime(d.date);
  d.close = +d.close;
});

// scale the range of the data
x.domain(d3.extent(data, function(d) {
  return d.date;
}));
y.domain([0, d3.max(data, function(d) {
  return d.close;
})]);

// add the area
svg.append("path")
  .data([data])
  .attr("class", "area")
  .attr("d", d => area(d, true))
  .attr("fill", "lightsteelblue")
  .attr("clip-path", "url(#clip)");

// add the valueline path.
svg.append("path")
  .data([data])
  .attr("class", "line")
  .attr("d", valueline)
  .attr("clip-path", "url(#clip)");

// add the X Axis
svg.append("g")
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x));

// add the Y Axis
svg.append("g")
  .call(d3.axisLeft(y));

clipRect.transition()
  .duration(5000)
  .ease(d3.easeLinear)
  .attr("width", width)
.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 2px;
}

pre#data {
  display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<pre id="data">
date,close
1-May-12,58.13
30-Apr-12,53.98
27-Apr-12,67.00
26-Apr-12,89.70
25-Apr-12,99.00
24-Apr-12,130.28
23-Apr-12,166.70
20-Apr-12,234.98
19-Apr-12,345.44
18-Apr-12,443.34
17-Apr-12,543.70
16-Apr-12,580.13
13-Apr-12,605.23
12-Apr-12,622.77
11-Apr-12,626.20
10-Apr-12,628.44
9-Apr-12,636.23
5-Apr-12,633.68
4-Apr-12,624.31
3-Apr-12,629.32
2-Apr-12,618.63
30-Mar-12,599.55
29-Mar-12,609.86
28-Mar-12,617.62
27-Mar-12,614.48
26-Mar-12,606.98
</pre>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Thank you. After your post, I read about the clip-path and understood the idea behind. It's ... just brilliant. Thanks, again. By the way, could you please give me a link to a certain example of using "interpolator?" I will write a summary note about this topic. – TrungNguyen Dec 27 '17 at 07:23
  • @TrungNguyen well, it's very complicated actually: that `d` atribute is a string... you'll have to interpolate one string to another string. Have a look here: https://stackoverflow.com/q/19750140/5768908 – Gerardo Furtado Dec 27 '17 at 07:31
  • I will spend sometime later for studying the interpolate. Now, only focus on "clip-path". Big thanks to you. – TrungNguyen Dec 27 '17 at 07:34
  • Actually you referred to the link I mentioned in the original post, and I myself was the one with last comment in it. I unluckily ignored the interpolate point. Will read it again later. – TrungNguyen Dec 27 '17 at 07:35