Skip to content

Instantly share code, notes, and snippets.

@pingpoli
Created May 29, 2021 10:25
Show Gist options
  • Save pingpoli/ea558dd154fa9668b8e924c2a4d32faf to your computer and use it in GitHub Desktop.
Save pingpoli/ea558dd154fa9668b8e924c2a4d32faf to your computer and use it in GitHub Desktop.
/*
A single-file JavaScript class to draw candlestick charts.
Use at your own risk.
https://twitter.com/pingpoli
https://pingpoli.de
*/
function pingpoliCandlestick( timestamp , open , close , high , low )
{
this.timestamp = parseInt(timestamp);
this.open = parseFloat(open);
this.close = parseFloat(close);
this.high = parseFloat(high);
this.low = parseFloat(low);
}
function pingpoliCandlestickChart( canvasElementID )
{
this.canvas = document.getElementById( canvasElementID );
this.width = parseInt( this.canvas.width );
this.height = parseInt( this.canvas.height );
this.context = this.canvas.getContext( "2d" );
this.canvas.addEventListener( "mousemove" , ( e ) => {
this.mouseMove( e );
} );
this.canvas.addEventListener( "mouseout" , ( e ) => {
this.mouseOut( e );
} );
this.canvas.style.backgroundColor = "#252525";
this.context.font = '12px sans-serif';
this.gridColor = "#444444";
this.gridTextColor = "#aaaaaa";
this.mouseHoverBackgroundColor = "#eeeeee";
this.mouseHoverTextColor = "#000000";
this.greenColor = "#00cc00";
this.redColor = "#cc0000";
this.greenHoverColor = "#00ff00";
this.redHoverColor = "#ff0000";
this.context.lineWidth = 1;
this.candleWidth = 5;
this.marginLeft = 10;
this.marginRight = 100;
this.marginTop = 10;
this.marginBottom = 30;
this.yStart = 0;
this.yEnd = 0;
this.yRange = 0;
this.yPixelRange = this.height-this.marginTop-this.marginBottom;
this.xStart = 0;
this.xEnd = 0;
this.xRange = 0;
this.xPixelRange = this.width-this.marginLeft-this.marginRight;
// these are only approximations, the grid will be divided in a way so the numbers are nice
this.xGridCells = 16;
this.yGridCells = 16;
this.b_drawMouseOverlay = false;
this.mousePosition = { x: 0 , y: 0 };
this.xMouseHover = 0;
this.yMouseHover = 0;
this.hoveredCandlestickID = 0;
this.candlesticks = [];
}
pingpoliCandlestickChart.prototype.addCandlestick = function( candlestick )
{
this.candlesticks.push( candlestick );
}
pingpoliCandlestickChart.prototype.mouseMove = function( e )
{
var getMousePos = ( e ) =>
{
var rect = this.canvas.getBoundingClientRect();
return { x: e.clientX-rect.left , y: e.clientY-rect.top };
}
this.mousePosition = getMousePos( e );
this.mousePosition.x += this.candleWidth/2;
this.b_drawMouseOverlay = true;
if ( this.mousePosition.x < this.marginLeft ) this.b_drawMouseOverlay = false;
if ( this.mousePosition.x > this.width-this.marginRight+this.candleWidth ) this.b_drawMouseOverlay = false;
if ( this.mousePosition.y > this.height-this.marginBottom ) this.b_drawMouseOverlay = false;
if ( this.b_drawMouseOverlay )
{
this.yMouseHover = this.yToValueCoords( this.mousePosition.y );
this.xMouseHover = this.xToValueCoords( this.mousePosition.x );
// snap to candlesticks
var candlestickDelta = this.candlesticks[1].timestamp-this.candlesticks[0].timestamp;
this.hoveredCandlestickID = Math.floor((this.xMouseHover-this.candlesticks[0].timestamp)/candlestickDelta);
this.xMouseHover = Math.floor(this.xMouseHover/candlestickDelta)*candlestickDelta;
this.mousePosition.x = this.xToPixelCoords( this.xMouseHover );
this.draw();
}
else this.draw();
}
pingpoliCandlestickChart.prototype.mouseOut = function( e )
{
this.b_drawMouseOverlay = false;
this.draw();
}
pingpoliCandlestickChart.prototype.draw = function()
{
// clear background
this.context.clearRect( 0 , 0 , this.width , this.height );
this.calculateYRange();
this.calculateXRange();
this.drawGrid();
this.candleWidth = this.xPixelRange/this.candlesticks.length;
this.candleWidth--;
if ( this.candleWidth%2 == 0 ) this.candleWidth--;
for ( var i = 0 ; i < this.candlesticks.length ; ++i )
{
var color = ( this.candlesticks[i].close > this.candlesticks[i].open ) ? this.greenColor : this.redColor;
if ( i == this.hoveredCandlestickID )
{
if ( color == this.greenColor ) color = this.greenHoverColor;
else if ( color == this.redColor ) color = this.redHoverColor;
}
// draw the wick
this.drawLine( this.xToPixelCoords( this.candlesticks[i].timestamp ) , this.yToPixelCoords( this.candlesticks[i].low ) , this.xToPixelCoords( this.candlesticks[i].timestamp ) , this.yToPixelCoords( this.candlesticks[i].high ) , color );
// draw the candle
this.fillRect( this.xToPixelCoords( this.candlesticks[i].timestamp )-Math.floor( this.candleWidth/2 ) , this.yToPixelCoords( this.candlesticks[i].open ) , this.candleWidth , this.yToPixelCoords( this.candlesticks[i].close ) - this.yToPixelCoords( this.candlesticks[i].open ) , color );
}
// draw mouse hover
if ( this.b_drawMouseOverlay )
{
// price line
this.context.setLineDash( [5,5] );
this.drawLine( 0 , this.mousePosition.y , this.width , this.mousePosition.y , this.mouseHoverBackgroundColor );
this.context.setLineDash( [] );
var str = this.roundPriceValue( this.yMouseHover );
var textWidth = this.context.measureText( str ).width;
this.fillRect( this.width-70 , this.mousePosition.y-10 , 70 , 20 , this.mouseHoverBackgroundColor );
this.context.fillStyle = this.mouseHoverTextColor;
this.context.fillText( str , this.width-textWidth-5 , this.mousePosition.y+5 );
// time line
this.context.setLineDash( [5,5] );
this.drawLine( this.mousePosition.x , 0 , this.mousePosition.x , this.height , this.mouseHoverBackgroundColor );
this.context.setLineDash( [] );
str = this.formatDate( new Date( this.xMouseHover ) );
textWidth = this.context.measureText( str ).width;
this.fillRect( this.mousePosition.x-textWidth/2-5 , this.height-20 , textWidth+10 , 20 , this.mouseHoverBackgroundColor );
this.context.fillStyle = this.mouseHoverTextColor;
this.context.fillText( str , this.mousePosition.x-textWidth/2 , this.height-5 );
// data
var yPos = this.mousePosition.y-95;
if ( yPos < 0 ) yPos = this.mousePosition.y+15;
this.fillRect( this.mousePosition.x+15 , yPos , 100 , 80 , this.mouseHoverBackgroundColor );
var color = ( this.candlesticks[this.hoveredCandlestickID].close > this.candlesticks[this.hoveredCandlestickID].open ) ? this.greenColor : this.redColor;
this.fillRect( this.mousePosition.x+15 , yPos , 10 , 80 , color );
this.context.lineWidth = 2;
this.drawRect( this.mousePosition.x+15 , yPos , 100 , 80 , color );
this.context.lineWidth = 1;
this.context.fillStyle = this.mouseHoverTextColor;
this.context.fillText( "O: "+this.candlesticks[this.hoveredCandlestickID].open , this.mousePosition.x+30 , yPos+15 );
this.context.fillText( "C: "+this.candlesticks[this.hoveredCandlestickID].close , this.mousePosition.x+30 , yPos+35 );
this.context.fillText( "H: "+this.candlesticks[this.hoveredCandlestickID].high , this.mousePosition.x+30 , yPos+55 );
this.context.fillText( "L: "+this.candlesticks[this.hoveredCandlestickID].low , this.mousePosition.x+30 , yPos+75 );
}
}
pingpoliCandlestickChart.prototype.drawGrid = function()
{
// roughly divide the yRange into cells
var yGridSize = (this.yRange)/this.yGridCells;
// try to find a nice number to round to
var niceNumber = Math.pow( 10 , Math.ceil( Math.log10( yGridSize ) ) );
if ( yGridSize < 0.25 * niceNumber ) niceNumber = 0.25 * niceNumber;
else if ( yGridSize < 0.5 * niceNumber ) niceNumber = 0.5 * niceNumber;
// find next largest nice number above yStart
var yStartRoundNumber = Math.ceil( this.yStart/niceNumber ) * niceNumber;
// find next lowest nice number below yEnd
var yEndRoundNumber = Math.floor( this.yEnd/niceNumber ) * niceNumber;
for ( var y = yStartRoundNumber ; y <= yEndRoundNumber ; y += niceNumber )
{
this.drawLine( 0 , this.yToPixelCoords( y ) , this.width , this.yToPixelCoords( y ) , this.gridColor );
var textWidth = this.context.measureText( this.roundPriceValue( y ) ).width;
this.context.fillStyle = this.gridTextColor;
this.context.fillText( this.roundPriceValue( y ) , this.width-textWidth-5 , this.yToPixelCoords( y )-5 );
}
// roughly divide the xRange into cells
var xGridSize = (this.xRange)/this.xGridCells;
// try to find a nice number to round to
niceNumber = Math.pow( 10 , Math.ceil( Math.log10( xGridSize ) ) );
if ( xGridSize < 0.25 * niceNumber ) niceNumber = 0.25 * niceNumber;
else if ( xGridSize < 0.5 * niceNumber ) niceNumber = 0.5 * niceNumber;
// find next largest nice number above yStart
var xStartRoundNumber = Math.ceil( this.xStart/niceNumber ) * niceNumber;
// find next lowest nice number below yEnd
var xEndRoundNumber = Math.floor( this.xEnd/niceNumber ) * niceNumber;
// if the total x range is more than 5 days, format the timestamp as date instead of hours
var b_formatAsDate = false;
if ( this.xRange > 60*60*24*1000*5 ) b_formatAsDate = true;
for ( var x = xStartRoundNumber ; x <= xEndRoundNumber ; x += niceNumber )
{
this.drawLine( this.xToPixelCoords( x ) , 0 , this.xToPixelCoords( x ) , this.height , this.gridColor );
var date = new Date( x );
var dateStr = "";
if ( b_formatAsDate )
{
var day = date.getDate();
if ( day < 10 ) day = "0"+day;
var month = date.getMonth()+1;
if ( month < 10 ) month = "0"+month;
dateStr = day+"."+month;
}
else
{
var minutes = date.getMinutes();
if ( minutes < 10 ) minutes = "0"+minutes;
dateStr = date.getHours()+":"+minutes;
}
this.context.fillStyle = this.gridTextColor;
this.context.fillText( dateStr , this.xToPixelCoords( x )+5 , this.height-5 );
}
}
pingpoliCandlestickChart.prototype.calculateYRange = function()
{
for ( var i = 0 ; i < this.candlesticks.length ; ++i )
{
if ( i == 0 )
{
this.yStart = this.candlesticks[i].low;
this.yEnd = this.candlesticks[i].high;
}
else
{
if ( this.candlesticks[i].low < this.yStart )
{
this.yStart = this.candlesticks[i].low;
}
if ( this.candlesticks[i].high > this.yEnd )
{
this.yEnd = this.candlesticks[i].high;
}
}
}
this.yRange = this.yEnd - this.yStart;
}
pingpoliCandlestickChart.prototype.calculateXRange = function()
{
this.xStart = this.candlesticks[0].timestamp;
this.xEnd = this.candlesticks[this.candlesticks.length-1].timestamp;
this.xRange = this.xEnd - this.xStart;
}
pingpoliCandlestickChart.prototype.yToPixelCoords = function( y )
{
return this.height - this.marginBottom - (y-this.yStart) * this.yPixelRange/this.yRange;
}
pingpoliCandlestickChart.prototype.xToPixelCoords = function( x )
{
return this.marginLeft + (x-this.xStart) * this.xPixelRange/this.xRange;
}
pingpoliCandlestickChart.prototype.yToValueCoords = function( y )
{
return this.yStart + ( this.height - this.marginBottom - y ) * this.yRange/this.yPixelRange;
}
pingpoliCandlestickChart.prototype.xToValueCoords = function( x )
{
return this.xStart + ( x - this.marginLeft ) * this.xRange/this.xPixelRange;
}
pingpoliCandlestickChart.prototype.drawLine = function( xStart , yStart , xEnd , yEnd , color )
{
this.context.beginPath();
// to get a crisp 1 pixel wide line, we need to add 0.5 to the coords
this.context.moveTo( xStart+0.5 , yStart+0.5 );
this.context.lineTo( xEnd+0.5 , yEnd+0.5 );
this.context.strokeStyle = color;
this.context.stroke();
}
pingpoliCandlestickChart.prototype.fillRect = function( x , y , width , height , color )
{
this.context.beginPath();
this.context.fillStyle = color;
this.context.rect( x , y , width , height );
this.context.fill();
}
pingpoliCandlestickChart.prototype.drawRect = function( x , y , width , height , color )
{
this.context.beginPath();
this.context.strokeStyle = color;
this.context.rect( x , y , width , height );
this.context.stroke();
}
pingpoliCandlestickChart.prototype.formatDate = function( date )
{
var day = date.getDate();
if ( day < 10 ) day = "0"+day;
var month = date.getMonth()+1;
if ( month < 10 ) month = "0"+month;
var hours = date.getHours();
if ( hours < 10 ) hours = "0"+hours;
var minutes = date.getMinutes();
if ( minutes < 10 ) minutes = "0"+minutes;
return day+"."+month+"."+date.getFullYear()+" - "+hours+":"+minutes;
}
pingpoliCandlestickChart.prototype.roundPriceValue = function( value )
{
if ( value > 1.0 ) return Math.round( value*100 )/100;
if ( value > 0.001 ) return Math.round( value*1000 )/1000;
if ( value > 0.00001 ) return Math.round( value*100000 )/100000;
if ( value > 0.0000001 ) return Math.round( value*10000000 )/10000000;
else return Math.round( value*1000000000 )/1000000000;
}
@pingpoli
Copy link
Author

