Making Movies With amCharts

Publikováno: 16.1.2019

In this article, I want to show off the flexibility and real power of amCharts 4. We’re going to learn how to combine multiple charts that run together with animations that form a movie experience. Even if you’re only interested in creating a different kind of animation that has nothing to do with charts, you can still use this library, since it’s more than making charts. The core of amCharts is made to help with everything SVG: creation, layout, … Read article

The post Making Movies With amCharts appeared first on CSS-Tricks.

Celý článek

In this article, I want to show off the flexibility and real power of amCharts 4. We’re going to learn how to combine multiple charts that run together with animations that form a movie experience. Even if you’re only interested in creating a different kind of animation that has nothing to do with charts, you can still use this library, since it’s more than making charts. The core of amCharts is made to help with everything SVG: creation, layout, animation — basically a library that makes working with SVG fun!

Here’s the kind of thing I’m talking about. It's actually a demonstration of seven different charts animated together. We’ll walk through this together, covering how it works and how to re-create it so you can have amCharts in your tool belt the next time you’re working with charts or complex animations.

See the Pen React Hook: setEffect example by amCharts team (@amcharts) on CodePen.

First, let’s outline the stages of the movie

There’s a lot going on in the movie. Let’s break it up into digestible parts that allow us to parse out what’s going on and re-create those parts under the hood.

Here’s the gist of what we’re working with:

  1. The initial animations and states
  2. The pie chart pops up
  3. The pie chart morphs to a country
  4. The plane flies to some other country
  5. The plane becomes big and flies away
  6. The column chart appears and bends to a radar column chart

The initial animations and states

The first thing we’re hit is a pie chart rising from the depths with a curved line wrapped around it. There’s nothing special about the pie chart at this point, but we’ll cover it in the next section.

But what about that curved line? Remember, we make charts, so this line is simply a XY chart with a single line on it. All the other details — grid, labels, tick marks, etc. — are hidden. So, what we’re really looking at is a stripped-down line chart!

Setting up the line and pie chart animations

amCharts calls the lines on this chart a line series. A line series has a variable called tensionX and, in this case, it’s been set to 0.75, making for a curvy line. We have to think of tension like we would a rope that is being held by two people and both ends: the tighter the rope is pulled, the greater the tension; conversely, the tension gets looser as the two ends let up. That 0.75 value is a taking a quarter of a unit away from the initial value (1), creating less tension.

// Creates the line on the chart
var lineSeries = lineChart.series.push(new am4charts.LineSeries());
lineSeries.dataFields.dateX = "date";
lineSeries.dataFields.valueY = "value";
lineSeries.sequencedInterpolation = true;
lineSeries.fillOpacity = 0.3;
lineSeries.strokeOpacity = 0;
lineSeries.tensionX = 0.75; Loosens the tension to create a curved line
lineSeries.fill = am4core.color("#222a3f");
lineSeries.fillOpacity = 1;

Initially, all the values of the series are the same: a flat line. Then, we set valueY value of the line’s animation to 80, meaning it pops up to the eights row of the chart height — that will make plenty of room for the pie when it comes in.

// Defines the animation that reveals the curved line
lineSeries.events.on("shown", function(){
  setTimeout(showCurve, 2000)
});

// Sets the animation properties and the valueY so the line bounces up to
// 80 on the chart's y-axis
function showCurve() {
  lineSeries.interpolationEasing = am4core.ease.elasticOut;
  lineSeries.dataItems.getIndex(3).setValue("valueY", 80, 2000);
  setTimeout(hideCurve, 2000);
}

// This is the initial state where the line starts at 30 on the y-axis
// before it pops up to 80
function hideCurve() {
  lineSeries.interpolationEasing = am4core.ease.elasticOut;
  lineSeries.dataItems.getIndex(3).setValue("valueY", 30, 2000);
  setTimeout(showCurve, 2000);
}

Here is the line chart alone so we have a better visual for how that looks:

See the Pen deconstructing amCharts movie, stage 1 by amCharts team (@amcharts) on CodePen.

Next, the pie chart pops up from the bottom. Like a line series, amCharts includes a pie series and it has a dy property that we can set to hidden with a state of 400.

// Make the circle to show initially, meaning it will animate its properties from hidden state to default state
circle.showOnInit = true;

// Set the circle's easing and the duration of the pop up animation
circle.defaultState.transitionEasing = am4core.ease.elasticOut;
circle.defaultState.transitionDuration = 2500;

