javascriptchart.js

How To Change Candlestick Logic In Chart.js On The Previous Close


By now, everyone knows that the candles on a financial chart should be green if the close is higher than the open and red if the close is higher than the open.

This is a sample

https://www.chartjs.org/chartjs-chart-financial/

But as you can see, many are adapting by setting the color of the candles on the close of the previous candle. That is, if the current close is higher than the close of the previous point, the candle will be green, otherwise red.

https://it.tradingview.com/chart/?symbol=NASDAQ%3ATSLA

The logic adapts to the closing price of the previous point and no longer to the opening and closing of the single point.


Now see the question

Is there a way to adapt this logic to chart.js?

Starting from the financial graph provided by chart.js already exposed above

https://www.chartjs.org/chartjs-chart-financial/

is there a way to adapt the new candlestick logic for the previous point, so not for the opening and closing price of the single candle?

Update

I'm trying to modify the plugin, but to no avail.

The value it's not overwritten.

let previousClose = null;

class CandlestickElement extends FinancialElement {
    static id = 'candlestick';

    static defaults = {
        ...FinancialElement.defaults,
        borderWidth: 1,
    };

    draw(ctx) {
        const me = this;

        const {x, open, high, low, close} = me;

        let borderColors = me.options.borderColors;
        let fillColors = me.options.fillColors;

        if (typeof borderColors === 'string') {
            borderColors = {
                up: borderColors,
                down: borderColors,
                unchanged: borderColors
            };
        }
        if (typeof fillColors === 'string') {
            fillColors = {
                up: fillColors,
                down: fillColors,
                unchanged: fillColors
            };
        }

        let borderColor;
        let fillColor;

        ctx.lineWidth = helpers.valueOrDefault(me.options.borderWidth, chart_js.defaults.elements.candlestick.borderWidth);
        
        if (previousClose === null || close > previousClose) {
            borderColor = 'green';
            fillColor = 'green';
        } else if (close < previousClose) {
            borderColor = 'red';
            fillColor = 'red';
        } else {
            borderColor = 'grey';
            fillColor = 'grey';
        }
        console.log(fillColor, previousClose, close);
        previousClose = close;

        ctx.fillStyle = fillColor;

        ctx.beginPath();
        ctx.moveTo(x, high);
        ctx.lineTo(x, Math.min(open, close));
        ctx.moveTo(x, low);
        ctx.lineTo(x, Math.max(open, close));
        ctx.stroke();
        ctx.fillRect(x - me.width / 2, close, me.width, open - close);
        ctx.strokeRect(x - me.width / 2, close, me.width, open - close);
        ctx.closePath();
    }
}

The strange thing is that if you enter the same value for all

ctx.fillStyle = 'violet';

the value is overwritten correctly for all candles, so you will see on the chart all candles with the new value set.

Update 2

The reason seems that the iteration on the candle is executed multiple times (I should figure out why or how to reset previousClose at each cycle).

You can see it by inserting

console.log('Drawing candle at x:', x, 'Previous Close:', previousClose, 'Current Close:', close);

in the draw(ctx) function immediately after the line

const { x, open, high, low, close } = me;

