In this tutorial, I’ll walk you through plotting the average color of a video by frame using D3 and Vue. I’ll also show you how to use a web-worker to do the color calculation.

Here’s an overview of how this will work: we’ll create a canvas element that will be hidden. When the video is playing, we’ll write a function using requestAnimationFrame that will draw the current video frame into the canvas elements context. We’ll then get the image data out of the canvas and pass it into a Web Worker which will then compute the average RGB value of the canvas. The Web Worker will then post a message back to a D3 component that will plot the red, blue, and green values as well as the combined average color. Here’s what it will look like:


Here’s the repo for the entire tutorial. If you want to clone it, just make sure to run npm install before starting the dev server.

The Setup

As with most of these tutorials, we’ll be using webpack to build and run single file components. We’ll also need it to load our web-worker. Make sure you have the vue-cli install (run npm install -g vue-cli if you don’t). Then, from your projects directory run:

vue init webpack video-color-plot

You won’t need vue-router for this one.

In order to get the worker to work, we need to install worker-loader and tell our base webpack config to use that loader for any file that ends in ‘worker.js`.

npm install --save worker-loader

Then, in the build/webpack.base.conf.js add this to the array of rules for webpack modules:


test: /\worker\.js$/,

loader: 'worker-loader',

include: [resolve('src'), resolve('test')]


This will allow you to import workers directly into a Vue component. Now we can start!

The App Component


<div :class="$style.container">

<video :class="$" src="./assets/test.mp4" autoplay loop controls ref="video" id="video"/>

<canvas :class="$style.canvas" ref="canvas" hidden></canvas>

<chart v-if="videoLoaded" :frame="frame" :color="averageColor"/>




import Chart from './chart.vue'

import MyWorker from './worker.js'

const FPS = 25

const INTERVAL = 1000 / (FPS / 3)

let then = new Date().getTime()

export default {

name: 'app',

data () {

return {

videoLoaded: false,

isPlaying: false,

ctx: null,

averageColor: {r: 0, g: 0, b: 0},

frame: 0



mounted () {

const canvasNode = this.$refs.canvas

const videoNode = this.$

this.ctx = canvasNode.getContext('2d')

this.worker = new MyWorker()

// when the worker has completed its calculation, is sends a message back to the main thread.

// we update this components state with the response which triggers an update in the histogram

// component, causing the average color to be plotted.

this.worker.onmessage = e => {

this.averageColor =

this.frame = Math.floor(videoNode.currentTime * FPS)


// set canvas to be same height as video when the video is loaded

videoNode.addEventListener('canplay', e => {

canvasNode.height = videoNode.offsetHeight

canvasNode.width = videoNode.offsetWidth

this.videoLoaded = true


videoNode.addEventListener('play', e => {

this.isPlaying = true


videoNode.addEventListener('pause', e => {

this.isPlaying = false



methods: {

// draws the current video frame into the canvas element using the web worker

drawCanvas (time) {


const now = new Date().getTime()

let delta = now - then

if (delta > INTERVAL) {

then = now - (delta % INTERVAL)

const vid = this.$

let x1 = 0

let y1 = 0

x1 = vid.offsetWidth

y1 = vid.offsetHeight

//draws image into canvas using video source

this.ctx.drawImage(vid, 0, 0, x1, y1)

//gets the image data from the canvas

const data = this.ctx.getImageData(0, 0, x1, y1)





watch: {

isPlaying (isPlaying) {

// as long as the video is playing, continue to draw the canvas




components: {





In the template you can see we have a source video and an empty canvas element. The chart component will be displayed only after the video has loaded and started playing. It accepts two props — frame and average color.

When the main component is mounted, we set the canvas to be the same size as the video. We also set up an onmessage function that will update the components state with the result of the worker’s color calculation (explained below).

The bulk of the work is done in the drawCanvas method. Here, we’re using requestAnimationFrame to re-call the drawCanvas method as fast as the browser will let us.

We’re actually limiting the execution of the drawing of the canvas to be 1/3 of the frame rate of the video. I played around with this value to get the best results. You can read more about the details of this technique here, thanks to Addy Osmani.

Lets take a look at the web worker.

The Worker

The main job of the worker is to accept data, which is imageData obtained from the canvas, and loop through it to get the average red, green, and blue values. I did not invent this technique (you can read more about it from this excellent stack overflow post).

onmessage = function (e) {

const data =

const length =

const BLOCKSIZE = 5

const STEPS = 4

let i = -4

let count = 0

let rgb = {r: 0, g: 0, b: 0}

while ((i += BLOCKSIZE * STEPS) < length) {


rgb.r +=[i]

rgb.g +=[i + 1]

rgb.b +=[i + 2]


// ~~ used to floor values

rgb.r = ~~(rgb.r / count)

rgb.g = ~~(rgb.g / count)

rgb.b = ~~(rgb.b / count)



Alternatively, you could use something like ColorThief to do this. Essentially, the imageData from a canvas element is returned as a Uint8ClampedArray (you can read more about it here), and we loop through it until our iterator i is greater than the length of the data. When the calculation is made, it posts a message back to the main component, which then updates that component’s state. When the state updates, the Chart component will re-render, plotting the new color at the current frame. Lets look at the Chart component.

The Chart



<svg :class="$style.svg" id="svg" ref="svg">





import {select} from 'd3'

import {scaleLinear} from 'd3-scale'

import {rgb} from 'd3-color'

import {axisBottom, axisLeft} from 'd3-axis'

import {map} from 'lodash'


const FPS = 25

let xAxis = null

let yAxis = null

export default {

name: 'chart',

props: ['frame', 'color'],

mounted: function () {




watch: {

frame () {




methods: {

// moves the needle showing the current frame

getCurrentTime: function () {

const vid = document.getElementById('video')

this.currentTime = vid.currentTime

const dX = xAxis(this.currentTime * FPS)


.attr('d', () => {

return `M${dX},${HISTOGRAM_HEIGHT} ${dX}, 0`




updateChart: function () {

let frame = this.frame

let color = this.color

const svg = select('#svg')

// map through the color keys (red, green, and blue) and

// append a circle for each one at the current frame

map(color, (c, k) => {



.attr('r', 1)

.attr('cx', xAxis(frame))

.attr('cy', yAxis(c))

.attr('fill', g => {

const n = rgb()

n[k] = c

return n



// for each update, append a rectangle showing the current average color



.attr('x', xAxis(frame))

.attr('y', yAxis(310))

.attr('width', 3)

.attr('height', 20)

.attr('fill', rgb(color.r, color.g, color.b))


setupChart: function () {

const videoNode = document.getElementById('video')

const svgNode = document.getElementById('svg')

svgNode.setAttribute('width', videoNode.offsetWidth)

svgNode.setAttribute('height', HISTOGRAM_HEIGHT)

// xDomain is the length of the video in frames

const xDomain = [0, videoNode.duration * FPS]

// yDomain is the range of possible values for each color

const yDomain = [0, 256]

const svg = select('#svg')

const width = svg.attr('width')

const height = svg.attr('height')

const g = svg.append('g')

//both axes are D3 linear scales

xAxis = scaleLinear().range([0, width])

yAxis = scaleLinear().range([height, 0])

// apply the domain to the axes



// draws the bottom axis


.attr('class', 'axis axis-x')

.attr('transform', `translate(0, ${height})`)


// draws the left axis


.attr('class', 'axis axis--y')


// draws the needle line which shows the current frame


.attr('class', 'needle-line')

.style('stroke', 'black')

.style('stroke-width', '3px')

.style('opacity', '1')





This is where all the D3 magic happens. When the component mounts, we setup our chart. The chart has an xAxis (representing the length of the video in frames) and a yAxis (ranging from 0–256, which represents the range of possible values for red, blue, and green). In D3, we draw the axis using the length in pixels of the svg element, but the domain of the axis is not in pixels, it in frames (xAxis) or rgb values (yAxis). D3 makes it easy to translate abstract data (the frame number coupled with the RGB data) into a visual representation (a dot on the plotted chart) with scales. We create a scale (lines 94,95) and apply a domain to each one (lines 98, 99). A domain is just a range of possible values.

After the chart is setup, we watch for the frame prop value to change and then we call updateChart . The averageColor prop is an object with r , g , and b keys. We use lodash’s map to map through the object and for each key, get the value (number between 0 and 256) and apply it to the yAxis. D3 takes that value and translates it into a pixel to know where to actually draw the circle.


That’s it, really. Working with D3 inside of Vue is really nice, and there isn’t any kind of magic setup you need to achieve this. Try it out with different videos and see what kind of results you get!