Needed a simple but pretty line chart for a presentation and ended up creating this.
Forked from Hakim El Hattab's Pen Animated Line Chart.
A Pen by Anonasaurus Rex on CodePen.
Needed a simple but pretty line chart for a presentation and ended up creating this.
Forked from Hakim El Hattab's Pen Animated Line Chart.
A Pen by Anonasaurus Rex on CodePen.
<div> | |
<button onclick="append();">Append point</button> | |
<button onclick="restart();">Replay</button> | |
<button onclick="reset();">Reset</button> | |
</div> |
// don't try to learn anything from the code, it's a | |
// series of hacks. this one's all about the visuals. | |
// - @hakimel | |
var LineChart = function( options ) { | |
var data = options.data; | |
var canvas = document.body.appendChild( document.createElement( 'canvas' ) ); | |
var context = canvas.getContext( '2d' ); | |
var rendering = false, | |
paddingX = 40, | |
paddingY = 40, | |
width = options.width || window.innerWidth, | |
height = options.height || window.innerHeight, | |
progress = 0; | |
canvas.width = width; | |
canvas.height = height; | |
var maxValue, | |
minValue; | |
var y1 = paddingY + ( 0.05 * ( height - ( paddingY * 2 ) ) ), | |
y2 = paddingY + ( 0.50 * ( height - ( paddingY * 2 ) ) ), | |
y3 = paddingY + ( 0.95 * ( height - ( paddingY * 2 ) ) ); | |
format(); | |
render(); | |
function format( force ) { | |
maxValue = 0; | |
minValue = Number.MAX_VALUE; | |
data.forEach( function( point, i ) { | |
maxValue = Math.max( maxValue, point.value ); | |
minValue = Math.min( minValue, point.value ); | |
} ); | |
data.forEach( function( point, i ) { | |
point.targetX = paddingX + ( i / ( data.length - 1 ) ) * ( width - ( paddingX * 2 ) ); | |
point.targetY = paddingY + ( ( point.value - minValue ) / ( maxValue - minValue ) * ( height - ( paddingY * 2 ) ) ); | |
point.targetY = height - point.targetY; | |
if( force || ( !point.x && !point.y ) ) { | |
point.x = point.targetX + 30; | |
point.y = point.targetY; | |
point.speed = 0.04 + ( 1 - ( i / data.length ) ) * 0.05; | |
} | |
} ); | |
} | |
function render() { | |
if( !rendering ) { | |
requestAnimationFrame( render ); | |
return; | |
} | |
context.font = '10px sans-serif'; | |
context.clearRect( 0, 0, width, height ); | |
context.fillStyle = '#222'; | |
context.fillRect( paddingX, y1, width - ( paddingX * 2 ), 1 ); | |
context.fillRect( paddingX, y2, width - ( paddingX * 2 ), 1 ); | |
context.fillRect( paddingX, y3, width - ( paddingX * 2 ), 1 ); | |
if( options.yAxisLabel ) { | |
context.save(); | |
context.globalAlpha = progress; | |
context.translate( paddingX - 15, height - paddingY - 10 ); | |
context.rotate( -Math.PI / 2 ); | |
context.fillStyle = '#fff'; | |
context.fillText( options.yAxisLabel, 0, 0 ); | |
context.restore(); | |
} | |
var progressDots = Math.floor( progress * data.length ); | |
var progressFragment = ( progress * data.length ) - Math.floor( progress * data.length ); | |
data.forEach( function( point, i ) { | |
if( i <= progressDots ) { | |
point.x += ( point.targetX - point.x ) * point.speed; | |
point.y += ( point.targetY - point.y ) * point.speed; | |
context.save(); | |
var wordWidth = context.measureText( point.label ).width; | |
context.globalAlpha = i === progressDots ? progressFragment : 1; | |
context.fillStyle = point.future ? '#aaa' : '#fff'; | |
context.fillText( point.label, point.x - ( wordWidth / 2 ), height - 18 ); | |
if( i < progressDots && !point.future ) { | |
context.beginPath(); | |
context.arc( point.x, point.y, 4, 0, Math.PI * 2 ); | |
context.fillStyle = '#1baee1'; | |
context.fill(); | |
} | |
context.restore(); | |
} | |
} ); | |
context.save(); | |
context.beginPath(); | |
context.strokeStyle = '#1baee1'; | |
context.lineWidth = 2; | |
var futureStarted = false; | |
data.forEach( function( point, i ) { | |
if( i <= progressDots ) { | |
var px = i === 0 ? data[0].x : data[i-1].x, | |
py = i === 0 ? data[0].y : data[i-1].y; | |
var x = point.x, | |
y = point.y; | |
if( i === progressDots ) { | |
x = px + ( ( x - px ) * progressFragment ); | |
y = py + ( ( y - py ) * progressFragment ); | |
} | |
if( point.future && !futureStarted ) { | |
futureStarted = true; | |
context.stroke(); | |
context.beginPath(); | |
context.moveTo( px, py ); | |
context.strokeStyle = '#aaa'; | |
if( typeof context.setLineDash === 'function' ) { | |
context.setLineDash( [2,3] ); | |
} | |
} | |
if( i === 0 ) { | |
context.moveTo( x, y ); | |
} | |
else { | |
context.lineTo( x, y ); | |
} | |
} | |
} ); | |
context.stroke(); | |
context.restore(); | |
progress += ( 1 - progress ) * 0.02; | |
requestAnimationFrame( render ); | |
} | |
this.start = function() { | |
rendering = true; | |
} | |
this.stop = function() { | |
rendering = false; | |
progress = 0; | |
format( true ); | |
} | |
this.restart = function() { | |
this.stop(); | |
this.start(); | |
} | |
this.append = function( points ) { | |
progress -= points.length / data.length; | |
data = data.concat( points ); | |
format(); | |
} | |
this.populate = function( points ) { | |
progress = 0; | |
data = points; | |
format(); | |
} | |
}; | |
var chart = new LineChart({ data: [] }); | |
reset(); | |
chart.start(); | |
function append() { | |
chart.append([ | |
{ label: 'Rnd', value: 1300 + ( Math.random() * 1500 ), future: true } | |
]); | |
} | |
function restart() { | |
chart.restart(); | |
} | |
function reset() { | |
chart.populate([ | |
{ label: 'One', value: 0 }, | |
{ label: 'Two', value: 100 }, | |
{ label: 'Three', value: 200 }, | |
{ label: 'Four', value: 840 }, | |
{ label: 'Five', value: 620 }, | |
{ label: 'Six', value: 500 }, | |
{ label: 'Seven', value: 600 }, | |
{ label: 'Eight', value: 1100 }, | |
{ label: 'Nine', value: 800 }, | |
{ label: 'Ten', value: 900 }, | |
{ label: 'Eleven', value: 1200, future: true }, | |
{ label: 'Twelve', value: 1400, future: true } | |
]); | |
} | |
body { | |
background: #000; | |
text-align: center; | |
margin: 0; | |
overflow: hidden; | |
} | |
div { | |
position: absolute; | |
left: 5px; | |
top: 5px; | |
} |