// Make it appear from bottom by setting dy of hidden state to 300;
circle.hiddenState.properties.dy = 300;

To illustrate this concept, here is a demo with that simple circle in place of a pie chart:

See the Pen deconstructing amCharts movie, initial animation by amCharts team (@amcharts) on CodePen.

A brief overview of amChart states

The idea of states in amCharts is this: you can have any number of custom states on any sprite. Then, instead of creating multiple animations with a slew of various different properties, state is changed from one to another and all the required properties that are set on the target state will animate from current values to the new state values.

Any numeric, percentage or color property of a sprite can be animated. By default, sprites have hidden and default states baked in. The hidden state is applied initially and followed by the revealed state, which is the default. There are other states, of course, like hover, active, disabled, among others, including custom states. Here is another demo showing a slice chart with innerRadius, radius and fill animating on hover:

See the Pen deconstructing amCharts movie, understanding states by amCharts team (@amcharts) on CodePen.

The pie chart pops up

Here is a demo of a basic pie chart. After some time, we’ll hide all the slices, except one, then show them all again.

See the Pen deconstructing amCharts movie, pie chart by amCharts team (@amcharts) on CodePen.

If you look at the code in the demo, you will see some of properties of the slices are customized via pieSeries.slices.template or pieSeries.labels.template. Most of the customization, of course, can be done using themes (amCharts 4 supports using multiple themes at the same time), but since we only need to change a few properties, we can use a template. We’re using a pie chart type and all of the slices of the pie series will be created using the provided template which passes any of the inherited properties we use from the template onto our pie chart.

// Call included themes for styling and animating
am4core.useTheme(am4themes_amchartsdark);
am4core.useTheme(am4themes_animated);
// ...
// Call the Pie Chart template
var pieChart = mainContainer.createChild(am4charts.PieChart);

What if you want to set a custom color for the chart’s font? We can do this by adding a field in the data, like fontColor. That allows us to set custom colors there and then tell the label template that it should look at the field to inform the color property value.

// Define custom values that override one provided by the template
pieChart.data = [{
  "name": "[bold]No[/]",
  "fontColor": am4core.color("#222a3f"),
  "value": 220,
  "tickDisabled":true
}, {
  "name": "Hm... I don't think so.",
  "radius":20,
  "value": 300,
  "tickDisabled":false
}, {
  "name": "Yes, we are using amCharts",
  "value": 100,
  "labelDisabled": true,
  "tickDisabled":true
}];

Any property of a sprite can be customized like this. And even later, after the chart is initialized, we can change any property via the template, or if we need to access some individual object, we can get any value using something like series.slices.getIndex(3) to isolate it.

To summarize: there isn't a single object on the chart that can’t be customized or accessed, changed, even after the chart is built. We’re working with a lot of flexibility!

The pie chart morphs into a country

I’ll be blunt: There is no way to morph a whole pie chart or some other complex object to the shape of a country. In amCharts 4, one polygon can morph into another one. And there are prebuilt methods that simply morph a polygon to a circle or to a rectangle. Here’s what we’ll do:

  • First, we hide all the slices of a pie chart, except one. This makes effectively transforms the remaining slice into a circle.
  • Then we animate the innerRadius property to 0, and the slice becomes a true circle.
  • There’s already a map chart at this moment, but it is hidden out of view. While it hides, we zoom into a selected country and morph it into a circle as well.
  • Next, we’ll show the country (which is now a circle) and hide the pie chart (which looks like the same circle at this time).
  • Finally, we morph the country back to its original shape.

Here is a simplified demo where we zoom in to the country, morph it to a circle, then morph it back to its default state:

See the Pen deconstructing amCharts movie, morphing by amCharts team (@amcharts) on CodePen.

Inspect that code. Note that all the methods we call, like zoomToMapObject, morphToCircle or morphBack, return an Animation object. An animation object dispatches events like animationstarted, animationprogress or animationended and we can attach listeners to them. This ensures that one animation is triggered only after another one is finished. And, if we change the duration of an animation, we won't need to adjust timers accordingly, because events will handle it. In amCharts 4, Sprites, Components, DataItems, Animations, and other objects have an event dispatcher object which regulate any events. You can add listeners for these events and use them to make your applications super interactive.

The plane flies from one country to another

At one point, an airplane surfaces at London on a map chart and travels all the way to Silicon Valley.

