用 amCharts 制作电影

Avatar of Antanas Marcelionis
Antanas Marcelionis

DigitalOcean 为您旅程的各个阶段提供云产品。立即开始使用 200 美元的免费积分!

在本文中,我想展示 amCharts 4 的灵活性和真正强大的功能。我们将学习如何将多个图表组合在一起,并使用动画来打造电影般的体验。即使您只对创建与图表无关的动画感兴趣,您仍然可以使用这个库,因为它不仅仅是制作图表。amCharts 的核心是帮助处理所有 SVG 内容:创建、布局、动画——它本质上是一个让使用 SVG 变得有趣的库!

这就是我要谈论的事情。它实际上展示了七个不同的图表一起动画。我们将一起逐步完成它,涵盖它的工作原理以及如何重新创建它,以便您下次使用图表或复杂动画时能够将 amCharts 纳入您的工具箱。

查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 React Hook:setEffect 示例

首先,让我们概述电影的阶段

电影中有很多内容。让我们将其分解成易于理解的部分,以便我们解析正在发生的事情,并在幕后重新创建这些部分。

以下是我们正在处理的要点

  1. 初始动画和状态
  2. 饼图弹出
  3. 饼图变形为一个国家
  4. 飞机飞往另一个国家
  5. 飞机变大飞走
  6. 柱状图出现并弯曲成雷达柱状图

初始动画和状态

我们首先看到的是一个饼图从深处升起,上面有一条弯曲的线缠绕着它。此时饼图本身没有什么特别之处,但我们将在下一节中介绍它。

但那条弯曲的线是怎么回事呢?请记住,我们制作图表,因此这条线仅仅是一个带有一条线的 XY 图表。所有其他细节——网格、标签、刻度线等——都被隐藏了。所以,我们实际上看到的是一个简化的折线图!

设置折线图和饼图动画

amCharts 将此图表上的线称为线系列。线系列有一个名为tensionX的变量,在本例中,它被设置为 0.75,从而 tạo ra một đường cong。我们必须将张力视为由两个人拉住的绳子,两端都有:绳子拉得越紧,张力越大;反之,随着两端松开,张力也变松。该 0.75 值从初始值(1)中减去四分之一单位,从而创建更小的张力。

// 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;

最初,系列的所有值都相同:一条直线。然后,我们将线的动画的valueY值设置为 80,这意味着它弹出到图表高度的第八行——这将为饼图进入留出足够的空间。

// 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);
}

以下是单独的折线图,以便我们更好地了解其外观

查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 分解 amCharts 电影,第 1 阶段

接下来,饼图从底部弹出。与线系列类似,amCharts 包含一个饼图系列,它具有一个可以设置为隐藏,状态为 400 的dy属性。

// 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;

为了说明这个概念,以下是一个使用简单圆圈代替饼图的演示

查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 分解 amCharts 电影,初始动画

简要概述 amChart 状态

amCharts 中 状态 的想法是:您可以在任何精灵上拥有任意数量的自定义状态。然后,与其创建多个具有各种不同属性的动画,不如将状态从一个状态更改为另一个状态,所有在目标状态上设置的必需属性都将从当前值动画到新状态值。

精灵的任何数字、百分比或颜色属性都可以进行动画处理。默认情况下,精灵内置了隐藏状态和默认状态。隐藏状态最初应用,然后是显示状态,即默认状态。当然,还有其他状态,例如悬停活动禁用其他状态,包括自定义状态。以下是一个演示,它展示了一个切片图,其innerRadiusradiusfill在悬停时进行动画处理

查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 分解 amCharts 电影,了解状态

饼图弹出

以下是一个基本饼图的演示。经过一段时间后,我们将隐藏所有切片,除了一个切片,然后再次显示所有切片。

查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 分解 amCharts 电影,饼图

