在本文中,我想展示 amCharts 4 的灵活性和真正强大的功能。我们将学习如何将多个图表组合在一起,并使用动画来打造电影般的体验。即使您只对创建与图表无关的动画感兴趣,您仍然可以使用这个库,因为它不仅仅是制作图表。amCharts 的核心是帮助处理所有 SVG 内容:创建、布局、动画——它本质上是一个让使用 SVG 变得有趣的库!
这就是我要谈论的事情。它实际上展示了七个不同的图表一起动画。我们将一起逐步完成它,涵盖它的工作原理以及如何重新创建它,以便您下次使用图表或复杂动画时能够将 amCharts 纳入您的工具箱。
查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 React Hook:setEffect 示例。
首先,让我们概述电影的阶段
电影中有很多内容。让我们将其分解成易于理解的部分,以便我们解析正在发生的事情,并在幕后重新创建这些部分。
以下是我们正在处理的要点
初始动画和状态
我们首先看到的是一个饼图从深处升起,上面有一条弯曲的线缠绕着它。此时饼图本身没有什么特别之处,但我们将在下一节中介绍它。
但那条弯曲的线是怎么回事呢?请记住,我们制作图表,因此这条线仅仅是一个带有一条线的 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 中 状态 的想法是:您可以在任何精灵上拥有任意数量的自定义状态。然后,与其创建多个具有各种不同属性的动画,不如将状态从一个状态更改为另一个状态,所有在目标状态上设置的必需属性都将从当前值动画到新状态值。
精灵的任何数字、百分比或颜色属性都可以进行动画处理。默认情况下,精灵内置了隐藏状态和默认状态。隐藏状态最初应用,然后是显示状态,即默认状态。当然,还有其他状态,例如悬停、活动、禁用、其他状态,包括自定义状态。以下是一个演示,它展示了一个切片图,其innerRadius
、radius
和fill
在悬停时进行动画处理
查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 分解 amCharts 电影,了解状态。
饼图弹出
以下是一个基本饼图的演示。经过一段时间后,我们将隐藏所有切片,除了一个切片,然后再次显示所有切片。
查看 CodePen 上 amCharts 团队 (@amcharts) 创建的 分解 amCharts 电影,饼图。
如果您查看演示中的代码,您将看到切片的一些属性是通过pieSeries.slices.template
或pieSeries.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)。
飞机变大并飞走了
当我们的飞机到达目标城市(在完整动画中是硅谷)时,我们会缩放和旋转它,使其水平变大。

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

这是通过对图表对象的 `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 吗?有示例可以展示吗?你对入门有什么问题吗?或者也许你对这个概念并不认同,想要聊聊优缺点?在评论区告诉我们吧!