It might look complex and scary, but it’s using a lot of the concepts we’ve already covered and the features come standard with the map chart included in amCharts:

  • MapImageSeries is created and sprites (circles and labels) are mapped to the actual longitude latitude coordinates of both cities.
// Create first image container
var imageSeries = mapChart.series.push(new am4maps.MapImageSeries());

// London properties
var city1 = imageSeries.mapImages.create();
// London's latitude/longitude
city1.latitude = 51.5074;
city1.longitude = 0.1278;
// prevent from scaling when zoomed
city1.nonScaling = true;

// New York City properties
var city2 = imageSeries.mapImages.create();
// NY latitude/longitude
city2.latitude = 40.7128;
city2.longitude = -74.0060;
// Prevent scaling when zoomed
city2.nonScaling = true;
  • MapLineSeries, like the standard line series we saw earlier, creates a line between the cities based on the coordinates that are provided, going from one map image to another. By default, the line is drawn so that it follows the shortest distance between the objects. That happens to be a curved line in this case. We could make it a straight line if we’d like.
// Create the map line series
var lineSeries = mapChart.series.push(new am4maps.MapLineSeries());
var mapLine = lineSeries.mapLines.create();

// Tell the line to connect the two cities (latitudes/longitudes an be used alternatively)
mapLine.imagesToConnect = [city1, city2]

// Draw the line in dashes
mapLine.line.strokeDasharray = "1,1";
mapLine.line.strokeOpacity = 0.2;
  • An object (plane) is added to the MapLine and it moves between the endpoints of the line by animating the plane’s position property from 0 to 1.
// Create the plane container
var planeContainer = mapLine.lineObjects.create();

planeContainer.position = 0;
// Set the SVG path of a plane for the sprite
var plane = planeContainer.createChild(am4core.Sprite);
planeContainer.nonScaling = false;
planeContainer.scale = 0.015;

// SVG plane illustration
plane.path = "M71,515.3l-33,72.5c-0.9,2.1,0.6,4.4,2.9,4.4l19.7,0c2.8,0,5.4-1,7.5-2.9l54.1-39.9c2.4-2.2,5.4-3.4,8.6-3.4 l103.9,0c1.8,0,3,1.8,2.3,3.5l-64.5,153.7c-0.7,1.6,0.5,3.3,2.2,3.3l40.5,0c2.9,0,5.7-1.3,7.5-3.6L338.4,554c3.9-5,9.9-8,16.2-8c24.2,0,85.5-0.1,109.1-0.2c21.4-0.1,41-6.3,59-17.8c4.2-2.6,7.9-6.1,11.2-9.8c2.6-2.9,3.8-5.7,3.7-8.5c0.1-2.8-1.1-5.5-3.7-8.5c-3.3-3.7-7-7.2-11.2-9.8c-18-11.5-37.6-17.7-59-17.8c-23.6-0.1-84.9-0.2-109.1-0.2c-6.4,0-12.3-2.9-16.2-8L222.6,316.6c-1.8-2.3-4.5-3.6-7.5-3.6l-40.5,0c-1.7,0-2.9,1.7-2.2,3.3L237,470c0.7,1.7-0.5,3.5-2.3,3.5l-103.9,0c-3.2,0-6.2-1.2-8.6-3.4l-54.1-39.9c-2.1-1.9-4.7-2.9-7.5-2.9l-19.7,0c-2.3,0-3.8,2.4-2.9,4.4l33,72.5C72.6,507.7,72.6,511.8,71,515.3z";
plane.fill = am4core.color("#eeeab5");

plane.horizontalCenter = "middle";
plane.verticalCenter = "middle";

Here is a demo of a plane flying from London to New York:

See the Pen deconstructing amCharts movie, map part by amCharts team (@amcharts) on CodePen.

Notice that the plane becomes bigger when it hits the line’s halfway point? This is done with three additional lines of code we can stick at the end.

// Make the plane to be bigger in the middle of the line
planeContainer.adapter.add("scale", function(scale, target) {
  return (0.07 - 0.10 * (Math.abs(0.5 - target.position))) / mapChart.zoomLevel;
})

We’re using a method that called an adapter, which is another super-powerful feature of amCharts 4. In this case, the adapter modifies the scale property (0.07 to 0.10), based on planes position (0.5).

The plane becomes big and flies away

When our plane reaches the target city (Silicon Valley in the full movie), we scale and rotate it to become horizontal and big.

