[Chartjs]-Chart.js (chartjs-node-canvas) create date-based floating bar graph

1👍

Inspired by this answer, I came up with the following solution.

const baseData = [
 { awardedDate: "2022-06-22T12:21:17.22Z" }, 
 { awardedDate: "2022-06-18T12:21:17.22Z" }, 
 { awardedDate: "2022-06-15T12:21:17.22Z" }, 
 { awardedDate: "2022-05-20T12:21:17.22Z" }, 
 { awardedDate: "2022-05-10T12:21:17.22Z" }, 
 { awardedDate: "2022-04-16T12:21:17.22Z" }, 
 { awardedDate: "2022-04-09T12:21:17.22Z" }, 
 { awardedDate: "2022-04-03T12:21:17.22Z" }, 
 { awardedDate: "2022-04-01T12:21:17.22Z" }, 
 { awardedDate: "2022-02-18T12:21:17.22Z" }, 
 { awardedDate: "2022-02-12T12:21:17.22Z" }, 
 { awardedDate: "2022-01-17T12:21:17.22Z" }
];

const badgesPerMonth = baseData
  .map(o => o.awardedDate)
  .sort()
  .map(v => moment(v))
  .map(m => m.format('MMM YYYY'))
  .reduce((acc, month) => {
    const badges = acc[month] || 0;
    acc[month] = badges + 1;
    return acc;
  }, {});
const months = Object.keys(badgesPerMonth);
const labels = months.concat('Total');
const data = [];
let total = 0;
for (let i = 0; i < months.length; i++) {
  const vStart = total;
  total += badgesPerMonth[months[i]];
  data.push([vStart, total]);  
}
data.push(total);
const backgroundColors = data
  .map((o, i) => 'rgba(255, 99, 132, ' + (i + (11 - data.length)) * 0.1 + ')');

new Chart('badges', {
  type: 'bar',
  data: {
    labels: labels,
    datasets: [{
      label: 'Badges',
      data: data,
      backgroundColor: backgroundColors,
      barPercentage: 1,
      categoryPercentage: 0.95
    }]
  },
  options: {
    plugins: {
      tooltip: {
        callbacks: {
          label: ctx => {
            const v = data[ctx.dataIndex];
            return Array.isArray(v) ? v[1] - v[0] : v;
          }
        }
      }
    },
    scales: {
      y: {
        ticks: {
          beginAtZero: true,
          stepSize: 2
        }
      }
    }
  }
});
<script src="https://rawgit.com/moment/moment/2.2.1/min/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.8.0/chart.min.js"></script>
<canvas id="badges" height="95"></canvas>

If you also want to see the gaps, you would first have to initialize badgesPerMonth with following months between the earliest and latest date, each with value zero. Please take a look at this answer to get an idea about how this could be done.

0👍

After reading @uminder’s reply, I was able to create the following code which solved my problem:

    dateGroups = Object.fromEntries(
        Object.entries(dateGroups).sort(([d1,],[d2,]) => {return (d1 < d2) ? -1 : ((d1 > d2) ? 1 : 0)})
    )
    const dateTimesConst = Object.keys(dateGroups)
    const dateValuesConst = Object.values(dateGroups)
    let dateTimes = []
    let dateValues = []
    let prevLength = 0
    let mostBadgesPerMonth = 0
    
    for (let i = 0; i < dateValuesConst.length; i++) {
        const currentMonth = new Date(Date.parse(dateTimesConst[i]))
        const previousMonth = new Date(Date.UTC(currentMonth.getUTCFullYear(), currentMonth.getUTCMonth() - 1, 1, 0, 0, 0, 0)).toISOString()
        const nextMonth = new Date(Date.UTC(currentMonth.getUTCFullYear(), currentMonth.getUTCMonth() + 1, 1, 0, 0, 0, 0)).toISOString()

        // if (!dateTimesConst.includes(previousMonth)) prevLength = 0


        const length = dateValuesConst[i].length
        dateValues.push([prevLength, length])
        dateTimes.push(dateTimesConst[i])
        prevLength = length
        if (length > mostBadgesPerMonth) mostBadgesPerMonth = length

        // if (!dateTimesConst.includes(nextMonth) && i !== dateValuesConst.length - 1) {
        //     dateTimes.push(nextMonth)
        //     dateValues.push([length, 0])
        //     prevLength = 0
        // }
    }

    function barColorCode() {
        return (ctx) => {
            const start = ctx.parsed._custom.start
            const end = ctx.parsed._custom.end

            return start <= end ? "rgba(50, 168, 82, 1)" : (start > end) ? "rgba(191, 27, 27, 1)" : "black"
        }
    }

    const config = {
        type: "bar",
        data: {
            labels: dateTimes,
            datasets: [{
                label: "Badges",
                data: dateValues,
                elements: {
                    bar: {
                        backgroundColor: barColorCode()
                    }
                },
                barPercentage: 1,
                categoryPercentage: 0.95,
                borderSkipped: false
            }]
        },
        options: {
            plugins: {
                legend: {
                    display: false
                },
                title: {
                    display: true,
                    text: "Test",
                    color: "#FFFFFF"
                }
            },
            scales: {
                x: {
                    type: 'time',
                    title: {
                        display: true,
                        text: 'Date',
                        color: "#FFFFFF"
                    },
                    time: {
                        unit: "month",
                        round: "month"
                    },
                    min: dateTimesConst[0],
                    max: dateTimesConst[dateTimesConst.length - 1],
                    grid: {
                        borderColor: "#FFFFFF",
                        color: "#FFFFFF"
                    },
                    ticks: {
                        color: "#FFFFFF"
                    }
                },
                y: {
                    title: {
                        display: true,
                        text: 'Number of Badges',
                        borderColor: "#FFFFFF",
                        color: "#FFFFFF"
                    },
                    min: 0,
                    max: mostBadgesPerMonth + 1,
                    grid: {
                        borderColor: "#FFFFFF",
                        color: "#FFFFFF"
                    },
                    ticks: {
                        color: "#FFFFFF"
                    }
                }
            }
        },
        plugins: [
            {
                id: 'custom_canvas_background_color',
                beforeDraw: (chart) => {
                    const ctx = chart.ctx;
                    ctx.save();
                    ctx.fillStyle = '#303030';
                    ctx.fillRect(0, 0, chart.width, chart.height);
                    ctx.restore();
                  }
            }
        ]
    };

    const imageBuffer = await canvasRenderService.renderToBuffer(config)

    fs.writeFileSync("./chart2.png", imageBuffer)

Again, big thanks to @uminder for the inspiration.

Leave a comment