如果您查看演示中的代码,您将看到切片的一些属性是通过pieSeries.slices.templatepieSeries.labels.template自定义的。当然,大多数自定义可以通过 使用主题 来完成(amCharts 4 支持同时使用多个主题),但由于我们只需要更改几个属性,因此我们可以 使用模板。我们使用的是饼图类型,饼图系列的所有切片都将使用提供的模板创建,该模板会将我们从模板中使用的任何继承属性传递到我们的饼图。

// 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);

如果您想为图表的字体设置自定义颜色怎么办?我们可以通过在数据中添加一个字段来实现,例如fontColor。这使我们能够在那里设置自定义颜色,然后告诉标签模板它应该查看该字段以告知color属性值。

// 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
}];

精灵的任何属性都可以像这样自定义。即使在图表初始化之后,我们也可以通过模板更改任何属性,或者如果我们需要访问某个单独的对象,我们可以使用类似series.slices.getIndex(3)这样的东西来将其隔离。

总结:图表上没有一个对象不能被自定义或访问、更改,即使在图表构建之后。我们拥有很大的灵活性!

饼图变形为一个国家

我直言不讳地说:不可能将整个饼图或其他复杂对象变形为一个国家的形状。在 amCharts 4 中,一个多边形可以变形为另一个多边形。并且存在一些预制方法,可以简单地将多边形变形为圆形或矩形。以下是我们将要采取的步骤

  • 首先,我们将隐藏饼图的所有切片,除了一个切片。这实际上将剩余的切片转换为一个圆形。
  • 然后,我们对innerRadius属性进行动画处理,使其变为 0,切片将变成一个真正的圆形。
  • 此时,地图图表已经存在,但它被隐藏在视野之外。在它隐藏的同时,我们将放大到选定的国家,并将它变形为圆形。
  • 接下来,我们将显示该国家(现在是一个圆形),并将隐藏饼图(此时看起来与圆形相同)。
  • 最后,我们将该国家变形回其原始形状。

以下是一个简化的演示,我们放大到该国家,将其变形为圆形,然后将其变形回其默认状态

查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 分解 amCharts 电影,变形

检查一下代码。请注意,我们调用的所有方法,例如 `zoomToMapObject`、`morphToCircle` 或 `morphBack`,都会返回一个 `Animation` 对象。动画对象会分发事件,例如 `animationstarted`、`animationprogress` 或 `animationended`,我们可以为它们添加监听器。这确保了一个动画只有在另一个动画完成后才会触发。而且,如果我们更改动画的持续时间,我们就不需要相应地调整计时器,因为事件会处理它。在 amCharts 4 中,Sprites、Components、DataItems、Animations 和其他对象都有一个事件调度器对象,它可以控制所有事件。你可以添加这些事件的监听器,并利用它们来让你的应用程序超级交互式。

飞机从一个国家飞往另一个国家

在某一时刻,一架飞机在地图图表上伦敦上空出现,并一直飞往硅谷。

这可能看起来很复杂、很吓人,但它使用了我们已经涵盖的很多概念,并且这些功能是 amCharts 中包含的 地图图表 的标准配置。

  • 创建了 `MapImageSeries`,并将 Sprites(圆形和标签)映射到两个城市实际的经纬度坐标。
// 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`,就像我们之前看到的标准线系列一样,根据提供的坐标,在两个城市之间创建一条线,从一个地图图像到另一个。默认情况下,这条线是按照物体之间最短距离绘制的。在这种情况下,恰好是一条曲线。如果我们想,我们可以把它变成一条直线。
// 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;
  • 一个物体(飞机)被添加到 `MapLine` 中,它通过从 0 到 1 动画飞机的 `position` 属性,在直线的两个端点之间移动。
// 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";

这是一个从伦敦飞往纽约的飞机的演示。

请查看 CodePen 上的这个示例:拆解 amCharts 动画,地图部分,由 amCharts 团队(@amcharts)制作。

注意飞机在到达直线中点时变大了?这是通过我们在结尾添加的另外三行代码实现的。

