Tobias Ahlin Home of tobiasahlin.com http://tobiasahlin.com Meaningful Motion with Action-Driven Animation <div class="blog-banner banner-gray"> <div class="blog-banner-content"> <div class="post-ada-container"> <div class="post-ada-window"> <div class="post-ada-message">Do thing?</div> <div class="post-ada-button post-ada-button-cancel">Cancel<div class="post-ada-button-overlay"></div></div> <div class="post-ada-button post-ada-button-ok">Do it<div class="post-ada-button-overlay"></div></div> </div> <img src="/static/cursor.png" width="30" class="post-ada-cursor" /> </div> </div> </div> <style> .post-ada-container { width: 100px; height: 80px; position: relative; display: inline-block; } .post-ada-window { border-radius: 3px; background-color: #fff; text-align: center; display: block; width: 100%; height: 100%; font-family: SF UI, SF Diplay, Helvetica Neue, Arial, sans-serif; font-weight: 400; font-size: 10px; transform: translateZ(0); overflow: hidden; } .post-ada-message { padding-top: 15px; transform: translateZ(0); } .post-ada-button { position: absolute; bottom: 0; height: 20px; background-color: #e5e5e5; border-top: 1px solid #cecece; width: 50%; line-height: 20px; padding-bottom: 1px; } .post-ada-button-overlay { position: absolute; height: 100%; width: 100%; top: 0; left: 0; background-color: #000; opacity: 0; } .post-ada-button-cancel { left: 0; } .post-ada-button-ok { right: 0; border-left: 1px solid #cecece; } .post-ada-cursor { position: absolute; bottom: -25px; right: -18px; } </style> <script> // Post icon animation var postIconTimeline = anime.timeline({ loop: true }); postIconTimeline .add({ targets: '.post-ada-cursor', translateX: "-75px", easing: "easeOutBack", duration: 400, delay: 100 }).add({ // Press button targets: '.post-ada-button-cancel .post-ada-button-overlay', opacity: [0, 0.3], easing: 'easeOutExpo', duration: 100 }).add({ // Press button targets: '.post-ada-button-cancel .post-ada-button-overlay', opacity: [0.3, 0], easing: 'easeOutExpo', duration: 100 }).add({ // Shrink modal targets: '.post-ada-window', scale: 0.7, opacity: 0, easing: "easeOutExpo", duration: 600 }).add({ // Reveal modal targets: '.post-ada-window', scale: 1, opacity: 1, easing: "easeOutExpo", duration: 600, delay: 1000 }).add({ // Move cursor targets: '.post-ada-cursor', translateX: "0px", easing: "easeOutBack", duration: 400, delay: 100 }).add({ // Press button targets: '.post-ada-button-ok .post-ada-button-overlay', opacity: [0, 0.3], easing: 'easeOutExpo', duration: 100, delay: 200 }).add({ // Press button targets: '.post-ada-button-ok .post-ada-button-overlay', opacity: [0.3, 0], easing: 'easeOutExpo', duration: 100 }).add({ // Expand modal targets: '.post-ada-window', scale: 1.3, opacity: 0, easing: "easeOutExpo", duration: 600 }).add({ // Reveal modal targets: '.post-ada-window', scale: [0.6, 1], opacity: 1, easing: "easeOutExpo", duration: 600, delay: 1000 }); </script> <p>How do we animate interfaces in ways that are not just beautiful, but meaningful? When we add motion to interfaces we want to in one way or another improve the user experience, be it through aiding the comprehension of a concept, setting the mood, improving the perception of speed, or directing the attention of a user. Regardless of the intent of the animation, when animations fail to be meaningful they often fail because of the same reason; failed animations simply visualize objects morphing between being hidden and visible, rather than visualizing the actions unfolding on screen. A window rarely just closes or opens; a message is sent, a draft is discarded, an item is used.</p> <p>This is essentially <em>state-driven animation</em> vs. <em>action-driven animation</em>. By applying action-driven animation you can catch yourself in the act of creating something that’s not as meaningful as it could be. Are you simply morphing between states, or are you visualizing actions? Meaningful motion is about clear and engaging storytelling, and we can apply action-driven animation to remind ourselves when we’re straying from that path.</p> <!--more--> <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/2.0.2/anime.min.js"></script> <h2 id="state-driven-animation">State-driven animation</h2> <p>Let’s take a look at a basic example of state-driven animation vs. action-driven animation: interacting with a modal. This is a simulation of state-driven animation being applied to a modal:</p> <div class="post-demo-content post-ada-canvas"> <div class="post-ada-content ada-modal-sda"> <div class="post-ada-pause-play post-ada-pause-play-1"></div> <div class="post-ada-modal"> <div class="post-ada-modal-message">Do you wish to do thing?</div> <div class="post-ada-modal-button post-ada-modal-button-left">Cancel<div class="post-ada-modal-button-overlay"></div></div> <div class="post-ada-modal-button post-ada-modal-button-right">Do thing<div class="post-ada-modal-button-overlay"></div></div> </div> </div> </div> <p>As the modal appears, it fades in. Whatever button is pushed, it fades out again. What’s wrong with this animation? Fading between hidden and visible isn’t helpful to understand what’s happening on screen, other than in underlining that an object is being shown and hidden.</p> <p>You might be thinking “C’mon, what is there to understand? It’s a modal. It’s being hidden, and shown!”. Yes, but—the user is also triggering an action. Rather than only morphing between states, we can use motion to reinforce what action is being triggered by the user.</p> <h2 id="action-driven-animation">Action-driven animation</h2> <p>Here’s is a simulation of action-driven animation being applied to the same modal:</p> <div class="post-demo-content post-ada-canvas"> <div class="post-ada-content ada-modal-ada"> <div class="post-ada-pause-play post-ada-pause-play-2"></div> <div class="post-ada-modal"> <div class="post-ada-modal-message">Do you wish to do thing?</div> <div class="post-ada-modal-button post-ada-modal-button-left">Cancel<div class="post-ada-modal-button-overlay"></div></div> <div class="post-ada-modal-button post-ada-modal-button-right">Do thing<div class="post-ada-modal-button-overlay"></div></div> </div> </div> </div> <p>Now, how is this better than the previous animation? Try to ignore for the moment the aesthetics of the two. What we want to focus on is this: what does the second animation convey that the first one does not?</p> <p>In the first example, with state-driven animation, we use two different animations: fading in, and fading out. Clicking <em>Cancel</em> and clicking <em>Do it</em> both triggers the fade out animation. In other words, the only thing we’re differentiating between is the <em>states</em> of the modal: hidden vs. visible.</p> <p>Contrarily, in the second example we’ve got three different animations, and we differ between the two <em>actions</em> by playing different animations depending on the chosen option. On <em>Cancel</em>, we clearly show that the modal is being cancelled by scaling it down and fading it out, sending it back to where it came from. When the affirmative action is selected (<em>Do thing</em>), we do the opposite: scale it up and fade it out, bringing it closer to the user. In other words, we’re not only differentiating between states, but <em>how you travel between those states</em>, i.e., what actions are being performed.</p> <p>It’s common to think of apps as a series of views or states, and animations as a way to travel between those states. Take this Email app and two of its states:</p> <p><img src="/static/action-driven-animation/state-driven-animation.png" alt="Illustrated example of state-driven animation, showing the transition between two states of an email app; the main window with a composing window overlaying it (state A), and after closing it, just the main window (state b)" /></p> <p>If we do so, it’s easy to in our animations neglect <em>how</em> these states are connected, and always use the same transition to animate between those states. Rule of thumb: if your methods are called something like <code class="highlighter-rouge">showWindow()</code> and <code class="highlighter-rouge">hideWindow()</code>, or if you’re only animating <code class="highlighter-rouge">opacity</code>, you’re leaving your users in the dark to figure out exactly what happened. They’ll see that something changed, but not what caused that change. It’s sort of like leaving a party without saying goodbye; the host won’t know if you hated the party or just had to run home to do your laundry.</p> <p>Action-driven animation elevates <em>the connections between the views</em> to become the plots of the motion. In other words, what took you between state and A and state B? In our Email app, you can get between state A (composing visible) and state B (composing hidden) in at least two different ways: either by sending an email, or by discarding an email and closing the window.</p> <p><img src="/static/action-driven-animation/action-driven-animation.png" alt="Illustrated example of action-driven animation, showing two different animations between two states of an email app; you can hide the compose window either by sending an email, or by discarding an email" /></p> <p>You’ll make your app easier to understand by visualizing whatever caused the change of state. By creating a unique animation for every action we can help users differentiate between different action paths, and help them intuitively gain a deeper understanding of the events unfolding on screen.</p> <h2 id="when-state-driven-animation-fails">When state-driven animation fails</h2> <p>That sounds simple, right? It’s easier said than done. A while back, just before giving a talk on UI animation at a conference in Prague, I was stumped by an animation used by Tumblr. Before continuing, I want to underline that I highly respect the Tumblr design team—they’ve produced some admirable design, and I have on several occasions used some of their implementations as examples to be inspired by. That said, something went wrong in this specific instance.</p> <p>I had just signed up for a new Tumblr account and was getting ready to make my first post. But for some reason, whatever I posted kept disappearing as I hit <em>Post</em>:</p> <video playsinline="" controls="" width="800" poster="/static/action-driven-animation/posters/tumblr.jpg"> <source src="/static/action-driven-animation/tumblr-fail.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/tumblr-fail.webm" type="video/webm" /> </video> <p>Or, as you can see towards the end of the clip, that’s not at all what happened. Whatever option I selected, <em>Close</em> and <em>Post</em> both triggered the same animation (with the exception of a spinner showing up briefly inside the Post button when I selected <em>Post</em>). Nothing gave me a hint of that something had been successfully posted. The post section just disappeared.</p> <p>What’s gone wrong here? On first sight this animation looks meaningful and helpful. But again, we’re just morphing between two states, rather than visualizing the actions unfolding on the screen. Cancelling a post and successfully posting is two fundamentally different actions, yet they share the same animation. In this instance state-driven animation doesn’t just fail to be as meaningful as it could be—it’s causing confusion and obfuscating what’s happening.</p> <p>To be fair to Tumblr, the issue is in this instance is being amplified by the Dashboard on-boarding that’s floating just below the navigation, pushing down new posts below the fold. But regardless of the unlucky circumstances, the animation can be improved by moving from state-driven animation to action-driven animation. Rather than only morphing between the two states of the posting section (visible and hidden), the action of posting could be associated with an animation that clearly transitions between you writing a post, to the post being published.</p> <h2 id="applying-action-driven-animation">Applying action-driven animation</h2> <p>So how do you go about using action-driven animation to improve the animations of your product? To show this in practice, I’ve mocked up a dummy email app (shown previously in the email illustration above). Let’s look closer at the actions related to working with a message, and make the move from state-driven animation to action-driven animation together.</p> <p>The first step is to list all important actions in the part of the app that you’re working with, rather than all views. There are only two views involved in working with a message in our Email app, but there are four different actions. You can:</p> <ol> <li>Create a new message</li> <li>Send the message</li> <li>Discard the message</li> <li>Close the message and save it as a draft</li> </ol> <p>If we just faded between the different views, this is what it would look like working with the app (going through the different actions in the order above):</p> <video playsinline="" width="800" controls="" poster="/static/action-driven-animation/posters/state-driven-animation.jpg"> <source src="/static/action-driven-animation/email-fade.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/email-fade.webm" type="video/webm" /> </video> <p>To create more meaningful animations we need to look critically at every fade and ask ourselves: what actions are being taken?</p> <h3 id="choosing-a-storytelling-axis">Choosing a storytelling axis</h3> <p>Before we create a unique animation for every action, let’s settle on a primary axis to weave our storytelling around. If you’re interacting with something that will affect yourself or the user directly (e.g. a modal asking you if you want to continue), the Z axis is usually the best choice. If we weave our storytelling around the Z axis, like we did in our modal example earlier, we can make the modal move closer towards you as you choose an affirmative action (scaling up the modal, almost as if it lifts off the screen and travels towards you), and we can make it move away from you as you dismiss it. The Z axis reinforces that the modal relates to <em>you</em>.</p> <p>When you’re interacting with something that relates to someone else, like an email message that you’re about to send off to someone, your best choice of primary axis is usually the Y axis. By moving something rapidly upwards along the Y axis and making it exit the screen while still accelerating away from you, we reinforce the concept of you sending off something to someone else. It’s almost as if you’re taking an envelope and sending it off to its next destination. Or as if you’re using a salmon cannon:</p> <p><img src="/static/action-driven-animation/cannon-1.gif" width="800" /></p> <p><img src="/static/action-driven-animation/cannon-2.gif" width="800" /></p> <p>Since we’re working with an email message here that we’ll eventually send off to someone else, let’s pick the Y axis as our primary axis. What that in mind, let’s break down every action and animation, before reviewing it all together.</p> <h3 id="creating-a-new-email">Creating a new email</h3> <p>Rather than only fading in a new message once you create it, we can animate the window to make it appear like we’re summoning a new message from below. Just like how in this lovely GIF (by <a href="http://jonasmosesson.se">Jonas Mosesson</a>) the toast appears with a bounce from below:</p> <p><img src="/static/action-driven-animation/toast.gif" /></p> <p>In the same vein, we can fade in the message as we animate it upwards along the Y axis, but we’ll make it stay in view:</p> <video playsinline="" controls="" width="800" poster="/static/action-driven-animation/posters/new-message.jpg"> <source src="/static/action-driven-animation/new-message-animated.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/new-message-animated.webm" type="video/webm" /> </video> <p>This animation is by itself not much more meaningful than only fading in and out, but it starts to get interesting as soon as we start adding the other animations that are clearly different from this, but connected to the same storyline.</p> <h3 id="discarding-a-message">Discarding a message</h3> <p>Once you discard an email we can make it fall back down, returning it to where it came from. Here I’m also applying a slight tilt to the window on its way down to illustrate that you’re throwing it away (rather than storing it somewhere). As if you’re dropping a piece of paper, rather than putting it away neatly:</p> <video playsinline="" controls="" width="800" poster="/static/action-driven-animation/posters/close.jpg"> <source src="/static/action-driven-animation/new-close-animated.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/new-close-animated.webm" type="video/webm" /> </video> <h3 id="sending-a-message">Sending a message</h3> <p>Once you hit send, let’s shoot it off, away from us (and hopefully towards a recipient):</p> <video playsinline="" controls="" width="800" poster="/static/action-driven-animation/posters/send.jpg"> <source src="/static/action-driven-animation/new-send-animated.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/new-send-animated.webm" type="video/webm" /> </video> <p>With those three actions visualized in distinctly different ways, we’ve now got a clear story created around how messages are created, discarded, and sent off. Even though you might not infer the meaning of all the animations on first sight (they’re not necessarily intuitive), they’re clearly different and connected to an action each, and you can at the very least learn how to tell the different animations apart.</p> <h3 id="saving-as-a-draft">Saving as a draft</h3> <p>We’ve got one more action to cover. Saving a draft is crucially <em>not</em> the same thing as closing the window and discarding an email. Again, we’re dealing with actions, not states. It’s not a matter of showing or hiding objects, it’s a matter of visualizing the events unfolding on screen.</p> <p>If we try to close the new message window while writing a message, we’re asked if we want to discard the email, or save it as a draft. Given that someone chooses “Save as draft”, what’s important to convey? At least two things, 1) that the message is being saved as a draft somewhere, and 2) exactly where that somewhere is. We want users of our app to understand where they can find that draft later, so that they can send it when the time is right:</p> <video playsinline="" controls="" width="800" poster="/static/action-driven-animation/posters/save-draft.jpg"> <source src="/static/action-driven-animation/save-draft-animated.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/save-draft-animated.webm" type="video/webm" /> </video> <p>This animation clearly shows how your message message gets stored in your outbox, and it even tells you where your outbox is located! We’ve restricted ourselves to animating along the Y-axis up to this point to enforce the concept of <em>creating something and sending it off</em>. Now that we’re deviating from that storyline and instead storing a message, we can drop the axis restriction and shrink the message while we move it towards the outbox icon.</p> <p>To make it clear that we’re storing the message inside the outbox (and not just shrinking and hiding the message window), we’re adding another effect: as the message window arrives at the outbox icon, we make the outbox icon grow with a small bounce effect (as if it swallows the message), and we add a small counter indicating that your number of saved drafts just increased from 0 to 1. This animation doesn’t just tell you what events are being triggered: it teaches you how the app works, and how to navigate it.</p> <h3 id="review-before--after">Review: before &amp; after</h3> <p>Before we started applying action-driven animation, we faded views in and out, which gave the false impression of us using animations to make the experience better. The animations looked a bit better than simply popping views in and out of view, but the animations didn’t add much value in terms of helping users understand of what events were unfolding on screen. This is an example of state-driven animation, and it looked like this:</p> <video playsinline="" controls="" width="800" poster="/static/action-driven-animation/posters/state-driven-animation.jpg"> <source src="/static/action-driven-animation/email-fade.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/email-fade.webm" type="video/webm" /> </video> <p>After critically reviewing every animation and asking ourselves what each animation should convey and how best to do so, we iterated on the experience and created a set of animations that each conveys what events are unfolding on screen. We did so with the help of action-driven animation, which helps us identify the number of animations that we need to create through its focus on possible action paths, rather than on possible states. After applying action-driven animation, this is what our prototype looks like:</p> <video playsinline="" controls="" width="800" poster="/static/action-driven-animation/posters/action-driven-animation.jpg"> <source src="/static/action-driven-animation/all-animated.mp4" type="video/mp4" /> <source src="/static/action-driven-animation/all-animated.webm" type="video/webm" /> </video> <h2 id="in-sum">In sum</h2> <p>I divided UI animations into two different models: <strong>state-driven animation</strong> and <strong>action-driven animation</strong>. I argued that the first model tends to produce animations that lack any significant meaning; they are not helpful. I presented an example from Tumblr to illustrate how state.driven animation can, even when it looks beautiful, miscommunicate and cause confusion.</p> <p>I argued that a better way to work with animations is to focus on the actions or events that are being triggered, which I call <strong>action-driven animation</strong>. We reviewed how this technique can be applied to a modal and an email app, and how it aids users in understanding what actions and events are unfolding before them, but also how the app works, and how to navigate it.</p> <p>Is action-driven animation always better? The statistician George Box famously said “All models are wrong but some are useful”. He elaborated:</p> <blockquote> <p>All models are wrong but some are useful […] The only question of interest is “Is the model illuminating and useful?”</p> </blockquote> <p>In the same vein, action-driven animation is not a model that claims to absolutely explain or prove how an app should be animated. It’s a simplified way of thinking about motion that you can apply to review the interface animations of an app or product to create more meaningful animations. I’ve found it useful to use in my work, and I hope you will, too.</p> <h3 id="technical-details">Technical details</h3> <p>Although no code has been shared in this post, all of the examples that I created are animated with the lovely <a href="http://anime-js.com">anime.js library</a> (by <a href="http://juliangarnier.com">Julian Garnier</a>). If you want to play around with the email prototype and its animations, feel free <a href="/static/action-driven-animation/email-prototype.zip">download all the example code</a> and have a look around. All icons are from <a href="https://material.io/icons/">Google’s Material</a>.</p> <h3 id="related-posts">Related posts</h3> <ul> <li><a href="/blog/curved-path-animations-in-css/">Moving along a curved path in CSS with layered animation</a></li> <li><a href="/blog/how-to-animate-box-shadow/">How to animate “box-shadow” with silky smooth performance</a></li> <li><a href="/spinkit/">SpinKit: Simple spinners animated with CSS</a></li> </ul> <style> .post-ada-canvas { background-color: #2b2f3c; display: block; border: 0; } .post-ada-content { max-width: 700px; height: 500px; position: relative; } .post-ada-pause-play { width: 42px; height: 42px; background-color: #000; opacity: 0.3; position: absolute; top: 0px; left: 0px; border-radius: 30px; } .post-ada-pause-play:active { opacity: 0.5; } .post-ada-pause-play::after { content: ""; display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 20px; height: 20px; margin: auto; background-image: url(/static/play.svg); background-size: contain; } .post-ada-pause-play::before { content: ""; display: block; position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 20px; height: 20px; margin: auto; background-image: url(/static/pause.svg); background-size: contain; } .post-ada-pause-play.state-is-paused::after { display: block; } .post-ada-pause-play.state-is-paused::before { display: none; } .post-ada-modal { font-family: SF UI, SF Display, Helvetica Neue, Arial, sans-serif; position: absolute; margin: auto; top: 0; left: 0; bottom: 0; right: 0; background-color: #fff; border-radius: 5px; height: 200px; width: 300px; max-width: 100%; text-align: center; cursor: default; overflow: hidden; } .post-ada-modal-message { padding-top: 65px; } .post-ada-modal-button { border-top: 1px solid #cecece; height: 40px; line-height: 40px; position: absolute; width: 50%; bottom: 0; overflow: hidden; } .post-ada-modal-button-overlay { position: absolute; height: 100%; width: 100%; top: 0; left: 0; background-color: #000; opacity: 0; } .post-ada-modal-button-left { left: 0; } .post-ada-modal-button-right { right: 0; border-left: 1px solid #cecece; } </style> <script> // Animation 1 - state-driven animation var stateDrivenModalTimeline = anime.timeline({ loop: true }); stateDrivenModalTimeline .add({ targets: '.ada-modal-sda .post-ada-modal-button-left .post-ada-modal-button-overlay', opacity: [0, 0.3], easing: 'easeOutExpo', duration: 100 }) .add({ targets: '.ada-modal-sda .post-ada-modal-button-left .post-ada-modal-button-overlay', opacity: [0.3, 0], easing: 'easeOutExpo', duration: 100 }) .add({ targets: '.ada-modal-sda .post-ada-modal', opacity: 0, easing: 'easeOutExpo', delay: 200 }) .add({ targets: '.ada-modal-sda .post-ada-modal', opacity: 1, easing: 'easeOutExpo', delay: 400 }) .add({ targets: '.ada-modal-sda .post-ada-modal-button-right .post-ada-modal-button-overlay', opacity: [0, 0.3], easing: 'easeOutExpo', duration: 100 }) .add({ targets: '.ada-modal-sda .post-ada-modal-button-right .post-ada-modal-button-overlay', opacity: [0.3, 0], easing: 'easeOutExpo', duration: 100 }) .add({ targets: '.ada-modal-sda .post-ada-modal', opacity: 0, easing: 'easeOutExpo', delay: 200 }) .add({ targets: '.ada-modal-sda .post-ada-modal', opacity: 1, easing: 'easeOutExpo', delay: 400 }); // Animation 2 - action driven animation var actionDrivenModalTimeline = anime.timeline({ loop: true }); actionDrivenModalTimeline .add({ targets: '.ada-modal-ada .post-ada-modal-button-left .post-ada-modal-button-overlay', opacity: [0, 0.3], easing: 'easeOutQuad', delay: 300, duration: 100 }).add({ targets: '.ada-modal-ada .post-ada-modal-button-left .post-ada-modal-button-overlay', opacity: [0.3, 0], easing: 'easeOutQuad', duration: 100 }).add({ targets: '.ada-modal-ada .post-ada-modal', opacity: 0, scale: 0.8, easing: 'easeInQuad', duration: 400 }).add({ targets: '.ada-modal-ada .post-ada-modal', opacity: 1, scale: [0.5, 1], easing: 'easeOutQuad', duration: 500, delay: 600 }).add({ targets: '.ada-modal-ada .post-ada-modal-button-right .post-ada-modal-button-overlay', opacity: [0, 0.3], easing: 'easeOutQuad', delay: 200, duration: 100 }).add({ targets: '.ada-modal-ada .post-ada-modal-button-right .post-ada-modal-button-overlay', opacity: [0.3, 0], easing: 'easeOutQuad', duration: 100 }).add({ targets: '.ada-modal-ada .post-ada-modal', opacity: 0, scale: 1.2, duration: 400, easing: 'easeInQuad' }).add({ targets: '.ada-modal-ada .post-ada-modal', opacity: 1, scale: [0.5, 1], easing: 'easeOutQuad', duration: 400, delay: 600 }); // Wire up play buttons with animations document.querySelector('.post-ada-pause-play-1').onclick = function() {pausePlayAnimation(stateDrivenModalTimeline, this)}; document.querySelector('.post-ada-pause-play-2').onclick = function() {pausePlayAnimation(actionDrivenModalTimeline, this)}; // Helper function function pausePlayAnimation(timeline, e) { if (timeline.paused) { timeline.play(); $(e).removeClass("state-is-paused"); } else { timeline.pause(); $(e).addClass("state-is-paused"); } } </script> Tue, 16 May 2017 08:52:00 +0000 http://tobiasahlin.com/blog/meaningful-motion-w-action-driven-animation/ http://tobiasahlin.com/blog/meaningful-motion-w-action-driven-animation/ Data visualization with Chart.js: An introduction <div class="blog-banner banner-blue"> <div class="blog-banner-content"> <img class="chartjs-banner-logo" src="/static/chartjs-logo.svg" alt="Graph.js" height="120" width="120" /> </div> <style> .chartjs-banner-logo { background-color: #fff; padding: 20px; border-radius: 5px; box-shadow: 0 1px 5px rgba(0,0,0,0.1); margin-top: -35px; margin-left: -30px; } .article-preview .chartjs-banner-logo { margin-left: 0; } @media all and (max-width: 900px) { .chartjs-banner-logo { width: 80px; height: 80px; padding: 15px; margin-left: 0; } .article-preview .chartjs-banner-logo { margin-top: -25px; } } </style> </div> <p>You can tell powerful stories with data. If you want to visualize data in a blog post, on your site, or in a presentation, there are a few libraries out there that can help you achieve stunning results with relatively little work.</p> <p><a href="http://www.chartjs.org">Chart.js</a> is one of those libraries. When I’m teaching data at <a href="https://www.hyperisland.com">Hyper Island</a>, this is one of the essential tools that’s included in the <a href="https://www.hyperisland.com/programs-and-courses/digital-data-strategist">Data Strategist</a> program. Although less flexible and capable than <a href="https://d3js.org">D3</a>, it’s easier to wrap your head around and to get started with, yet powerful enough to cover more than just your basic needs. In this introductory tutorial we’ll build an interactive graph and get a brief overview of the framework’s cababilities.</p> <!--more--> <h2 id="what-well-build">What we’ll build</h2> <p>We’re going to create a simple but powerful responsive line graph, visualizing the world population during the last 500 years, and a prediction for 2050:</p> <h4 id="number-of-earthlings-in-millions">Number of earthlings (in millions)</h4> <p class="blog-content-wrapper"> <canvas id="chartjs-intro-myChart" width="800" height="450"></canvas> </p> <style> #myChart { width: 100%; } </style> <p>We’ll customize the graph to use our own colors, and you’ll be able to click the legends to toggle the visibility of the corresponding lines, as well as hover the points for details. You can <a href="/demo/chartjs-intro/static/final.zip">download the finished result</a> or <a href="/demo/chartjs-intro/">view the demo</a>.</p> <h2 id="what-youll-need">What you’ll need</h2> <p>No prior experience with either HTML, CSS or JavaScript is needed to go through this tutorial. Hi beginners 👋 some experience will help you grasp the nuances of what’s going on, but regardless of your level of expertize, coming out on the other side of the tutorial you’ll have a graph. All you need is a text editor (I highly recommend <a href="https://www.sublimetext.com">Sublime</a> or <a href="https://atom.io">Atom</a>).</p> <h2 id="wait-whats-chartjs">Wait, what’s Chart.js?</h2> <p><img alt="Chart.js logo" src="/static/chartjs-logo.svg" class="alignright" /> Chart.js is a community maintained open-source library (it’s <a href="https://github.com/chartjs/Chart.js">available on GitHub</a>) that helps you easily visualize data using JavaScript. It’s similar to <a href="https://gionkunz.github.io/chartist-js/">Chartist</a> and <a href="https://developers.google.com/chart/">Google Charts</a>. It supports 8 different chart types (including bars, lines, &amp; pies), and they’re all responsive. In other words, you set up your chart once, and Chart.js will do the heavy-lifting for you and make sure that it’s always legible (for example by removing some uncritical details if the chart gets smaller).</p> <p>In a gist, this is what you need to do to draw a chart with Chart.js:</p> <ol> <li>Define <em>where</em> on your page to draw the graph.</li> <li>Define <em>what type of graph</em> you want to draw.</li> <li><em>Supply Chart.js with data</em>, labels, and other options.</li> </ol> <p>…and you’ll get a beautiful, responsive, graph! Although we won’t dig too deep into changing the design of our graph in this tutorial, Chart.js graphs are highly customizable. As a rule, whatever you see you can affect, and although the charts look good without much customization, you’ll likely be able to realize all your (or someone else’s) design visions with some extra effort.</p> <h2 id="step-1-add-chartjs">Step 1: Add Chart.js</h2> <p>First of we need to add Chart.js to our page so that we can use the library. For this project, I’ve prepared a simple playground with a HTML file with only the essentials. <a href="/demo/chartjs-intro/static/starting-point.zip">Download the starting point</a> and open the folder, and you should see three files:</p> <ul> <li>index.html</li> <li>styles.css</li> <li>script.js</li> </ul> <p>I’ve added some basic styling to <code class="highlighter-rouge">style.css</code>, but <code class="highlighter-rouge">script.js</code> is completely empty—this is where we’ll add the code to draw our graph in a moment. For now, open up <code class="highlighter-rouge">index.html</code> in a text editor. To use Chart.js we need to link to the library inside of our <code class="highlighter-rouge">head</code>. You could download the library and host it yourself, but the easiest (and probably fastest) way is to just use a CDN (<em>content delivery network</em>, in this instance a fancy way of saying “nice people who’re hosting the libraries we need”).</p> <p>If you go to <a href="http://www.chartjs.org">chartjs.org</a> and click on <em>Documentation</em>, you’ll see a link to <a href="https://cdnjs.com/libraries/Chart.js">their recommended CDN</a>. Follow the link, and look for the URL ending with <code class="highlighter-rouge">Chart.min.js</code>. At the time of this writing, the latest version is <code class="highlighter-rouge">2.5.0</code>:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre> </div> <p>Copy and paste this row onto row 5 in <code class="highlighter-rouge">index.html</code>. After pasting, your <code class="highlighter-rouge">head</code> should look like so:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>World population<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">type=</span><span class="s">"text/css"</span> <span class="na">href=</span><span class="s">"style.css"</span><span class="nt">&gt;</span> <span class="nt">&lt;/head&gt;</span> </code></pre> </div> <h2 id="step-2-prepare-a-place-in-your-html-to-render-the-chart">Step 2: Prepare a place in your HTML to render the chart</h2> <p>The last thing we need to prepare before we can start visualizing our data is to define an area in our HTML where we want to draw the graph. For Chart.js you do this by adding a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API"><code class="highlighter-rouge">canvas</code></a> element, and setting <code class="highlighter-rouge">width</code> and <code class="highlighter-rouge">height</code> to define the proportions of your graph.</p> <p>On row 13 in <code class="highlighter-rouge">index.html</code>, copy and paste this row to create your canvas element:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"myChart"</span> <span class="na">width=</span><span class="s">"1600"</span> <span class="na">height=</span><span class="s">"900"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <p>Notice that we’ve added an <code class="highlighter-rouge">id</code> (<code class="highlighter-rouge">myChart</code>) to the <code class="highlighter-rouge">canvas</code> element that we can later use to reference our designated graph element in JavaScript or CSS. What this ID is set to has no significance for Chart.js; you can name it whatever you want. What matters is that you use the exact same ID when you reference it in JavaScript or CSS. If you’re adding several graphs to a page, just make sure that every ID is unique (you could for example give your graphs more specific names, like <code class="highlighter-rouge">populationChart</code> or <code class="highlighter-rouge">regionChart</code>).</p> <h2 id="step-3-prepare-the-data">Step 3: Prepare the data</h2> <p>The data we’re using is from <a href="https://en.wikipedia.org/wiki/World_population#Population_growth_by_region">Wikipedia’s article on World population</a>, and includes a world population prediction from <a href="https://esa.un.org/unpd/wpp/Publications/Files/Key_Findings_WPP_2015.pdf">UN’s World Population Prospects report</a>. Here’s the raw data that we’ll be using:</p> <h4 id="world-historical-and-predicted-populations-in-millions">World historical and predicted populations (in millions)</h4> <table class="article-table"> <thead> <tr> <th style="text-align: left">Country</th> <th style="text-align: right">1500</th> <th style="text-align: right">1600</th> <th style="text-align: right">1700</th> <th style="text-align: right">1750</th> <th style="text-align: right">1800</th> <th style="text-align: right">1850</th> <th style="text-align: right">1900</th> <th style="text-align: right">1950</th> <th style="text-align: right">1999</th> <th style="text-align: right">2050</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><strong>Africa</strong></td> <td style="text-align: right">86</td> <td style="text-align: right">114</td> <td style="text-align: right">106</td> <td style="text-align: right">106</td> <td style="text-align: right">107</td> <td style="text-align: right">111</td> <td style="text-align: right">133</td> <td style="text-align: right">221</td> <td style="text-align: right">783</td> <td style="text-align: right">2478</td> </tr> <tr> <td style="text-align: left"><strong>Asia</strong></td> <td style="text-align: right">282</td> <td style="text-align: right">350</td> <td style="text-align: right">411</td> <td style="text-align: right">502</td> <td style="text-align: right">635</td> <td style="text-align: right">809</td> <td style="text-align: right">947</td> <td style="text-align: right">1402</td> <td style="text-align: right">3700</td> <td style="text-align: right">5267</td> </tr> <tr> <td style="text-align: left"><strong>Europe</strong></td> <td style="text-align: right">168</td> <td style="text-align: right">170</td> <td style="text-align: right">178</td> <td style="text-align: right">190</td> <td style="text-align: right">203</td> <td style="text-align: right">276</td> <td style="text-align: right">408</td> <td style="text-align: right">547</td> <td style="text-align: right">675</td> <td style="text-align: right">734</td> </tr> <tr> <td style="text-align: left"><strong>Latin America</strong></td> <td style="text-align: right">40</td> <td style="text-align: right">20</td> <td style="text-align: right">10</td> <td style="text-align: right">16</td> <td style="text-align: right">24</td> <td style="text-align: right">38</td> <td style="text-align: right">74</td> <td style="text-align: right">167</td> <td style="text-align: right">508</td> <td style="text-align: right">784 </td> </tr> <tr> <td style="text-align: left"><strong>North America</strong></td> <td style="text-align: right">6</td> <td style="text-align: right">3</td> <td style="text-align: right">2</td> <td style="text-align: right">2</td> <td style="text-align: right">7</td> <td style="text-align: right">26</td> <td style="text-align: right">82</td> <td style="text-align: right">172</td> <td style="text-align: right">312</td> <td style="text-align: right">433</td> </tr> </tbody> </table> <p>To draw lines and add labels along axes, Chart.js expects the data to be passed in the form of a set of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array">arrays</a>, like so: <code class="highlighter-rouge">[10, 4, 7]</code>. We’re going to use 6 arrays in total: one for all the year labels to be shown along the X axis (1500-2050), and one array for each country, containing the population data. So all we need to do is copy each row in our table above, seperate each value with a comma, and then end and start the list with []-brackets.</p> <p>The table above, reformatted to arrays, looks like so:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="c1">// Our labels along the x-axis</span> <span class="kd">var</span> <span class="nx">years</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1500</span><span class="p">,</span><span class="mi">1600</span><span class="p">,</span><span class="mi">1700</span><span class="p">,</span><span class="mi">1750</span><span class="p">,</span><span class="mi">1800</span><span class="p">,</span><span class="mi">1850</span><span class="p">,</span><span class="mi">1900</span><span class="p">,</span><span class="mi">1950</span><span class="p">,</span><span class="mi">1999</span><span class="p">,</span><span class="mi">2050</span><span class="p">];</span> <span class="c1">// For drawing the lines</span> <span class="kd">var</span> <span class="nx">africa</span> <span class="o">=</span> <span class="p">[</span><span class="mi">86</span><span class="p">,</span><span class="mi">114</span><span class="p">,</span><span class="mi">106</span><span class="p">,</span><span class="mi">106</span><span class="p">,</span><span class="mi">107</span><span class="p">,</span><span class="mi">111</span><span class="p">,</span><span class="mi">133</span><span class="p">,</span><span class="mi">221</span><span class="p">,</span><span class="mi">783</span><span class="p">,</span><span class="mi">2478</span><span class="p">];</span> <span class="kd">var</span> <span class="nx">asia</span> <span class="o">=</span> <span class="p">[</span><span class="mi">282</span><span class="p">,</span><span class="mi">350</span><span class="p">,</span><span class="mi">411</span><span class="p">,</span><span class="mi">502</span><span class="p">,</span><span class="mi">635</span><span class="p">,</span><span class="mi">809</span><span class="p">,</span><span class="mi">947</span><span class="p">,</span><span class="mi">1402</span><span class="p">,</span><span class="mi">3700</span><span class="p">,</span><span class="mi">5267</span><span class="p">];</span> <span class="kd">var</span> <span class="nx">europe</span> <span class="o">=</span> <span class="p">[</span><span class="mi">168</span><span class="p">,</span><span class="mi">170</span><span class="p">,</span><span class="mi">178</span><span class="p">,</span><span class="mi">190</span><span class="p">,</span><span class="mi">203</span><span class="p">,</span><span class="mi">276</span><span class="p">,</span><span class="mi">408</span><span class="p">,</span><span class="mi">547</span><span class="p">,</span><span class="mi">675</span><span class="p">,</span><span class="mi">734</span><span class="p">];</span> <span class="kd">var</span> <span class="nx">latinAmerica</span> <span class="o">=</span> <span class="p">[</span><span class="mi">40</span><span class="p">,</span><span class="mi">20</span><span class="p">,</span><span class="mi">10</span><span class="p">,</span><span class="mi">16</span><span class="p">,</span><span class="mi">24</span><span class="p">,</span><span class="mi">38</span><span class="p">,</span><span class="mi">74</span><span class="p">,</span><span class="mi">167</span><span class="p">,</span><span class="mi">508</span><span class="p">,</span><span class="mi">784</span><span class="p">];</span> <span class="kd">var</span> <span class="nx">northAmerica</span> <span class="o">=</span> <span class="p">[</span><span class="mi">6</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">7</span><span class="p">,</span><span class="mi">26</span><span class="p">,</span><span class="mi">82</span><span class="p">,</span><span class="mi">172</span><span class="p">,</span><span class="mi">312</span><span class="p">,</span><span class="mi">433</span><span class="p">];</span> </code></pre> </div> <p>Copy all of these rows, and paste them into <code class="highlighter-rouge">script.js</code>.</p> <h2 id="step-4-draw-a-line">Step 4: Draw a line!</h2> <p>At last! We’ve arrived at the moment of truth. Visualizing data with Graph.js is pretty straightforward. All we need to do is define what graph we want to draw, and pass in the data that we want to visualize. Let’s start by drawing one single line to see if we can get it to work: below the data that you just pasted into <code class="highlighter-rouge">script.js</code>, add these lines of JavaScript:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"myChart"</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">myChart</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'line'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="nx">years</span><span class="p">,</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="nx">africa</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <p>…open up <code class="highlighter-rouge">index.html</code> in a browser, refresh and… tada! You created a graph! It’s not the prettiest, but hey, it’s rising and it’s looking all professional!</p> <p>What’s happening in this bit of code? Let’s break it down. First, we locate the <code class="highlighter-rouge">canvas</code> element that we added earlier to our <code class="highlighter-rouge">index.html</code> file (notice <code class="highlighter-rouge">"myChart"</code>):</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"myChart"</span><span class="p">);</span> </code></pre> </div> <p>Then, using that <code class="highlighter-rouge">canvas</code> element, we create a line chart (<code class="highlighter-rouge">type: 'line'</code>), and pass along some of our data. <code class="highlighter-rouge">labels: years</code> sets our array of <code class="highlighter-rouge">years</code> (that we created earlier) for the labels along the x-axis, and <code class="highlighter-rouge">data: africa</code> uses our <code class="highlighter-rouge">africa</code> variable to draw the line.</p> <p>For every line that we want to create, we add another <code class="highlighter-rouge">object</code> to the <code class="highlighter-rouge">datasets</code> array. On every <code class="highlighter-rouge">object</code> we can make a range of adjustments: we can not only pass the data to draw the line, but we can change the name, change the beavior, and change the looks of the line.</p> <p>Let’s try that right now. You may have noticed that our line is missing a label (it says <em>undefined</em> at the top of the graph), and it’s not very colorful. Boo! Let’s make it ✨👌</p> <h2 id="step-5-style-the-line">Step 5: Style the line</h2> <p>Start out by giving our first line a name. After <code class="highlighter-rouge">data: africa</code>, add a comma (<strong>hey!</strong> I’m serious about the comma (<em>remember the comma!</em>), miss it and everything breaks), create a new row, and add <code class="highlighter-rouge">label: "Africa"</code>:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code>{ data: africa, label: "Africa" } </code></pre> </div> <p>To set the border color and remove the big gray area below the graph, add another comma after <code class="highlighter-rouge">label: "Africa"</code> and add these two lines:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code>borderColor: "#3e95cd", fill: false </code></pre> </div> <p>Well ain’t that ✨👌 (refresh and you should see a blue line named Africa)!</p> <h2 id="step-6-add-the-rest-of-the-data">Step 6: Add the rest of the data</h2> <p>All we need to do now is copy the code for Africa and paste it another four times, adding a comma after every <code class="highlighter-rouge">}</code>. Go through the list of lines and make sure that you use <em>all</em> our region variables (<code class="highlighter-rouge">asia</code>, <code class="highlighter-rouge">europe</code>, etc.), and name the lines accordingly. Once you’re done, it should look something like this:</p> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="p">{</span> <span class="nl">data</span><span class="p">:</span> <span class="nx">africa</span><span class="p">,</span> <span class="nx">label</span><span class="err">:</span> <span class="s2">"Africa"</span><span class="p">,</span> <span class="nx">borderColor</span><span class="err">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="nx">fill</span><span class="err">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="nx">asia</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="nx">europe</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="nx">latinAmerica</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="nx">northAmerica</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"North America"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}</span> </code></pre> </div> <p>If you refresh, you should have a graph visualizing the earth’s population over time 🎉 Hooray!</p> <p>The lines are all the same color though. You can pick any color that you want for a line, but to use the same color scheme as in my example graph, change the <code class="highlighter-rouge">borderColor</code> value for each of the new regions: <code class="highlighter-rouge">#8e5ea2</code>, <code class="highlighter-rouge">#3cba9f</code>, <code class="highlighter-rouge">#e8c3b9</code>, <code class="highlighter-rouge">#c45850</code>.</p> <p>And with that, you’re done! Your finished graph should look <a href="/demo/chartjs-intro/">something like this</a>.</p> <h2 id="troubleshooting">Troubleshooting</h2> <p>Are you stuck somewhere? Is something not working? Don’t worry, that happens all the time. First of all, I recommend right-clicking somewhere on your page, selecting <em>Inspect Element</em> (or an equivelent), and then selecting the <em>Console</em>. If you’re using Safari and this option is not showing up: open <em>Preferences</em>, select <em>Advanced</em>, and at the very bottom, check <em>Show Develop menu in menu bar</em>.</p> <p>As soon as something is not working, get into the habit of checking the console for errors. In this particular case, unfortunately, Chart.js has a tendency to not throw very useful errors. To overcome any obstacles that you can’t resolve by working with the Console, you can compare your code with the intended result after each step:</p> <ul> <li><a href="/demo/chartjs-intro/static/01-Add Chartjs.zip">Step 1: Add Chart.js</a> (zip)</li> <li><a href="/demo/chartjs-intro/static/02-Canvas.zip">Step 2: Prepare a place in your HTML to render the chart</a> (zip)</li> <li><a href="/demo/chartjs-intro/static/03-Prepare data.zip">Step 3: Prepare the data</a> (zip)</li> <li><a href="/demo/chartjs-intro/static/04-Draw a line.zip">Step 4: Draw a line!</a> (zip)</li> <li><a href="/demo/chartjs-intro/static/05-Style the line.zip">Step 5: Style the line</a> (zip)</li> <li><a href="/demo/chartjs-intro/static/06-Add the data.zip">Step 6: Add the rest of the data</a> (zip)</li> </ul> <h2 id="next-steps">Next steps</h2> <p>Isn’t it amazing how by visualizing data you can take a cluster of disconnected numbers and create small simple stories that are completely intuitive to grasp? If you want to go beyond line graphs and start playing around with different ways of visualizing data, I recommend reading through the <a href="http://www.chartjs.org/docs/">Chart.js documentation</a>, or using my collection of Chart.js examples to kickstart your project: <a href="/blog/chartjs-charts-to-get-you-started/">10 Chart.js example charts to get you started</a>.</p> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script> <script src="/demo/chartjs-intro/script.js"></script> Wed, 03 May 2017 10:12:00 +0000 http://tobiasahlin.com/blog/introduction-to-chartjs/ http://tobiasahlin.com/blog/introduction-to-chartjs/ 10 Chart.js example charts to get you started <div class="blog-banner banner-pink"> <div class="blog-banner-content"> <img class="chartjs-banner-10x" src="/static/chartjs-10x.png" alt="10 Graph.js example graphs" height="142" width="160" /> </div> <style> .chartjs-banner-10x { margin-top: -35px; margin-left: -10px; } @media all and (max-width: 900px) { .chartjs-banner-10x { width: 120px; height: 106px; } .article-preview .chartjs-banner-10x { margin-top: -5px; } } </style> </div> <p><a href="http://www.chartjs.org/">Chart.js</a> is a powerful data visualization library, but I know from experience that it can be tricky to just get started and get a graph to show up. There are all sorts of things that can wrong, and I often just want to have <em>something</em> working so I can start tweaking it.</p> <p>This is a list of 10 working graphs (bar chart, pie chart, line chart, etc.) with colors and data set up to render decent looking charts that you can copy and paste into your own projects, and quickly get going with customizing and fine-tuning to make them fit your style and purpose.</p> <!--more--> <p>To use these examples, make sure to also include Chart.js:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre> </div> <p>These are the graphs that we’ll go through (click to get to the code):</p> <p class="post-chartjs-icon-overview"> <a href="#1-bar-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/bars.png" alt="Bar chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Bar chart</span> </a> <a href="#2-line-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/line.png" alt="Line chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Line chart</span> </a> <a href="#3-pie-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/pie.png" alt="Pie chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Pie chart</span> </a> <a href="#4-radar-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/radar.png" alt="Radar chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Radar chart</span> </a> <a href="#5-polar-area-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/polar.png" alt="Polar area chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Polar area</span> </a> <a href="#6-doughnut-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/doughnut.png" alt="Doughnut chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Doughnut chart</span> </a> <a href="#7-horizontal-bar-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/horizontal.png" alt="Horizontal bar chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Horizontal bars</span> </a> <a href="#8-grouped-bar-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/grouped.png" alt="Grouped bar chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Grouped bars</span> </a> <a href="#9-mixed-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/mixed.png" alt="Mixed chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Mixed charts</span> </a> <a href="#10-bubble-chart" class="post-chartjs-icon-overview-item"> <img src="/static/graph-icons/bubbles.png" alt="Bubble chart" width="95" height="60" /> <span class="post-chartjs-icon-overview-title">Bubble chart</span> </a> </p> <style> .post-chartjs-icon-overview { display: flex; flex-flow: row wrap; justify-content: space-between; } .post-chartjs-icon-overview-item { flex: 1 20%; background-color: #f2f2f2; border-radius: 3px; text-align: center; color: #111; max-width: 18%; font-size: 11px; text-decoration: none; margin-bottom: 10px; padding-top: 10px } .post-chartjs-icon-overview-item:hover { background-color: #e6e6e6; color: #000; } .post-chartjs-icon-overview-item img { height: auto; } .post-chartjs-icon-overview-title { margin-top: 10px; margin-bottom: 10px; display: block; line-height: 1.1em; } </style> <h2 id="1-bar-chart">1. Bar chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-bar-chart" width="800" height="450"></canvas> </p> <p>Bar charts are created by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">bar</code> (to flip the direction of the bars, <a href="#horizontal-bar-chart">set <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">horizontalBar</code></a>). The colors of the bars are set by passing one color to <code class="highlighter-rouge">backgroundColor</code> (all bars will have the same color), or an array of colors.</p> <p>If you’re passing an array (like in the example below), the colors are assigned to the label and number that share the same index in their respective arrays. I.e., below, “Africa” being the first label, will be set to <code class="highlighter-rouge">#3e95cd</code> (the first color), and <code class="highlighter-rouge">2478</code> (the first number).</p> <h4 id="bar-chart-html--javascript">Bar chart HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"bar-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="c1">// Bar chart</span> <span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"bar-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'bar'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Africa"</span><span class="p">,</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="s2">"North America"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Population (millions)"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="p">[</span><span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span><span class="s2">"#3cba9f"</span><span class="p">,</span><span class="s2">"#e8c3b9"</span><span class="p">,</span><span class="s2">"#c45850"</span><span class="p">],</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">2478</span><span class="p">,</span><span class="mi">5267</span><span class="p">,</span><span class="mi">734</span><span class="p">,</span><span class="mi">784</span><span class="p">,</span><span class="mi">433</span><span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">legend</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Predicted world population (millions) in 2050'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="2-line-chart">2. Line chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-line-chart" width="800" height="450"></canvas> </p> <p>Line charts are created by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">line</code>. By default, lines come with a dark transparent fill, covering the area between the line and x-axis. I think these fills tend to obfuscate other lines, so I’ve removed them on every dataset in this example (<code class="highlighter-rouge">fill: false</code>).</p> <p>If you want to remove fills for all your line graphs, a more efficiant way of achieving the same effect is to change the global default for fills: <code class="highlighter-rouge">Chart.defaults.global.elements.line.fill = false;</code>.</p> <h4 id="line-chart-html--javascript">Line chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"line-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"line-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'line'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="mi">1500</span><span class="p">,</span><span class="mi">1600</span><span class="p">,</span><span class="mi">1700</span><span class="p">,</span><span class="mi">1750</span><span class="p">,</span><span class="mi">1800</span><span class="p">,</span><span class="mi">1850</span><span class="p">,</span><span class="mi">1900</span><span class="p">,</span><span class="mi">1950</span><span class="p">,</span><span class="mi">1999</span><span class="p">,</span><span class="mi">2050</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[{</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">86</span><span class="p">,</span><span class="mi">114</span><span class="p">,</span><span class="mi">106</span><span class="p">,</span><span class="mi">106</span><span class="p">,</span><span class="mi">107</span><span class="p">,</span><span class="mi">111</span><span class="p">,</span><span class="mi">133</span><span class="p">,</span><span class="mi">221</span><span class="p">,</span><span class="mi">783</span><span class="p">,</span><span class="mi">2478</span><span class="p">],</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Africa"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">282</span><span class="p">,</span><span class="mi">350</span><span class="p">,</span><span class="mi">411</span><span class="p">,</span><span class="mi">502</span><span class="p">,</span><span class="mi">635</span><span class="p">,</span><span class="mi">809</span><span class="p">,</span><span class="mi">947</span><span class="p">,</span><span class="mi">1402</span><span class="p">,</span><span class="mi">3700</span><span class="p">,</span><span class="mi">5267</span><span class="p">],</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">168</span><span class="p">,</span><span class="mi">170</span><span class="p">,</span><span class="mi">178</span><span class="p">,</span><span class="mi">190</span><span class="p">,</span><span class="mi">203</span><span class="p">,</span><span class="mi">276</span><span class="p">,</span><span class="mi">408</span><span class="p">,</span><span class="mi">547</span><span class="p">,</span><span class="mi">675</span><span class="p">,</span><span class="mi">734</span><span class="p">],</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#3cba9f"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">40</span><span class="p">,</span><span class="mi">20</span><span class="p">,</span><span class="mi">10</span><span class="p">,</span><span class="mi">16</span><span class="p">,</span><span class="mi">24</span><span class="p">,</span><span class="mi">38</span><span class="p">,</span><span class="mi">74</span><span class="p">,</span><span class="mi">167</span><span class="p">,</span><span class="mi">508</span><span class="p">,</span><span class="mi">784</span><span class="p">],</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#e8c3b9"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">6</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">7</span><span class="p">,</span><span class="mi">26</span><span class="p">,</span><span class="mi">82</span><span class="p">,</span><span class="mi">172</span><span class="p">,</span><span class="mi">312</span><span class="p">,</span><span class="mi">433</span><span class="p">],</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"North America"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#c45850"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'World population per region (in millions)'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="3-pie-chart">3. Pie chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-pie-chart" width="800" height="450"></canvas> </p> <p>Pie charts are created by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">pie</code>. They are almost identical to <a href="#doughnut-chart">doughnut charts</a>, and will work with the same configurations (part from changing the type).</p> <h4 id="pie-chart-html--javascript">Pie chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"pie-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"pie-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'pie'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Africa"</span><span class="p">,</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="s2">"North America"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Population (millions)"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="p">[</span><span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span><span class="s2">"#3cba9f"</span><span class="p">,</span><span class="s2">"#e8c3b9"</span><span class="p">,</span><span class="s2">"#c45850"</span><span class="p">],</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">2478</span><span class="p">,</span><span class="mi">5267</span><span class="p">,</span><span class="mi">734</span><span class="p">,</span><span class="mi">784</span><span class="p">,</span><span class="mi">433</span><span class="p">]</span> <span class="p">}]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Predicted world population (millions) in 2050'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="4-radar-chart">4. Radar chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-radar-chart" width="800" height="600"></canvas> </p> <h4 id="radar-chart-html--javascript">Radar chart: HTML &amp; JavaScript</h4> <p>Radar charts—also known as web charts, spider charts, star charts—are created by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">radar</code>. Radar charts typically require more vertical space than other graphs to be legible, so you might have to tweak the graph proportions.</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"radar-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"600"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"radar-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'radar'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Africa"</span><span class="p">,</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="s2">"North America"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"1950"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(179,181,198,0.2)"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"rgba(179,181,198,1)"</span><span class="p">,</span> <span class="na">pointBorderColor</span><span class="p">:</span> <span class="s2">"#fff"</span><span class="p">,</span> <span class="na">pointBackgroundColor</span><span class="p">:</span> <span class="s2">"rgba(179,181,198,1)"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mf">8.77</span><span class="p">,</span><span class="mf">55.61</span><span class="p">,</span><span class="mf">21.69</span><span class="p">,</span><span class="mf">6.62</span><span class="p">,</span><span class="mf">6.82</span><span class="p">]</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"2050"</span><span class="p">,</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(255,99,132,0.2)"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"rgba(255,99,132,1)"</span><span class="p">,</span> <span class="na">pointBorderColor</span><span class="p">:</span> <span class="s2">"#fff"</span><span class="p">,</span> <span class="na">pointBackgroundColor</span><span class="p">:</span> <span class="s2">"rgba(255,99,132,1)"</span><span class="p">,</span> <span class="na">pointBorderColor</span><span class="p">:</span> <span class="s2">"#fff"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mf">25.48</span><span class="p">,</span><span class="mf">54.16</span><span class="p">,</span><span class="mf">7.61</span><span class="p">,</span><span class="mf">8.06</span><span class="p">,</span><span class="mf">4.45</span><span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Distribution in % of world population'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="5-polar-area-chart">5. Polar area chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-polar-chart" width="800" height="450"></canvas> </p> <p>A polar area chart is created by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">polarArea</code>. Polar area charts are closely related to pie charts, with the difference that in addition to the angles representing the relative size of the data points, the radius of each element is set in relation to its value.</p> <h4 id="polar-chart-html--javascript">Polar chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"polar-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"polar-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'polarArea'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Africa"</span><span class="p">,</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="s2">"North America"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Population (millions)"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="p">[</span><span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span><span class="s2">"#3cba9f"</span><span class="p">,</span><span class="s2">"#e8c3b9"</span><span class="p">,</span><span class="s2">"#c45850"</span><span class="p">],</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">2478</span><span class="p">,</span><span class="mi">5267</span><span class="p">,</span><span class="mi">734</span><span class="p">,</span><span class="mi">784</span><span class="p">,</span><span class="mi">433</span><span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Predicted world population (millions) in 2050'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="6-doughnut-chart">6. Doughnut chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-doughnut-chart" width="800" height="450"></canvas> </p> <p>Doughnut charts are created by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">doughnut</code>. They are almost identical to <a href="#pie-chart">pie charts</a>, and will work the same configurations.</p> <h4 id="doughnut-chart-html--javascript">Doughnut chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"doughnut-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"doughnut-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'doughnut'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Africa"</span><span class="p">,</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="s2">"North America"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Population (millions)"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="p">[</span><span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span><span class="s2">"#3cba9f"</span><span class="p">,</span><span class="s2">"#e8c3b9"</span><span class="p">,</span><span class="s2">"#c45850"</span><span class="p">],</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">2478</span><span class="p">,</span><span class="mi">5267</span><span class="p">,</span><span class="mi">734</span><span class="p">,</span><span class="mi">784</span><span class="p">,</span><span class="mi">433</span><span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Predicted world population (millions) in 2050'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="7-horizontal-bar-chart">7. Horizontal bar chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-bar-chart-horizontal" width="800" height="450"></canvas> </p> <p>Horizontal bar charts are created by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">horizontalBar</code>. They are identical to regular bar charts in every other aspect, and will work with the same configurations.</p> <h4 id="horizontal-bar-chart-html--javascript">Horizontal bar chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"bar-chart-horizontal"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"bar-chart-horizontal"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'horizontalBar'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Africa"</span><span class="p">,</span> <span class="s2">"Asia"</span><span class="p">,</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="s2">"Latin America"</span><span class="p">,</span> <span class="s2">"North America"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Population (millions)"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="p">[</span><span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span><span class="s2">"#3cba9f"</span><span class="p">,</span><span class="s2">"#e8c3b9"</span><span class="p">,</span><span class="s2">"#c45850"</span><span class="p">],</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">2478</span><span class="p">,</span><span class="mi">5267</span><span class="p">,</span><span class="mi">734</span><span class="p">,</span><span class="mi">784</span><span class="p">,</span><span class="mi">433</span><span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">legend</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Predicted world population (millions) in 2050'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="8-grouped-bar-chart">8. Grouped bar chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-bar-chart-grouped" width="800" height="450"></canvas> </p> <p>A grouped bar chart is not a unique chart type per say, but it requires you to setup your data a bit differently compared to the bar charts we’ve seen so far.</p> <p><code class="highlighter-rouge">type</code> is still set to <code class="highlighter-rouge">bar</code>, but as soon as you pass more than one object to <code class="highlighter-rouge">datasets</code>, Chart.js will create a new group of bars for every object. Setting the color for that group of bars is then done by passing a color to <code class="highlighter-rouge">backgroundColor</code>.</p> <h4 id="grouped-bar-chart-html--javascript">Grouped bar chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"bar-chart-grouped"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"bar-chart-grouped"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'bar'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"1900"</span><span class="p">,</span> <span class="s2">"1950"</span><span class="p">,</span> <span class="s2">"1999"</span><span class="p">,</span> <span class="s2">"2050"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Africa"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">133</span><span class="p">,</span><span class="mi">221</span><span class="p">,</span><span class="mi">783</span><span class="p">,</span><span class="mi">2478</span><span class="p">]</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">408</span><span class="p">,</span><span class="mi">547</span><span class="p">,</span><span class="mi">675</span><span class="p">,</span><span class="mi">734</span><span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Population growth (millions)'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="9-mixed-chart">9. Mixed chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-mixed-chart" width="800" height="450"></canvas> </p> <p>You can mix several charts and overlay them on top of each other. This is done by setting <code class="highlighter-rouge">type</code> to <code class="highlighter-rouge">bar</code> (<strong>not</strong> to e.g. <code class="highlighter-rouge">mixed</code> or <code class="highlighter-rouge">line</code>—it has to be <code class="highlighter-rouge">bar</code>), and then setting the bar type for every dataset object in your datasets array.</p> <p>To produce the graph above, for example, we have four data objects: two set to <code class="highlighter-rouge">bar</code>, and two set to <code class="highlighter-rouge">line</code>, while the <code class="highlighter-rouge">type</code> for the Chart object is set to <code class="highlighter-rouge">bar</code>.</p> <h4 id="mixed-chart-html--javascript">Mixed chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"mixed-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"450"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"mixed-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'bar'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="p">[</span><span class="s2">"1900"</span><span class="p">,</span> <span class="s2">"1950"</span><span class="p">,</span> <span class="s2">"1999"</span><span class="p">,</span> <span class="s2">"2050"</span><span class="p">],</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="s2">"line"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#8e5ea2"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">408</span><span class="p">,</span><span class="mi">547</span><span class="p">,</span><span class="mi">675</span><span class="p">,</span><span class="mi">734</span><span class="p">],</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Africa"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="s2">"line"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">133</span><span class="p">,</span><span class="mi">221</span><span class="p">,</span><span class="mi">783</span><span class="p">,</span><span class="mi">2478</span><span class="p">],</span> <span class="na">fill</span><span class="p">:</span> <span class="kc">false</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Europe"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="s2">"bar"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(0,0,0,0.2)"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">408</span><span class="p">,</span><span class="mi">547</span><span class="p">,</span><span class="mi">675</span><span class="p">,</span><span class="mi">734</span><span class="p">],</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="s2">"Africa"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="s2">"bar"</span><span class="p">,</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(0,0,0,0.2)"</span><span class="p">,</span> <span class="na">backgroundColorHover</span><span class="p">:</span> <span class="s2">"#3e95cd"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[</span><span class="mi">133</span><span class="p">,</span><span class="mi">221</span><span class="p">,</span><span class="mi">783</span><span class="p">,</span><span class="mi">2478</span><span class="p">]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Population growth (millions): Europe &amp; Africa'</span> <span class="p">},</span> <span class="na">legend</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="10-bubble-chart">10. Bubble chart</h2> <p class="blog-content-wrapper"> <canvas id="post-example-bubble-chart" width="800" height="800"></canvas> </p> <p>Last but not least, there’s the bubble chart, a favorite of <a href="https://www.ted.com/talks/hans_rosling_shows_the_best_stats_you_ve_ever_seen">Hans Rosling</a>. Bubble charts can be great for visualizing a lot of different data points simultaneously. In this example, every bubble is made up of three values: x position, y position, and size (<code class="highlighter-rouge">r</code>)—showing the GDP, happiness, and population, respectively, of each country.</p> <p>In the example above, I’m using the happiness index from the <a href="http://worldhappiness.report/wp-content/uploads/sites/2/2016/03/HR-V1Ch2_web.pdf">World Happiness Report</a> for a country’s Y position, GDP estimates from <a href="http://bit.ly/2dcX425">International Monetary Fund</a> to set the X position, and the population size to set the size of the bubble. (Note that I’ve removed some of the data in the example code to reduce the amount of code you have to copy.)</p> <p>You can pass several objects (setting <code class="highlighter-rouge">x</code>, <code class="highlighter-rouge">y</code>, and <code class="highlighter-rouge">x</code>) to each <code class="highlighter-rouge">data</code> array within every dataset object (each object will create a new bubble), but in this example I’m using only one object per array since I want every bubble to have a unique color and label.</p> <h4 id="bubble-chart-html--javascript">Bubble chart: HTML &amp; JavaScript</h4> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;canvas</span> <span class="na">id=</span><span class="s">"bubble-chart"</span> <span class="na">width=</span><span class="s">"800"</span> <span class="na">height=</span><span class="s">"800"</span><span class="nt">&gt;&lt;/canvas&gt;</span> </code></pre> </div> <div class="language-javascript highlighter-rouge"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Chart</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"bubble-chart"</span><span class="p">),</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="s1">'bubble'</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">labels</span><span class="p">:</span> <span class="s2">"Africa"</span><span class="p">,</span> <span class="na">datasets</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="p">[</span><span class="s2">"China"</span><span class="p">],</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(255,221,50,0.2)"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"rgba(255,221,50,1)"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">21269017</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="mf">5.245</span><span class="p">,</span> <span class="na">r</span><span class="p">:</span> <span class="mi">15</span> <span class="p">}]</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Denmark"</span><span class="p">],</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(60,186,159,0.2)"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"rgba(60,186,159,1)"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">258702</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="mf">7.526</span><span class="p">,</span> <span class="na">r</span><span class="p">:</span> <span class="mi">10</span> <span class="p">}]</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Germany"</span><span class="p">],</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(0,0,0,0.2)"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"#000"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">3979083</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="mf">6.994</span><span class="p">,</span> <span class="na">r</span><span class="p">:</span> <span class="mi">15</span> <span class="p">}]</span> <span class="p">},</span> <span class="p">{</span> <span class="na">label</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Japan"</span><span class="p">],</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="s2">"rgba(193,46,12,0.2)"</span><span class="p">,</span> <span class="na">borderColor</span><span class="p">:</span> <span class="s2">"rgba(193,46,12,1)"</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">[{</span> <span class="na">x</span><span class="p">:</span> <span class="mi">4931877</span><span class="p">,</span> <span class="na">y</span><span class="p">:</span> <span class="mf">5.921</span><span class="p">,</span> <span class="na">r</span><span class="p">:</span> <span class="mi">15</span> <span class="p">}]</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="s1">'Predicted world population (millions) in 2050'</span> <span class="p">},</span> <span class="na">scales</span><span class="p">:</span> <span class="p">{</span> <span class="na">yAxes</span><span class="p">:</span> <span class="p">[{</span> <span class="na">scaleLabel</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">labelString</span><span class="p">:</span> <span class="s2">"Happiness"</span> <span class="p">}</span> <span class="p">}],</span> <span class="na">xAxes</span><span class="p">:</span> <span class="p">[{</span> <span class="na">scaleLabel</span><span class="p">:</span> <span class="p">{</span> <span class="na">display</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">labelString</span><span class="p">:</span> <span class="s2">"GDP (PPP)"</span> <span class="p">}</span> <span class="p">}]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span> </code></pre> </div> <h2 id="next-steps">Next steps</h2> <p>I hope you’ll be able to kick-start your process and quickly get started with these template graphs. Chart.js is highly customizable, so if you want to change the design of the graphs I recommend digging into <a href="http://www.chartjs.org/docs/">the official documentation</a> to explore all the parameters that you can tweak.</p> <p>If you’re new to <a href="http://www.chartjs.org/">Chart.js</a> and want to get a better overview of the library, I recommend reading my earlier post: <a href="/blog/introduction-to-chartjs/">Data visualization with Chart.js: An introduction</a>.</p> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script> <script src="/js/posts/chartjs-example-charts.js"></script> Wed, 03 May 2017 10:12:00 +0000 http://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/ http://tobiasahlin.com/blog/chartjs-charts-to-get-you-started/ Moving along a curved path in CSS with layered animation <div class="blog-banner banner-green"> <div class="blog-banner-content mnc-demo-container"> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot"></div> </div> <style> .mnc-demo-container { height: 100px; width: 100px; } .mnc-demo-grid { overflow: hidden; border: 2px solid rgba(0,0,0,0.2); height: 100%; width: 100%; } .mnc-demo-line { width: 100%; height: 2px; background-color: rgba(0,0,0,0.2); position: absolute; } .mnc-demo-line-vertical { width: 2px; height: 100%; background-color: rgba(0,0,0,0.2); position: absolute; } .mnc-demo-dot { will-change: transform; -webkit-animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); position: absolute; bottom: -10px; left: -10px; } .mnc-demo-dot:after { content: ''; display: block; will-change: transform; width: 20px; height: 20px; border-radius: 20px; background-color: #fff; -webkit-animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); } .mnc-demo-dot-final-x { -webkit-animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1); } .mnc-demo-dot-final-y { -webkit-animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64); } .mnc-demo-dot-final::after { -webkit-animation: none; animation: none; } @keyframes yAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @-webkit-keyframes yAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1); -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @keyframes xAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); -webkit-transform: translateX(100px); transform: translateX(100px); } } @-webkit-keyframes xAxis { 50% { -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64); -webkit-transform: translateX(100px); transform: translateX(100px); } } </style> </div> <p>CSS animations and transitions are great for animating something from point A to B. That is, if you want to animate along a straight path. No matter how much you bend your <a href="https://developer.mozilla.org/en/docs/Web/CSS/timing-function">bezier curves</a>, you can’t make something move along a curved path by applying an <code class="highlighter-rouge">animation</code> or a <code class="highlighter-rouge">transition</code> to an object. You can overshoot with custom timing functions, and produce spring-like effects, but the relative movement along the X and Y-axis will always be identical.</p> <p>Instead of turning to JavaScript for producing more natural-looking motion, there’s an easy way to work around this limitation: layered animation. By using two or more objects to drive an animation, we get fine-grained control over an object’s path, and can apply one timing function for the movement along the X-axis, and another one for the Y-axis.</p> <!--more--> <h2 id="the-problem">The problem</h2> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Regular path</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">What we want</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot"></div> </div> <style> .mnc-demo-container-inline { display: inline-block; position: relative; margin-right: 60px; margin-bottom: 30px; margin-top: 15px; } .mnc-demo-label { position: absolute; bottom: -38px; color: #666; font-size: 11px; text-align: center; width: 100%; } .mnc-demo-container-inline .mnc-demo-dot:after { background-color: #333; } .mnc-demo-dot-straight { -webkit-animation: straightLine 2.5s infinite linear; animation: straightLine 2.5s infinite linear; } .mnc-demo-dot-straight:after { -webkit-animation: none; animation: none; } @-webkit-keyframes straightLine { 50% { -webkit-transform: translate3D(100px, -100px, 0); transform: translate3D(100px, -100px, 0); } } @keyframes straightLine { 50% { -webkit-transform: translate3D(100px, -100px, 0); transform: translate3D(100px, -100px, 0); } } </style> </div> <p>Before we dive into the solution, let’s look closer at the problem. CSS <code class="highlighter-rouge">animations</code> and <code class="highlighter-rouge">transitions</code> restrict us to animating along straight paths. How? By always taking the shortest path from point A to point B. That’s great—this is what we’re after in most cases—but we lack a way of telling CSS to “take a nicer path” rather than “take the shortest path”.</p> <p>The most straightforward way of animating between two points in CSS (with hardware acceleration) is to use <code class="highlighter-rouge">transform</code> to <code class="highlighter-rouge">translate</code> an object over time. This produces movement along a linear path. In this <code class="highlighter-rouge">@keyframes</code> block, we’re going back and forth between (0,0) and (100,-100), as seen in the example above:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="k">@keyframes</span> <span class="n">straightLine</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translate3D</span><span class="p">(</span><span class="m">100px</span><span class="p">,</span> <span class="m">-100px</span><span class="p">,</span> <span class="m">0</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="nc">.dot</span> <span class="p">{</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">straightLine</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">linear</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>This isn’t complicated, but let’s stop here for a moment. To understand the solution to the problem we need to, at least visually, take the animation apart.</p> <p>At <code class="highlighter-rouge">0%</code> we start out at (0,0), and at <code class="highlighter-rouge">50%</code> we use <code class="highlighter-rouge">translate3D(100px, -100px, 0)</code> to move to (100,-100), then back again. Put another way, we move the object <code class="highlighter-rouge">100px</code> to the right, and <code class="highlighter-rouge">100px</code> upwards, and these two translations combined move the object at an angle.</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-straightX"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-straightY"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Combined</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight"></div> </div> <style> .mnc-demo-dot-straightX { -webkit-animation: straightLineX 2.5s infinite linear; animation: straightLineX 2.5s infinite linear; } .mnc-demo-dot-straightY { -webkit-animation: straightLineY 2.5s infinite linear; animation: straightLineY 2.5s infinite linear; } @-webkit-keyframes straightLineX { 50% { -webkit-transform: translateX(100px); transform: translateX(100px); } } @keyframes straightLineX { 50% { -webkit-transform: translateX(100px); transform: translateX(100px); } } @-webkit-keyframes straightLineY { 50% { -webkit-transform: translateX(100px); transform: translateX(100px); } } @keyframes straightLineY { 50% { -webkit-transform: translateY(-100px); transform: translateY(-100px); } } </style> </div> <h2 id="the-solution-one-timing-function-per-axis">The solution: one timing function per axis</h2> <p>So how do we create a curved path like the one showcased in the earlier example? To create a path that doesn’t go in a straight line, <strong>we want the movement speed along the X-axis and Y-axis to be out of sync</strong>.</p> <p>The previous examples all used <code class="highlighter-rouge">linear</code> timing functions, but if we add a container around the object we want to animate, we can apply one timing function for the X-axis, and another one for the Y-axis. Below, we’re using <code class="highlighter-rouge">ease-in</code> for the X-axis, and <code class="highlighter-rouge">ease-out</code> for the Y-axis:</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X: ease-in</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-ease-in"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y: ease-out</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-ease-out"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Combined</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-combined"></div> </div> <style> .mnc-demo-dot-ease-in { -webkit-animation: combined-xAxis 2.5s infinite ease-in; animation: combined-xAxis 2.5s infinite ease-in; } .mnc-demo-dot-ease-out { -webkit-animation: combined-yAxis 2.5s infinite ease-out; animation: combined-yAxis 2.5s infinite ease-out; } .mnc-demo-dot-combined { -webkit-animation: combined-xAxis 2.5s infinite ease-in; animation: combined-xAxis 2.5s infinite ease-in; } .mnc-demo-dot-combined::after { -webkit-animation: combined-yAxis 2.5s infinite ease-out; animation: combined-yAxis 2.5s infinite ease-out; } @keyframes combined-xAxis { 50% { -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; -webkit-transform: translateX(100px); transform: translateX(100px); } } @-webkit-keyframes combined-xAxis { 50% { -webkit-animation-timing-function: ease-in; animation-timing-function: ease-in; -webkit-transform: translateX(100px); transform: translateX(100px); } } @keyframes combined-yAxis { 50% { -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; -webkit-transform: translateY(-100px); transform: translateY(-100px); } } @-webkit-keyframes combined-yAxis { 50% { -webkit-animation-timing-function: ease-out; animation-timing-function: ease-out; -webkit-transform: translateY(-100px); transform: translateY(-100px); } } </style> </div> <h2 id="the-implementation-one-object-per-axis">The implementation: one object per axis</h2> <p>Unfortunately, we can’t just stack <code class="highlighter-rouge">transform</code> animations; only the last animation to be declared would run. So, how do we actually combine two animations? We put one object inside another, and run one animation on the container element, and a different one on the child element.</p> <p>In all the examples above, where we’ve seen the dot move along a curved path, we’ve seen two separate elements being animated, but the container has been completely transparent. To clearly see how the two objects interact in the curved examples, we can change the container to render a bordered box:</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X: container</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-square mnc-demo-dot-straight mnc-demo-dot-ease-in"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y: object</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-straight mnc-demo-dot-ease-out"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Combined</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-square mnc-demo-dot-combined"></div> </div> <style> .mnc-demo-dot-square { border-radius: 0; border: 1px solid #000; width: 20px; height: 20px; } .mnc-demo-dot-square::after { display: none; } .mnc-demo-dot-combined.mnc-demo-dot-square::after { display: block; } </style> </div> <p>The dot sits inside the bordered box, and follows the box’s motion along the X-axis, while it itself moves up and down along the Y-axis. Remove the border of the box, and we’ve got our curved path. Rather than adding two objects in our HTML, though, we can produce a pseudo-element. If we have this in our HTML:</p> <div class="language-html highlighter-rouge"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"dot"</span><span class="nt">&gt;&lt;/div&gt;</span> </code></pre> </div> <p>We can add a pseudo-element like so:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code> <span class="nc">.dot</span> <span class="p">{</span> <span class="c">/* Container. Animate along the X-axis */</span> <span class="p">}</span> <span class="nc">.dot</span><span class="nd">::after</span> <span class="p">{</span> <span class="c">/* Render dot, and animate along Y-axis */</span> <span class="p">}</span> </code></pre> </div> <p>Then we need two separate animation blocks: one for the X-axis, and one for the Y-axis. Notice how one uses <code class="highlighter-rouge">ease-in</code>, while the other one uses <code class="highlighter-rouge">ease-out</code>:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="nc">.dot</span> <span class="p">{</span> <span class="c">/* Some layout code… */</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">xAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">ease-in</span><span class="p">;</span> <span class="p">}</span> <span class="nc">.dot</span><span class="nd">::after</span> <span class="p">{</span> <span class="c">/* Render dot */</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">yAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">ease-out</span><span class="p">;</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">xAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">ease-in</span><span class="p">;</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">yAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">ease-out</span><span class="p">;</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Put together, with vender-prefixes for WebKit, and with some custom bezier curves instead of <code class="highlighter-rouge">ease-in</code> and <code class="highlighter-rouge">ease-out</code>, we can produce the example showcased at the very beginning of the post:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="nc">.demo-dot</span> <span class="p">{</span> <span class="nl">-webkit-animation</span><span class="p">:</span> <span class="n">xAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">xAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.demo-dot</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">content</span><span class="p">:</span> <span class="s2">''</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">-webkit-animation</span><span class="p">:</span> <span class="n">yAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">animation</span><span class="p">:</span> <span class="n">yAxis</span> <span class="m">2.5s</span> <span class="n">infinite</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="p">}</span> <span class="k">@-webkit-keyframes</span> <span class="n">yAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">yAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.02</span><span class="p">,</span> <span class="m">0.01</span><span class="p">,</span> <span class="m">0.21</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateY</span><span class="p">(</span><span class="m">-100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@-webkit-keyframes</span> <span class="n">xAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">@keyframes</span> <span class="n">xAxis</span> <span class="p">{</span> <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="nl">-webkit-animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.3</span><span class="p">,</span> <span class="m">0.27</span><span class="p">,</span> <span class="m">0.07</span><span class="p">,</span> <span class="m">1.64</span><span class="p">);</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">translateX</span><span class="p">(</span><span class="m">100px</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre> </div> <p>Which brings us back to where we started:</p> <div class="post-demo-content"> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">X movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-final mnc-demo-dot-final-x"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Y movement</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot mnc-demo-dot-final mnc-demo-dot-final-y"></div> </div> <div class="mnc-demo-container mnc-demo-container-inline"> <span class="mnc-demo-label">Curved path</span> <div class="mnc-demo-grid"> <div class="mnc-demo-line" style="top: 20px;"></div> <div class="mnc-demo-line" style="top: 40px;"></div> <div class="mnc-demo-line" style="top: 60px;"></div> <div class="mnc-demo-line" style="top: 80px;"></div> <div class="mnc-demo-line-vertical" style="left: 20px;"></div> <div class="mnc-demo-line-vertical" style="left: 40px;"></div> <div class="mnc-demo-line-vertical" style="left: 60px;"></div> <div class="mnc-demo-line-vertical" style="left: 80px;"></div> </div> <div class="mnc-demo-dot"></div> </div> </div> <p>You may have noticed that we’ve been using <code class="highlighter-rouge">@keyframes</code> blocks in all of the examples so far, but that’s purely because we happened to need several keyframes to move the dot back and forth. Layered animation works just as well with the <code class="highlighter-rouge">transition</code> property, if you only need to animate from point A to point B.</p> <p>If you have an absolutely positioned element you can achieve a curved path by animating the <code class="highlighter-rouge">left</code> and <code class="highlighter-rouge">bottom</code> properties, of a single element, and avoid the need of a container element. There’s a good reason to avoid doing so, however: it performs significantly worse, and will trigger a redraw for every frame of the animation. Using layered animation with a pseudo-element and animating the hardware-accelerated <code class="highlighter-rouge">translate</code> property produces a great-looking animation while making sure it also performs well.</p> Thu, 07 Jan 2016 11:52:00 +0000 http://tobiasahlin.com/blog/curved-path-animations-in-css/ http://tobiasahlin.com/blog/curved-path-animations-in-css/ How to animate "box-shadow" with silky smooth performance <div class="blog-banner banner-purple"> <div class="blog-banner-content" style="height: 90px;"> <div class="box-shadow-demo"></div> </div> <style> .box-shadow-demo { display: inline-block; background-color: #fff; width: 90px; height: 90px; border-radius: 5px; box-shadow: 0 1px 2px rgba(0,0,0,0.2); position: relative; border-radius: 5px; -webkit-animation: scaleAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); animation: scaleAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); } .box-shadow-demo::after { content: ""; border-radius: 5px; position: absolute; z-index: -1; top: 0; left: 0; width: 100%; height: 100%; box-shadow: 0 5px 15px rgba(0,0,0,0.5); opacity: 0; -webkit-animation: fadeAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); animation: fadeAnimation 3.5s infinite cubic-bezier(0.165, 0.84, 0.44, 1); } @-webkit-keyframes fadeAnimation { 0%, 80%, 100% { opacity: 0; } 30%, 50% { opacity: 1; } } @-webkit-keyframes scaleAnimation { 0%, 80%, 100% { -webkit-transform: scale(1, 1); transform: scale(1, 1); } 30%, 50% { -webkit-transform: scale(1.3, 1.3); transform: scale(1.3, 1.3); } } @keyframes fadeAnimation { 0%, 80%, 100% { opacity: 0; } 30%, 50% { opacity: 1; } } @keyframes scaleAnimation { 0%, 80%, 100% { -webkit-transform: scale(1, 1); transform: scale(1, 1); } 30%, 50% { -webkit-transform: scale(1.3, 1.3); transform: scale(1.3, 1.3); } } </style> </div> <p>How do you animate the <code class="highlighter-rouge">box-shadow</code> property in CSS without causing re-paints on every frame, and heavily impacting the performance of your page? Short answer: you don’t. Animating a change of <code class="highlighter-rouge">box-shadow</code> <em>will</em> hurt performance.</p> <p>There’s an easy way of mimicking the same effect, however, with minimal re-paints, that should let your animations run at a solid 60 FPS: animate the <code class="highlighter-rouge">opacity</code> of a pseudo-element.</p> <!--more--> <h2 id="demo">Demo</h2> <p><a href="/demo/animate-box-shadow/"><img src="/static/animate-box-shadow/demo.gif" alt="Recording of box-shadow demo in action" /></a></p> <p><a href="/demo/animate-box-shadow/">Have a look at the demo</a> and compare the two different techniques we’ll be exploring. If the two examples look the same to you, that’s the point. The only difference is how we apply and animate the shadow. On the left we’re animating <code class="highlighter-rouge">box-shadow</code> on <code class="highlighter-rouge">hover</code>, and on the right we’re adding a pseudo-element with <code class="highlighter-rouge">:after</code>, applying the shadow to that, and animating the <code class="highlighter-rouge">opacity</code> of that element.</p> <p>If you bring up your developer tools and hover one of these items, you should see something similar to this (green bars are paints; less is better):</p> <p><a href="/demo/animate-box-shadow/"><img src="/static/animate-box-shadow/animation-performance.png" alt="Animation performance when hovering the different boxes" /></a></p> <p>There are clearly more re-paints when hovering the cards on the left side (animating <code class="highlighter-rouge">box-shadow</code>), compared to hovering the cards on the right side (which animate the <code class="highlighter-rouge">opacity</code> of their pseudo-element).</p> <p>Why are we seeing this effect? There are <a href="http://csstriggers.com">very few CSS properties</a> that can be animated without constantly triggering repaints for every frame, namely <code class="highlighter-rouge">opacity</code> and <code class="highlighter-rouge">transform</code>. We minimize the amount of repaints (and work that your browser has to do) by sticking to only changing these two properties during the animation.</p> <p>This is the <strong>critical difference</strong> between the two techniques, stripping out all of the other layout styles:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="c">/* The slow way */</span> <span class="nc">.make-it-slow</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.15</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">box-shadow</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* Transition to a bigger shadow on hover */</span> <span class="nc">.make-it-slow</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.3</span><span class="p">);</span> <span class="p">}</span> <span class="c">/* The fast way */</span> <span class="nc">.make-it-fast</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.15</span><span class="p">);</span> <span class="p">}</span> <span class="c">/* Pre-render the bigger shadow, but hide it */</span> <span class="nc">.make-it-fast</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.3</span><span class="p">);</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">opacity</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* Transition to showing the bigger shadow on hover */</span> <span class="nc">.make-it-fast</span><span class="nd">:hover::after</span> <span class="p">{</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>In the example that performs better we have two layers: one for the box, and one for the shadow, and only animate the <code class="highlighter-rouge">opacity</code> property of the shadow layer.</p> <h2 id="breaking-it-down">Breaking it down</h2> <p>With the fundamentals in place, let’s look at how to create <a href="/demo/animate-box-shadow/">the 3D card effect showcased in the demo</a>. The first step is to move the shadow to a pseudo-element, like we did above. Let’s also add all of the layout code to create the card:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="c">/* All HTML you need is &lt;div class="box"&gt;&lt;/div&gt; */</span> <span class="c">/* Create a simple white box, and add the shadow for the initial state */</span> <span class="nc">.box</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.15</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">;</span> <span class="p">}</span> <span class="c">/* Create the hidden pseudo-element */</span> <span class="c">/* include the shadow for the end state */</span> <span class="nc">.box</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">content</span><span class="p">:</span> <span class="s2">''</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">z-index</span><span class="p">:</span> <span class="m">-1</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0</span><span class="p">,</span><span class="m">0.3</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">opacity</span> <span class="m">0.3s</span> <span class="n">ease-in-out</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>Note that we’re adding a <code class="highlighter-rouge">transition</code> to both the <code class="highlighter-rouge">.box</code>, and <code class="highlighter-rouge">.box::after</code>, since we’re going to animate both of these elements: <code class="highlighter-rouge">transform</code> for <code class="highlighter-rouge">.box</code>, and <code class="highlighter-rouge">opacity</code> for <code class="highlighter-rouge">.box::after</code>.</p> <p>These styles give us a white box with a subtle <code class="highlighter-rouge">box-shadow</code>. The stronger shadow from <code class="highlighter-rouge">.box::after</code> is completely hidden at this point, and you can’t interact with the box:</p> <div class="post-demo-content"> <div class="box"></div> <style> .box { position: relative; display: inline-block; width: 100px; height: 100px; border-radius: 5px; background-color: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.1); transition: all 0.3s ease-in-out; } </style> </div> <p>To create the same effect as in the <a href="/demo/animate-box-shadow/">demo</a>, now all we need to do is to scale up the <code class="highlighter-rouge">.box</code> on hover, and fade in the pseudo-element and its shadow:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="c">/* Scale up the box */</span> <span class="nc">.box</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">scale</span><span class="p">(</span><span class="m">1.2</span><span class="p">,</span> <span class="m">1.2</span><span class="p">);</span> <span class="p">}</span> <span class="c">/* Fade in the pseudo-element with the bigger shadow */</span> <span class="nc">.box</span><span class="nd">:hover::after</span> <span class="p">{</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>That’s it! Hover the box to preview the effect:</p> <div class="post-demo-content"> <div class="box2"></div> <style> .box2 { position: relative; display: inline-block; width: 100px; height: 100px; border-radius: 5px; background-color: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.1); -webkit-transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1); transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1); } /* Create the hidden pseudo-element */ /* include the shadow for the end state */ .box2::after { content: ''; position: absolute; z-index: -1; width: 100%; height: 100%; border-radius: 5px; opacity: 0; box-shadow: 0 5px 15px rgba(0,0,0,0.3); -webkit-transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1); transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1) } .box2:hover { -webkit-transform: scale(1.25, 1.25); transform: scale(1.25, 1.25); } .box2:hover::after { opacity: 1; } </style> </div> <p>To summarize, here’s all the CSS, with all vendor prefixes, and some custom easing for additional ✨👌:</p> <div class="language-css highlighter-rouge"><pre class="highlight"><code><span class="nc">.box</span> <span class="p">{</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">relative</span><span class="p">;</span> <span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">1px</span> <span class="m">2px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0.1</span><span class="p">);</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">-webkit-transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.box</span><span class="nd">::after</span> <span class="p">{</span> <span class="nl">content</span><span class="p">:</span> <span class="s1">""</span><span class="p">;</span> <span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span> <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span> <span class="nl">z-index</span><span class="p">:</span> <span class="m">-1</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">left</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">15px</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0.3</span><span class="p">);</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span> <span class="nl">-webkit-transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0.6s</span> <span class="n">cubic-bezier</span><span class="p">(</span><span class="m">0.165</span><span class="p">,</span> <span class="m">0.84</span><span class="p">,</span> <span class="m">0.44</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.box</span><span class="nd">:hover</span> <span class="p">{</span> <span class="nl">-webkit-transform</span><span class="p">:</span> <span class="n">scale</span><span class="p">(</span><span class="m">1.25</span><span class="p">,</span> <span class="m">1.25</span><span class="p">);</span> <span class="nl">transform</span><span class="p">:</span> <span class="n">scale</span><span class="p">(</span><span class="m">1.25</span><span class="p">,</span> <span class="m">1.25</span><span class="p">);</span> <span class="p">}</span> <span class="nc">.box</span><span class="nd">:hover::after</span> <span class="p">{</span> <span class="nl">opacity</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span> <span class="p">}</span> </code></pre> </div> <p>That’s certainly a lot of CSS to achieve the same effect as simply animating <code class="highlighter-rouge">box-shadow</code>, just with improved performance. Why bother?</p> <p>Even if your desktop likely handles animating <code class="highlighter-rouge">box-shadow</code> without any issues, your phone may not, and even your desktop may start to stutter when animating a more complex layout.</p> <p>Keep transitions and animations to only <code class="highlighter-rouge">transform</code> and <code class="highlighter-rouge">opacity</code>, and you’re certain to achieve the best possible performance, and with that, the best possible user experience.</p> Wed, 18 Nov 2015 14:52:00 +0000 http://tobiasahlin.com/blog/how-to-animate-box-shadow/ http://tobiasahlin.com/blog/how-to-animate-box-shadow/ Google Web Font Inspiration with TypeSource <div class="blog-banner banner-blue"> <div class="blog-banner-content" style="height: 90px;"> <img src="/static/typesource.svg" alt="TypeSource" width="90" /> </div> </div> <p>If you’ve been looking through Google’s directory of <a href="https://www.google.com/fonts">Web Fonts</a> in search for fonts to use in your next project, you might have felt your inspiration quickly drain. It can be difficult to imagine how a font would look in use when you’re left with a long list of black text on white.</p> <p>I built <a href="/typesource/">Typesource</a> to make it easier to find and match Google Web Fonts. I’ve carefully crafted each example, exploring different styles, combinations, and color schemes in every composition. In addition to the feed of inspiration, the HTML/CSS is available for all examples, so you can copy and paste the code into your project and quickly get started with your web type compositions.</p> <!--more--> <p class="center-content"> <a href="/typesource/" class="outlined-button">View TypeSource</a> </p> <p>I’ve posted 10 examples so far, and more are incoming. Here are some highlights:</p> <p><a href="/typesource/11-sense-of-style/"> <img src="/static/typesource/typesource01.png" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/10-wait-until-monday/"> <img src="/static/typesource/typesource02.jpg" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/09-history-of-swedish-meatballs/"> <img src="/static/typesource/typesource03.png" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/04-how-climate-change-will-affect-you/"> <img src="/static/typesource/typesource04.jpg" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/05-perfection/"> <img src="/static/typesource/typesource05.png" alt="Google Web Font example" /> </a></p> <p><a href="/typesource/06-a-search/"> <img src="/static/typesource/typesource06.jpg" alt="Google Web Font example" /> </a></p> <p class="center-content"> <a href="/typesource/" class="outlined-button">See all TypeSource examples</a> </p> Mon, 16 Nov 2015 13:10:00 +0000 http://tobiasahlin.com/blog/google-web-font-inspiration/ http://tobiasahlin.com/blog/google-web-font-inspiration/ Copy Lorem Ipsum from your Menu Bar with Loremify <div class="blog-banner banner-gray"> <div class="blog-banner-content" style="height: 120px;"> <img src="/static/loremify.png" alt="Loremify" width="120" /> </div> </div> <p><a href="http://kallepersson.se">Kalle Persson</a> and I just built and released a small utility app for OS X. <a href="http://loremify.com/">Loremify</a> is a free mac app to quickly copy Lorem Ipsum to your clipboard. It lets you wrap the dummy text in html or markdown, specify the amount of text, and copy it to your clipboard—all in one click. It sits in your OS X menu bar, and it’s <a href="https://itunes.apple.com/app/loremify/id1028877632">available on the App Store</a>.</p> <!--more--> <p><img src="/static/loremify.gif" alt="Demo of Loremify" /></p> <p>Switch to Markdown (or plain text) with the menu in the top left:</p> <p><img src="/static/loremify-markdown.gif" alt="Demo of switching output language in Loremify" /></p> <p>Loremify is still in its infancy, but we’d love to hear from you if you find it useful (or not useful). Reach out on twitter at <a href="http://twitter.com/tobiasahlin">@tobiasahlin</a> and <a href="https://twitter.com/kallepersson">@kallepersson</a>, and let us know what you think.</p> <p><a href="https://itunes.apple.com/app/loremify/id1028877632"><img src="/static/appstore.svg" width="200" class="aligncenter" /></a></p> Sun, 15 Nov 2015 13:10:00 +0000 http://tobiasahlin.com/blog/lorem-ipsum-in-your-menu-bar-with-loremify/ http://tobiasahlin.com/blog/lorem-ipsum-in-your-menu-bar-with-loremify/ Three new SpinKit spinners <div class="blog-banner banner-green"> <div class="sk-spinner-news blog-banner-content"> <div class="sk-news-rect sk-news-rect1"></div> <div class="sk-news-rect sk-news-rect2"></div> <div class="sk-news-rect sk-news-rect3"></div> <div class="sk-news-rect sk-news-rect4"></div> <div class="sk-news-rect sk-news-rect5"></div> </div> <style type="text/css"> .sk-spinner-news { width: 100px; height: 70px; text-align: center; font-size: 15px; } .sk-news-rect { background-color: #fff; height: 100%; width: 8px; display: inline-block; -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; animation: sk-stretchdelay 1.2s infinite ease-in-out; } .sk-news-rect2 { -webkit-animation-delay: -1.1s; animation-delay: -1.1s; } .sk-news-rect3 { -webkit-animation-delay: -1.0s; animation-delay: -1.0s; } .sk-news-rect4 { -webkit-animation-delay: -0.9s; animation-delay: -0.9s; } .sk-news-rect5 { -webkit-animation-delay: -0.8s; animation-delay: -0.8s; } @-webkit-keyframes sk-stretchdelay { 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 20% { -webkit-transform: scaleY(1.0) } } @keyframes sk-stretchdelay { 0%, 40%, 100% { transform: scaleY(0.4); -webkit-transform: scaleY(0.4); } 20% { transform: scaleY(1.0); -webkit-transform: scaleY(1.0); } } </style> </div> <p><a href="http://tobiasahlin.com/spinkit/">SpinKit</a> just got three new additions: a folding cube spinner, a classic circle spinner, and a scaling grid spinner. Just like the other spinners, they are animated using only the <code class="highlighter-rouge">transform</code> and the <code class="highlighter-rouge">opacity</code> properties, making them perform well, and easy to customize: just change the background color to match your site.</p> <!--more--> <p class="center-content"> <a href="/spinkit/" class="outlined-button">View SpinKit</a> </p> <p>If you want to use SpinKit in your project and stay up to date with new changes and additions, the project is now also available on <a href="https://www.npmjs.com/package/spinkit">npm</a> and <a href="http://bower.io">bower</a>. Just run:</p> <div class="highlighter-rouge"><pre class="highlight"><code>npm install spinkit </code></pre> </div> <p>…or:</p> <div class="highlighter-rouge"><pre class="highlight"><code>bower install spinkit </code></pre> </div> <p>With the recent <a href="https://github.com/tobiasahlin/SpinKit/releases">release of SpinKit 1.2</a>, we refactored a lot of the SCSS. The spinners are now easier to customize, and we fixed all known bugs. I highly recommend updating.</p> <p>Do you have a spinner in mind that you’re missing from SpinKit? <a href="http://twitter.com/tobiasahlin">Let me know</a>, and it may just join the SpinKit family in the next update.</p> <p>Happy spinning!</p> Thu, 06 Aug 2015 13:10:00 +0000 http://tobiasahlin.com/blog/spinkit-news/ http://tobiasahlin.com/blog/spinkit-news/ Wake <p>Bakken &amp; Bæck, <a href="https://medium.com/bakken-bæck/introducing-wake-2f3f0dcc1b20">announcing Wake</a>:</p> <blockquote> <p>We couldn’t find anything that would allow us to quickly share visual work as a team, and we needed a better tool to keep us all in sync. Our design process was fragmented across different tools and channels. We were storing files in Dropbox, and giving feedback on email, Skype or Slack.</p> </blockquote> <blockquote> <p>These tools are great for text-based communication, but they were not designed for the type of visual work we do. We needed a solution to keep everyone in the loop built specifically for designers.</p> </blockquote> <blockquote> <p>Together with Chris we started building a prototype for a tool that would improve our own design workflow.</p> </blockquote> <p><a href="http://wake.io">Wake</a> is a new tool by Bakken &amp; Bæck for collaborating around design, that let’s you “explain your thinking and get feedback”. It looks gorgeous.</p> <iframe width="960" height="540" style="height: 540px;" src="https://www.youtube.com/embed/GIalL5fkhPM?rel=0" frameborder="0" allowfullscreen="" class="size-960"></iframe> Wed, 25 Feb 2015 12:28:00 +0000 http://tobiasahlin.com/blog/wake/ http://tobiasahlin.com/blog/wake/ Coding is not the new literacy <blockquote> <p>Alan Kay did a <a href="https://www.youtube.com/watch?v=oKg1hTOQXoY">talk</a> at OOPSLA in 1997 titled “The computer revolution hasn’t happened yet,” in which he argued that we haven’t realized the potential that computers can provide for us. Eighteen years later, I still agree with him - it hasn’t happened yet. And teaching people how to loop over a list won’t make it happen either. To realize the potential of computers, we have to focus on the fundamental skills that allow us to harness external computation. We have to create a new generation of tools that allow us to express our models without switching professions and a new generation of modelers who wield them.</p> </blockquote> <p>— Chris Granger in <a href="http://www.chris-granger.com/2015/01/26/coding-is-not-the-new-literacy/">Coding is not the new literacy</a></p> Wed, 25 Feb 2015 12:15:00 +0000 http://tobiasahlin.com/blog/coding-is-not-the-new-literacy/ http://tobiasahlin.com/blog/coding-is-not-the-new-literacy/