Implement a cash flow timeline

2👍

There’s no chart type that produces the desired chart out of the box. It can however be created using a bar chart together with some custom code.

The Plugin Core API offers different hooks that let you perform custom code. You can define an inline plugin and draw the desired lines, shapes and texts directly on the canvas through the CanvasRenderingContext2D. I would use the beforeDraw hook as shown in the runnable code snippet below:

function drawVerticalTriangle(ctx, x, y1, y2) {
  ctx.beginPath();
  ctx.moveTo(x, y1);
  ctx.lineTo(x + 6, y2);
  ctx.lineTo(x - 6, y2);
  ctx.fill();
};

function drawRightArrow(ctx, xLeft, xRight, y) {
  ctx.lineWidth = 3;
  ctx.beginPath(); 
  ctx.moveTo(xLeft, y);
  ctx.lineTo(xRight - 5, y);
  ctx.stroke();      
  ctx.beginPath();      
  ctx.moveTo(xRight, y);
  ctx.lineTo(xRight - 10, y + 6);
  ctx.lineTo(xRight - 10, y - 6);
  ctx.fill();
};

function drawTopArrow(ctx, x, yBottom, yTop) {
  ctx.lineWidth = 3;
  ctx.beginPath(); 
  ctx.moveTo(x, yBottom);
  ctx.lineTo(x, yTop - 5);
  ctx.stroke();      
  drawVerticalTriangle(ctx, x, yTop -10, yTop);
};

new Chart('myChart', {
  type: 'bar',
  plugins: [{
    beforeDraw: chart => {      
      let ctx = chart.ctx; 
      let xAxis = chart.scales.x;
      let yAxis = chart.scales.y;
      xAxis.ticks.forEach((v, i) => {       
        let x = xAxis.getPixelForTick(i);      
        let y = yAxis.getPixelForValue(chart.data.datasets[0].data[i]) - 3; 
        ctx.fillStyle = chart.data.datasets[0].borderColor;
        drawVerticalTriangle(ctx, x, y, y + 10);
        y = yAxis.getPixelForValue(chart.data.datasets[1].data[i]) + 3; 
        ctx.fillStyle = chart.data.datasets[1].borderColor;
        drawVerticalTriangle(ctx, x, y, y - 10);
      }); 
      ctx.strokeStyle = '#9999FF';
      ctx.fillStyle = '#9999FF';      
      let y = yAxis.getPixelForValue(0);
      drawRightArrow(ctx, xAxis.left - 5, xAxis.right + 40, y);
      ctx.font = '12px Arial';
      ctx.textAlign = 'right';
      ctx.fillText('PERIOD', xAxis.right + 30, y + 20);
      drawTopArrow(ctx, xAxis.left, yAxis.bottom + 10, yAxis.top - 10);
    }    
  }],
  data: {
    labels: ['1st year', '2nd year', '3rd year', '4th year'],
    datasets: [
      {
        label: 'Cash inflow', 
        data: [560, 590, 520, 620],   
        backgroundColor: '#0F9D58',    
        borderColor: '#0F9D58',
        barThickness: 4
      },
      {
        label: 'Cash outflow', 
        data: [-600, -550, -410, -570],   
        backgroundColor: '#DB4437',    
        borderColor: '#DB4437',
        barThickness: 4        
      }     
    ]
  },
  options: {
    animation: {
      duration: 100      
    },
    layout: {
      padding: {
        right: 40
      }
    },
    scales: {
      y: {
        grid: {  
          display: false,
        }        
      },
      x: {
        stacked: true,
        grid: {
          display: false,
          drawBorder: false
        }   
      }
    }
  }
});
canvas {
  max-width: 400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.4.0/chart.min.js"></script>
<canvas id="myChart" height="110"></canvas>

👍:0

The best I could do so far is this:

// Import from the chart.js npm module.
import { Chart } from "chart.js"; 

// Get the context of the canvas element in our HTML file.
let ctx = document.querySelector("canvas#chart").getContext("2d"); 

// Create a chart.
var chrt = new Chart(ctx, {
  type: "bar",
  data: {
    labels: ["1st year", "2nd year", "3rd year"],
    datasets: [
      {
        data: [-y1out.value, -y2out.value, -y3out.value]
        , backgroundColor: ["#DB4437", "#DB4437", "#DB4437"]
        , label: 'Cash outflow',
      }
      , {
        data: [y1in.value, y2in.value, y3in.value]
        , backgroundColor: ["#0F9D58", "#0F9D58", "#0F9D58"]
        , label: 'Cash inflow',
      }
    ]
  }
});

// Update chart when user modifies input values
var els = document.getElementsByTagName("input");
for (var i = 0; i < els.length; i++) {
  els[i].addEventListener('input', function() {
    chrt.data.datasets[0].data = [-y1out.value, -y2out.value, -y3out.value]
    chrt.data.datasets[1].data = [y1in.value, y2in.value, y3in.value]
    chrt.update();
  });
}

Result

Result screenshot

Leave a comment