Solution

  • Solution 1

    Each candlestick element object has enough information for us to compute the colors locally, without transferring the value from one element to the next.

    The variables close, open, high, etc. are in pixels; however the element's $context property has references for the current dataIndex and the whole dataset. You can then define the colors this way:

    const dataIndex = this.$context.dataIndex,
       data = this.$context.dataset.data,
       dataClose = data[dataIndex].c,
       previousDataClose = dataIndex === 0 ? null : data[dataIndex - 1].c;
    
    if (previousDataClose === null || dataClose > previousDataClose) {
       borderColor = 'green';
       fillColor = 'green';
    } else if (dataClose < previousDataClose) {
       borderColor = 'red';
       fillColor = 'red';
    } else {
       borderColor = 'grey';
       fillColor = 'grey';
    }
    

    Here's a snippet based on the example in the docs including the piece of code from above; it also features a way to make those changes by implementing derived classes of the existing controller and element, rather than directly modifying their source code; that has the advantage that the code could use possible future updates of the financial charts extension:

    const CandlestickElement = Chart.registry.getElement('candlestick');
    const CandlestickController = Chart.registry.getController('candlestick')
    
    class CandlestickCorrectColorsElement extends CandlestickElement {
       static id = 'candlestick-correct-colors';
    
       draw(ctx) {
          const me = this;
    
          const {x, open, high, low, close} = me;
    
          let borderColor;
          let fillColor;
    
          //console.log(Chart.defaults.elements['candlestick-correct-color'].borderWidth)
          ctx.lineWidth = this.options.borderWidth ?? Chart.defaults.elements.candlestick.borderWidth;
    
          const dataIndex = this.$context.dataIndex,
             data = this.$context.dataset.data,
             dataClose = data[dataIndex].c,
             previousDataClose = dataIndex === 0 ? null : data[dataIndex - 1].c;
    
          if (previousDataClose === null || dataClose > previousDataClose) {
             borderColor = 'green';
             fillColor = 'green';
          } else if (dataClose < previousDataClose) {
             borderColor = 'red';
             fillColor = 'red';
          } else {
             borderColor = 'grey';
             fillColor = 'grey';
          }
    
          ctx.fillStyle = fillColor;
          ctx.strokeStyle = borderColor;
    
          ctx.beginPath();
          ctx.moveTo(x, high);
          ctx.lineTo(x, Math.min(open, close));
          ctx.moveTo(x, low);
          ctx.lineTo(x, Math.max(open, close));
          ctx.stroke();
          ctx.fillRect(x - me.width / 2, close, me.width, open - close);
          ctx.strokeRect(x - me.width / 2, close, me.width, open - close);
          ctx.closePath();
       }
    }
    
    class CandlestickCorrectColorsController extends CandlestickController{
    
       static id = 'candlestick-correct-colors';
    
       static defaults = {
          ...CandlestickController.defaults,
          dataElementType: CandlestickCorrectColorsElement.id
       };
    }
    
    Chart.register(CandlestickCorrectColorsElement, CandlestickCorrectColorsController);
    
    ////////////////////////////////////////////////////////////////////////////////
    
    const barCount = 60;
    const initialDateStr = new Date().toUTCString();
    
    const ctx = document.getElementById('chart').getContext('2d');
    ctx.canvas.width = 1000;
    ctx.canvas.height = 350;
    
    const barData = new Array(barCount);
    const lineData = new Array(barCount);
    
    getRandomData(initialDateStr);
    
    const chart = new Chart(ctx, {
       type: 'candlestick-correct-colors',
       data: {
          datasets: [{
             label: 'CHRT - Chart.js Corporation',
             data: barData,
          }, {
             label: 'Close price',
             type: 'line',
             data: lineData,
             hidden: true,
          }]
       }
    });
    
    function randomNumber(min, max) {
       return Math.random() * (max - min) + min;
    }
    
    function randomBar(target, index, date, lastClose) {
       const open = +randomNumber(lastClose * 0.95, lastClose * 1.05).toFixed(2);
       const close = +randomNumber(open * 0.95, open * 1.05).toFixed(2);
       const high = +randomNumber(Math.max(open, close), Math.max(open, close) * 1.1).toFixed(2);
       const low = +randomNumber(Math.min(open, close) * 0.9, Math.min(open, close)).toFixed(2);
    
       if (!target[index]) {
          target[index] = {};
       }
    
       Object.assign(target[index], {
          x: date.valueOf(),
          o: open,
          h: high,
          l: low,
          c: close
       });
    
    }
    
    function getRandomData(dateStr) {
       let date = luxon.DateTime.fromRFC2822(dateStr);
    
       for (let i = 0; i < barData.length;) {
          date = date.plus({days: 1});
          if (date.weekday <= 5) {
             randomBar(barData, i, date, i === 0 ? 30 : barData[i - 1].c);
             lineData[i] = {x: barData[i].x, y: barData[i].c};
             i++;
          }
       }
    }
    <div style="height: 350px">
        <canvas id="chart">
        </canvas>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-financial@0.2.1/dist/chartjs-chart-financial.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/luxon"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon"></script>


    Update

    An important case I missed, was pointed out by @user27160653 in messages: when animation is disabled (animation: false in options), there is no $context property on the candlestick elements, so the initial solution didn't work. To cover for that case, I've overridden the method calculateElementProperties of the CandlestickController in the new CandlestickCorrectColorsController to add the $context properties needed by the element:

    class CandlestickCorrectColorsController extends CandlestickController{
    //......
       calculateElementProperties(index, ruler, reset, options){
          const props = super.calculateElementProperties(index, ruler, reset, options);
          props.$context = {
             dataIndex: index,
             dataset: this.chart.data.datasets[this.index]
          };
          return props;
       }
    }
    

    At this point, any other property name could have been used, but there's no harm in sticking with $context, on the contrary, when animation is enabled, there's no duplicate information.

    That piece of code is already added to the above snippet.


    Solution 2

    Your initial idea could also be made to work, but it requires that the global variable previousClose is initialized each time (before) the dataset is drawn. That could be implemented in a plugin; here I saved the value of previousClose in chart.$context:

    const plugin4CandlestickCorrectColors = {
        id: 'plugin-candlestick-correct-colors',
        beforeDatasetDraw: function(chart){
            chart.$context.previousClose = null;
        }
    }
    

    This plugin has to be registered together with the classes. Here's the snippet:

    const CandlestickElement = Chart.registry.getElement('candlestick');
    const CandlestickController = Chart.registry.getController('candlestick')
    
    class CandlestickCorrectColorsElement extends CandlestickElement {
       static id = 'candlestick-correct-colors';
    
       draw(ctx) {
          const me = this;
    
          const {x, open, high, low, close} = me;
    
          let borderColor;
          let fillColor;
    
          //console.log(Chart.defaults.elements['candlestick-correct-color'].borderWidth)
          ctx.lineWidth = this.options.borderWidth ?? Chart.defaults.elements.candlestick.borderWidth ?? 0;
    
    
          const previousClose = this.$context.chart.$context.previousClose;
    
          if (previousClose === null || close > previousClose) {
             borderColor = 'green';
             fillColor = 'green';
          } else if (close < previousClose) {
             borderColor = 'red';
             fillColor = 'red';
          } else {
             borderColor = 'grey';
             fillColor = 'grey';
          }
          this.$context.chart.$context.previousClose = close;
    
          ctx.fillStyle = fillColor;
          ctx.strokeStyle = borderColor;
    
          ctx.beginPath();
          ctx.moveTo(x, high);
          ctx.lineTo(x, Math.min(open, close));
          ctx.moveTo(x, low);
          ctx.lineTo(x, Math.max(open, close));
          ctx.stroke();
          ctx.fillRect(x - me.width / 2, close, me.width, open - close);
          ctx.strokeRect(x - me.width / 2, close, me.width, open - close);
          ctx.closePath();
       }
    }
    
    class CandlestickCorrectColorsController extends CandlestickController{
    
       static id = 'candlestick-correct-colors';
    
       static defaults = {
          ...CandlestickController.defaults,
          dataElementType: CandlestickCorrectColorsElement.id
       };
    }
    
    const plugin4CandlestickCorrectColors = {
       id: 'plugin-candlestick-correct-colors',
       beforeDatasetDraw: function(chart){
          chart.$context.previousClose = null;
       }
    }
    
    Chart.register(CandlestickCorrectColorsElement, CandlestickCorrectColorsController, plugin4CandlestickCorrectColors);
    
    ////////////////////////////////////////////////////////////////////////////////
    
    const barCount = 60;
    const initialDateStr = new Date().toUTCString();
    
    const ctx = document.getElementById('chart').getContext('2d');
    ctx.canvas.width = 1000;
    ctx.canvas.height = 350;
    
    const barData = new Array(barCount);
    const lineData = new Array(barCount);
    
    getRandomData(initialDateStr);
    
    const chart = new Chart(ctx, {
       type: 'candlestick-correct-colors',
       data: {
          datasets: [{
             label: 'CHRT - Chart.js Corporation',
             data: barData,
          }, {
             label: 'Close price',
             type: 'line',
             data: lineData,
             hidden: true,
          }]
       }
    });
    
    function randomNumber(min, max) {
       return Math.random() * (max - min) + min;
    }
    
    function randomBar(target, index, date, lastClose) {
       const open = +randomNumber(lastClose * 0.95, lastClose * 1.05).toFixed(2);
       const close = +randomNumber(open * 0.95, open * 1.05).toFixed(2);
       const high = +randomNumber(Math.max(open, close), Math.max(open, close) * 1.1).toFixed(2);
       const low = +randomNumber(Math.min(open, close) * 0.9, Math.min(open, close)).toFixed(2);
    
       if (!target[index]) {
          target[index] = {};
       }
    
       Object.assign(target[index], {
          x: date.valueOf(),
          o: open,
          h: high,
          l: low,
          c: close
       });
    
    }
    
    function getRandomData(dateStr) {
       let date = luxon.DateTime.fromRFC2822(dateStr);
    
       for (let i = 0; i < barData.length;) {
          date = date.plus({days: 1});
          if (date.weekday <= 5) {
             randomBar(barData, i, date, i === 0 ? 30 : barData[i - 1].c);
             lineData[i] = {x: barData[i].x, y: barData[i].c};
             i++;
          }
       }
    }
    <div style="height: 350px">
        <canvas id="chart">
        </canvas>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-financial@0.2.1/dist/chartjs-chart-financial.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/luxon"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon"></script>