Animation of a simple plane illustration popping up over a point on a map at Silicon Valley. The plane starts small and then zooms in to a larger size that makes it appear up close.

At the same moment, we create another chart (the SlicedChart type) and add a PictorialSeries to it. The series share’s the same path as the plane, which creates a mask for the slices. We can use any SVG path here.

When the slices are shown, we want the plane to fly away:

Animation of the zoomed plane illustration from the previous image taking off and flying across the screen from left to right, leaving a chart behind it as it exits the screen.

This happens by animating the chart object’s dx property.

flyAway()

function flyAway(){
  var animation = pictorialChart.animate({property:"dx", to:2000}, 1500, am4core.ease.quadIn).delay(2000);
  animation.events.on("animationended", flyIn);
}

function flyIn(){
  var animation = pictorialChart.animate({property:"dx", from:-2000, to:0}, 1500, am4core.ease.quadOut);
  animation.events.on("animationended", flyAway);
}

Here is a demo of a Sliced chart:

See the Pen deconstructing amCharts movie, pictorial series by amCharts team (@amcharts) on CodePen.

The trail of a plane is again made with a line series, similar to the one we had in the beginning. This time, it's a combination of two separate series: one with positive and another with negative values. When a series has the sequencedInterpolation property set to true, the animation happens with some delay for each data value and we get an effect like this:

See the Pen deconstructing amCharts movie, plane trail by amCharts team (@amcharts) on CodePen.

var series1 = chart.series.push(new am4charts.LineSeries())
series1.dataFields.dateX = "date";
series1.dataFields.valueY = "value1";
series1.sequencedInterpolation = true;
series1.fillOpacity = 1;
series1.tensionX = 0.8;
series1.stroke = am4core.color("#222a3f")
series1.fill = am4core.color("#222a3f")

Then there’s the silhouette of a cityscape that scrolls horizontally as the plane passes by:

See the Pen deconstructing amCharts movie, city passing by by amCharts team (@amcharts) on CodePen.

This uses the same chart as the plane trail. We basically add another value axis to the chart and create a column series:

// Add a new axis to the chart
var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
// ... shortened for brevity

// Configure the column series
var series = chart.series.push(new am4charts.ColumnSeries())
series.dataFields.dateX = "date";
series.dataFields.valueY = "value";
series.sequencedInterpolation = true;
series.fillOpacity = 1;
series.fill = am4core.color("#222a3f");
series.stroke = am4core.color("#222a3f")

// Establish the columns at full width
series.columns.template.width = am4core.percent(100);

Then we update the background to gradient:

chart.background.fillOpacity = 0.2;
  var gradient = new am4core.LinearGradient();
  gradient.addColor(am4core.color("#222a3f"));
  gradient.addColor(am4core.color("#0975da"));
  gradient.rotation = -90;
  chart.background.fill = gradient;

And finally, we zoom in on the chart to half of the total range so that we can slowly change the zoom level later to create the moving city effect.

function startZoomAnimation(){
  // Animate the start and end values slowly to make it look like the city is moving
  var animation = dateAxis.animate([{property:"start", from:0, to:0.5}, {property:"end", from:0.5, to:1}], 15000, am4core.ease.linear);
  animation.events.on("animationended", startZoomAnimation);
}

Here is all the code, without the trail part for brevity:

am4core.useTheme(am4themes_amchartsdark);
am4core.useTheme(am4themes_animated);

// Main container
var mainContainer = am4core.create("introchart", am4core.Container);
mainContainer.width = am4core.percent(100);
mainContainer.height = am4core.percent(100);

var chart = mainContainer.createChild(am4charts.XYChart);
chart.padding(0, 0, 0, 0)
chart.zIndex = 20;

var data = [];
var date = new Date(2000, 0, 1, 0, 0, 0, 0);

for (var i = 0; i < 40; i++) {
  var newDate = new Date(date.getTime());
  newDate.setDate(i + 1);

  var value = Math.abs(Math.round(((Math.random() * 100 - i + 10) / 10)) * 10)

  data.push({ date: newDate, value: value });
}

chart.data = data;
chart.zoomOutButton.disabled = true;
chart.seriesContainer.zIndex = -1;

chart.background.fillOpacity = 0.2;
var gradient = new am4core.LinearGradient();
gradient.addColor(am4core.color("#222a3f"));
gradient.addColor(am4core.color("#0975da"));
gradient.rotation = -90;
chart.background.fill = gradient;