In line 148 when drawing the rectangle for the candlestick body, it is drawn with a width of candleWidth from -candleWidth/2 to +candleWidth/2. The this.mousePosition.x += this.candleWidth/2; you mentioned is just to offset the mouse so it's consistent with the drawing.

@fatFire
Copy link

fatFire commented Aug 14, 2021

Thank you, bro! I got it.

Copy link

ghost commented Aug 21, 2021

Thanks, used it in this project 🙏! https://github.com/sigf0x/api

@jonathansilva
Copy link

Great!
I would like to know if it is possible to convert Candlestick to Renko
I need a Renko chart ( in vanilla Javascript ) to do Backtests

@pingpoli
Copy link
Author

@jonathansilva I don't think there is an easy way to use the candlestick chart for Renko data. Candlesticks are time-based, while Renko data isn't, which would require changing the x-axis logic. However, the general chart features could be reused so it probably wouldn't be too complicated to create a vanilla javascript Renko chart.

@jonathansilva
Copy link

@jonathansilva I don't think there is an easy way to use the candlestick chart for Renko data. Candlesticks are time-based, while Renko data isn't, which would require changing the x-axis logic. However, the general chart features could be reused so it probably wouldn't be too complicated to create a vanilla javascript Renko chart.

Thank you very much :)
I will try to adapt here

@jonathansilva
Copy link

jonathansilva commented Jul 24, 2022

It's harder than I imagined
I think it's better to start from the beginning hahaha

@ewaschenko
Copy link

Hi, do you know how you would increase performance for this when plotting large datasets? ie when plotting 50K+ candles, the graph grinds to a halt and performance really suffers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment