I need your help please
CONTEXT
It turns out that I am creating reports from the backend, the idea is to create them there and then call them from the client. So far so good, I'm using nodejs with express and to create my .pdf files I use puppeter since it was recommended to me for its easy integration and accessible style modification. For the template I am using handlebars (hbs), I have been able to implement it.
But I have had to integrate graphs in the report, I had never done it before, so I started to investigate and read. In the end, I have chosen the Chart.js library because of its extensive documentation and examples that I have found, but at the moment of implementing it in my api it has not been so simple, I did not know how to do it, so I asked and they told me that because it did not integrate the graph creating the HTML and once it has it, it will be passed to the handlebar, as a triple bracket so that hbs interprets it as such (HTML).
So I did it, I followed this answer that I found on stackoverflow in English Chart.js with Node.js with a little translator, what I understood is that they only concatenate the configuration of the chart and from there I passed it to my template and I managed to show it in the pdf.
But then I wanted to add plugins to the chart, but it wouldn't let me, I wanted to add dataLabels chartjs-plugins-datalabels with a function or adding it as recommended in this video How to Show Values Inside a Stacked Bar Chart in Chart JS to show "labels" on top of the graphs that are stacked and although I tried everything that I have mentioned, I could not implement it
So, a little exhausted from researching and testing, I thought, why not pass the script of one in the template? So right now I'm in those, but the problem is that I try to register functions with the help of hbs but that doesn't help me either, and right now I can't show the graph.
ISSUE
How to display canvas functions in HBS?
I thought of using helpers something like this and then just passing it to the {{ element myChart }} script:
handlebars.handlebars.registerHelper('element',
function distanceFixed(chart) {
let c = document.createElement("canvas") as HTMLCanvasElement;
let ctx = c.getContext("2d")!;
ctx.fillStyle = "#FF7605";
ctx.strokeStyle = "#FF7605";
ctx.rect(145, 70, 15, 15);
ctx.fill()
ctx.fillStyle = "#fff";
ctx.fillText(chart.dataset.data[chart.dataIndex], 147, 82, 10);
ctx.stroke();
return c
}
Doing this did not work for me, it did not give me a specific error when debugging. Now try to pass it in the template
element: {
point: {
pointStyle: function {
let c = document.createElement("canvas") as HTMLCanvasElement;
let ctx = c.getContext("2d")!;
ctx.fillStyle = "#FF7605";
ctx.strokeStyle = "#FF7605";
ctx.rect(145, 70, 15, 15);
ctx.fill()
ctx.fillStyle = "#fff";
ctx.fillText(chart.dataset.data[chart.dataIndex], 147, 82, 10);
ctx.stroke();
return c
}
}
}
Problem question
How can I assign functions with canva elements in my template? And how can I add datalabels on top of the stacked charts?
The result I hope to get:
I leave the code with some comments that can be used to understand the structure a little, unfortunately I couldn't make a Demo to better exemplify myself, but I really need help to be able to implement custom functions and datalabels, maybe I'm stuck because I still need to study several concepts that would help me in the integration, but hey, any kind of help will be appreciated!!
code
// mi objeto de informacion
const dataHbs = {
apidata,
"KpiMaxTracked": 20,
"KpiMinTracked": 15,
"dateReport": moment(req.query.fecha_inicio).format('DD / MMMM / YYYY'),
"dayliWeekly": averageWeeklyHour,
"totalWeeklyHours": averageHoursDaily,
"averageActivityWeekly": Math.round(averageActivityWeekly / countElements),
"totalgeneralHours": totalGeneralHours,
"summaryStartsCounts": countStarts,
"weeklyReachHours": Math.round(averageHoursDaily / averageWeeklyHour),
// "canva": _htmlChat
};
// En mi controlado creo una instancia de donde obtengo el metodo con config de puppeter
// para generar el pdf y alli le paso mi objeto con la informacion a mostrar
// dataHbs es un objeto con la info a mostrar
const dateFileName = moment(req.query.fecha_inicio).format('MMMMDDYYYY');
const filename = `CIT-ReporteDiario-${dateFileName}`;
const pdfService = new PdfClass();
const pdfBuffer = await pdfService.getPdf('reporte-semanal-cuatrodias', dataHbs);
res.contentType("application/pdf");
res.send(pdfBuffer);
VIEW HBS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TRACKING</title>
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
</head>
<body>
<div>
<div>
<div class="container-fluid mb-2">
<div class="d-flex justify-content-between align-items-center">
<div class="h2">
<img style="height: 4rem;" alt="" srcset="">
</div>
<div class="d-flex flex-column">
<div class="h5">
<strong>Reporte Semanal Tracking</strong>
</div>
</div>
</div>
</div>
</div>
<div style="font-size: 12px;" class="row mt-2 mb-2">
<div class="col">
<table class="mt-2">
<tr>
<th colspan="2" style="color: #C00000;">
WEEKLY REVIEW
</th>
</tr>
<tr>
<td>
<img style="height: 12px;" src="https://ymlabqgbnnbvyybcyqjn.supabase.co/storage/v1/object/public/assets-ui/icons/status-check.svg" alt="status-check">
</td>
<td>Arriba de {{KpiMaxTracked}} Horas</td>
</tr>
<tr>
<td>
<img style="height: 12px;" src="https://ymlabqgbnnbvyybcyqjn.supabase.co/storage/v1/object/public/assets-ui/icons/status-close.svg" alt="status-close">
</td>
<td>Abajo de {{KpiMinTracked}} Horas</td>
</tr>
</table>
</div>
<div class="col">
<table class="mt-2">
<br>
<tr>
<th colspan="2">WEEKLY REACH HOURS</th>
<td class="text-right" style="white-space: nowrap;"><strong> 37</strong></td>
</tr>
<tr>
<th colspan="2">% ACTIVITY </th>
<td><strong>{{weeklyReachHours}}%</strong></td>
</tr>
</table>
</div>
</div>
<div>
</div>
<div style="font-size: 7px;" class="row">
<div class="col">
<table class="table table-sm mt-2 border-table">
<thead>
<tr>
<th><strong>AREA</strong></th>
<th><strong>NAME</strong></th>
<th class="text-center"><strong>WEEKLY <br> HOURS</strong></th>
<th class="text-center"><strong>WEEKLY SUMMARY HOURS</strong></th>
<th class="text-center"><strong>% ACTIVITY</strong></th>
<th class="text-center"><strong>% HOURS</strong></th>
<th class="text-center"><strong>ESTRELLAS</strong></th>
</tr>
</thead>
<tbody>
{{#each apidata}}
<tr>
<td><strong>{{rolTittle}}</strong></td>
<td style="white-space: nowrap;">{{username}}</td>
<td class="text-center">{{three_weekly_hours}}</td>
<td class="text-center">{{summaryHoursTrackedWeekly}}</td>
<td class="text-center">{{percentActivityW}}</td>
<td class="text-center">{{distanceFixed percentageHours}}</td>
<td class="text-center">{{summaryStarts}}</td>
</tr>
{{/each}}
</tbody>
<tfoot style="font-size: 8px;" class="font-weight-bold">
<tr>
<td style="white-space: nowrap;">
<strong>Total general</strong>
</td>
<td></td>
<td class="text-center"><strong>{{dayliWeekly}}</strong></td>
<td class="text-center"><strong>{{totalWeeklyHours}}</strong></td>
<td class="text-center"><strong>{{averageActivityWeekly}}</strong></td>
<td class="text-center"><strong>{{distanceFixed totalgeneralHours}}</strong></td>
<td class="text-center"><strong>{{summaryStartsCounts}}</strong></td>
</tr>
</tfoot>
</table>
</table>
</div>
</div>
{{!--
<div>{{{canva}}}</div> --}}
<section class="diaperChart">
<div>
<canvas id="myChart" style="h-75 mb-3"></canvas>
</div>
</section>
</div>
<script src="https://kit.fontawesome.com/91a50598c6.js" crossorigin="anonymous"></script>
<script>
let labels = [];
let lineBar = [];
let barHorizontal = [];
let colbWeekly = [];
{
{#
each apidata
}
}
labels.push("{{username}}")
lineBar.push("{{summaryHoursTrackedWeekly}}")
barHorizontal.push("{{percentActivityW}}")
colbWeekly.push("{{three_weekly_hours}}") {
{
/each}}
const ctx = document.getElementById('myChart');
const myChart = new Chart(ctx, {
type: 'horizontalBar',
data: {
{
{
!--labels is labels: labels--
}
}
labels,
datasets: [{
type: 'line',
label: '% ACTIVITY',
data: lineBar,
fill: false,
borderColor: '#FF7605',
borderWidth: 3
},
{
type: 'bar',
label: 'WEEKLY SUMMARY OF HOURS',
data: barHorizontal,
backgroundColor: '#222A35',
},
{
type: 'bar',
label: 'HOURS',
data: colbWeekly,
backgroundColor: '#008582'
}
]
},
options: {
plugins: {
datalabels: {
anchor: 'start',
align: '-45',
clamp: true,
color: '#FF7605'
}
},
element: {
point: {
pointStyle: function {
let c = document.createElement("canvas") as HTMLCanvasElement;
let ctx = c.getContext("2d") !;
ctx.fillStyle = "#FF7605";
ctx.strokeStyle = "#FF7605";
ctx.rect(145, 70, 15, 15);
ctx.fill()
ctx.fillStyle = "#fff";
ctx.fillText(chart.dataset.data[chart.dataIndex], 147, 82, 10);
ctx.stroke();
return c
}
}
},
scales: {
xAxis: {
stacked: true
},
yAxis: {
stacked: true,
ticks: {
beginAtZero: true
}
}
},
tittle: {
display: true,
text: 'Rendimiento Semanal'
},
legend: {
position: 'right'
}
}
});
</script>
</body>
</html>
Happy start of the week!
After trying for almost three weeks, I finally managed to finish what I hoped to achieve.
Doing some research I discovered, that it is not a good practice to access the DOM as I was trying before from the server side, so to improve this I found that Chartjs has a version of its library to be used in conjunction with node: chartjs-node -canvas
It does not require jsdom and besides that it allows all the functions of Chartjs except some like animations (because obviously they would not be necessary on the server side). This one also has some pretty cool methods that help you get a buffer from the chart itself.
Taking these tools into account, I decided to structure the report I needed in a different way. The following graph will explain it for me:
What I do then is build my data object, where I pass the buffer of the image that I generate with the help of the library that I decided to implement, I pass all this information to my HBS template where I make a handlebar utility to unstructure the data and the buffer that I get from the graph and I convert it to base64, then I pass it to the template with png format. When I already have my data and my template configured just as I need it, I call the method that exports the pdf with the help of puppeteer where I pass it the name of my template and the object of my data. Resulting in the following
Now answering the questions I asked in this post:
How can I assign functions with canva elements in my template?
To assign functions or call chartjs hooks, plugin hooks cannot be accessed from a specific plugin within that plugin's config, so I create my own custom plugin and in the plugin itself you can access all hooks .
If even though you separated them it still gives you Types problems, I recommend adding an index.d.ts file where you make an extended variable of what you need, here is an answer that explains it better How to use the options in the custom Chart js plugin v3
Another topic to always touch on in this question is that here I needed to group the information, unfortunately in Chartjs this effect cannot be achieved directly with its elements. However, using Canva elements to draw on it yes, what I did was add an AxisID and then through that ID that I name, format my "X" and "Y" axes
I leave you with a broad explanation on the topic of "grouping" or also known as "multi-level category labels" that may be useful to you Grouping y-axis labels on several lines in a ChartJS with an additional dimension
And how can I add datalabels on top of the stacked charts?
This is quite simple to do, you just have to use a plugin to integrate it . The documentation mentions registering a plugin in Chartjs
And that was all to achieve my expected result, it was extensive research on different topics that helped me learn a little more, and above all I took the experience learning to do reporting with integrated graphics from the server side.
DEMO
Best regards.