// 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;
})

我们使用了一种被称为“适配器”的方法,这是 amCharts 4 的另一个超级强大的功能。在这种情况下,适配器根据飞机的位置(0.5)修改 `scale` 属性(从 0.07 到 0.10)。

飞机变大并飞走了

当我们的飞机到达目标城市(在完整动画中是硅谷)时,我们会缩放和旋转它,使其水平变大。

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.

同时,我们创建另一个图表(SlicedChart 类型),并向其中添加一个 PictorialSeries。该系列共享与飞机相同的路径,这会为切片创建一个蒙版。我们可以在此处使用任何 SVG 路径。

当切片显示时,我们希望飞机飞走。

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.

这是通过对图表对象的 `dx` 属性进行动画实现的。

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);
}

这是一个切片图表的演示。

请查看 CodePen 上的这个示例:拆解 amCharts 动画,图形系列,由 amCharts 团队(@amcharts)制作。

飞机的尾迹再次使用线系列制作,类似于我们一开始使用的那个。这次,它是由两个独立的系列组成:一个带有正值,另一个带有负值。当一个系列的 `sequencedInterpolation` 属性设置为 `true` 时,动画会对每个数据值进行一些延迟,我们就会得到这样的效果。

请查看 CodePen 上的这个示例:拆解 amCharts 动画,飞机尾迹,由 amCharts 团队(@amcharts)制作。

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")

然后,随着飞机经过,出现一个水平滚动的天际线轮廓。

请查看 CodePen 上的这个示例:拆解 amCharts 动画,城市经过,由 amCharts 团队(@amcharts)制作。

这使用与飞机尾迹相同的图表。我们基本上是在图表中添加了另一个值轴,并创建了一个柱形系列。

// 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);

然后,我们更新背景为渐变。

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;

最后,我们放大图表到总范围的一半,以便我们可以稍后慢慢地改变缩放级别,以创建移动城市的特效。

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);
}

以下是所有代码,为了简洁,去掉了尾迹部分。

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);
}

柱状图出现并弯曲成雷达柱状图

你能猜到最后一幕发生了什么吗?初始图表(看起来像一个普通的柱状图)实际上被称为 雷达图表,它在图表 `startAngle`(269.9°)和 `endAngle`(270.1°)属性之间有一个非常小的弧度。

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

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

总弧度角只有 0.2° 度,这就是为什么图表的半径变得非常大,很难把它与普通的 XY 图表区分开来。我们所做的只是对开始和结束角度进行动画。我告诉你……我们真的可以对任何东西进行动画,包括角度!

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);
}

以下是这个弯曲动画的全部效果。

请查看 CodePen 上的这个示例:拆解 amCharts 动画,弯曲图表,由 amCharts 团队(@amcharts)制作。

好了,我们完成了!

哦,但我还想提最后一件重要的事情:容器。这支动画中的所有图表和其他非图表对象都包含在一个单独的 div 元素中。我们最初创建了 `mainConatainer`,并将所有对象排列在其中。容器支持水平、垂直、网格和绝对布局。可以对容器的子元素使用固定或绝对位置,并且容器可以嵌套在其他容器中。它们可以水平或垂直对齐,设置为固定或绝对的宽度或高度……等等。我有没有提到 amCharts 内置了日期、数字和文本格式化程序?说真的,我到此为止了。

作为 amCharts 团队的一员,我经常听到这样的评论:“但你也可以用 d3 来完成这一切。”是的,你可能说得对。但我们在这里仍然有真实的好处——更少的代码行、更少的编写时间,以及相对简单的启动。所有动画都由 1000 行代码组成,所以这也算是一个轻量级的资源。

但我真的很想知道你的想法。你之前尝试过 amCharts 吗?有示例可以展示吗?你对入门有什么问题吗?或者也许你对这个概念并不认同,想要聊聊优缺点?在评论区告诉我们吧!