var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
dateAxis.renderer.grid.template.location = 0;
dateAxis.renderer.ticks.template.disabled = true;
dateAxis.renderer.axisFills.template.disabled = true;

dateAxis.renderer.labels.template.disabled = true;
dateAxis.rangeChangeEasing = am4core.ease.sinIn;
dateAxis.renderer.inside = true;
dateAxis.startLocation = 0.5;
dateAxis.endLocation = 0.5;
dateAxis.renderer.baseGrid.disabled = true;
dateAxis.tooltip.disabled = true;
dateAxis.renderer.line.disabled = true;
dateAxis.renderer.grid.template.strokeOpacity = 0.07;

var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.tooltip.disabled = true;
valueAxis.renderer.ticks.template.disabled = true;
valueAxis.renderer.axisFills.template.disabled = true;
valueAxis.renderer.labels.template.disabled = true;
valueAxis.renderer.inside = true;
valueAxis.min = 0;
valueAxis.max = 100;
valueAxis.strictMinMax = true;
valueAxis.tooltip.disabled = true;
valueAxis.renderer.line.disabled = true;
valueAxis.renderer.baseGrid.disabled = true;
valueAxis.renderer.grid.template.strokeOpacity = 0.07;

var series = chart.series.push(new am4charts.ColumnSeries())
series.dataFields.dateX = "date";
series.dataFields.valueY = "value";
series.sequencedInterpolation = true;
series.fillOpacity = 1;
series.fill = am4core.color("#222a3f");
series.stroke = am4core.color("#222a3f")

series.columns.template.width = am4core.percent(100);

chart.events.on("ready", startZoomAnimation);

function startZoomAnimation(){
  // Animate the start and end values slowly to make it look like city is moving
  var animation = dateAxis.animate([{property:"start", from:0, to:0.5}, {property:"end", from:0.5, to:1}], 15000, am4core.ease.linear);
  animation.events.on("animationended", startZoomAnimation);
}

The column chart appears and bends to a radar column chart

Can you guess what happens in the final scene? The initial chart (which looks like a regular column chart) is actually what’s called a radar chart with a very small arc between the chart’s startAngle (269.9°) and endAngle (270.1°) properties.

var radarChart = mainContainer.createChild(am4charts.RadarChart);
// ... Chart properties go here

radarChart.startAngle = 269.9;
radarChart.endAngle = 270.1;

The total arc angle is only 0.2° degrees and that's why the radius of a chart becomes very big and tough tell it apart from a regular XY chart. And all we do to bend the chart is animate the start and end angles. I told you… we can literally animate everything, including angles!

radarChart.events.on("ready", bend);

function bend() {
  var animation = radarChart.animate([{ property: "startAngle", to: 90 }, { property: "endAngle", to: 450 }], 3500, am4core.ease.cubicIn).delay(1000);
    animation.events.on("animationended", unbend);
}

function unbend() {
  var animation = radarChart.animate([{ property: "startAngle", to: 269.9 }, { property: "endAngle", to: 270.1 }], 3500, am4core.ease.cubicIn).delay(500);
  animation.events.on("animationended", bend);
}

Here’s that bending animation in all its glory:

See the Pen deconstructing amCharts movie, bending the chart by amCharts team (@amcharts) on CodePen.

OK, we are done!

Oh, but there is one last, important thing I would like to mention: Containers. All charts and other non-chart objects in this movie are contained in a single div element. Initially, we created mainConatainer and arranged all the objects inside it. Containers support horizontal, vertical, grid and absolute layouts. Fixed or absolute positions can be used for a container's children, and containers can be nested inside other containers. They can be aligned horizontally or vertically, set to a fixed or absolute width or height… and so on. And did I mentioned, that amCharts has built-in date, number and text formatters? Seriously, I will stop here.

As a part of the amCharts team, I often hear comments like, "but you can do all of this with d3.” Yes, you are probably right. But there are still real benefits we’re working with here — fewer lines of code, time spent writing it, and a relatively simple startup. All the animation is made up of 1,000 lines of code, so this is also a pretty lightweight resource.

But, I’m really interested in what you think. Have you tried amCharts before and have examples to show off? How about questions about getting started? Or maybe you’re not sold on the concept altogether and want to chat the pros and cons? Let’s hear it in the comments!

The post Making Movies With amCharts appeared first on CSS-Tricks.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace