apps/recallassess/recallassess-api/src/api/integration/bat-chart.service.ts
BatChartService
Integration API — Bat Chart. Business Logic Service for RecallAssess.
Properties |
|
Methods |
| Private createPostBatChartConfiguration | ||||||||||||||||||||
createPostBatChartConfiguration(skills: Array
|
||||||||||||||||||||
|
Create POST-BAT chart configuration — mirrors createPreBatChartConfiguration so the module labels render identically (bold, halo'd, dark) in the PDF.
Parameters :
Returns :
ChartConfiguration<polarArea, number[], string>
|
| Private createPreBatChartConfiguration | ||||||||||||||||
createPreBatChartConfiguration(skills: Array
|
||||||||||||||||
|
Create PRE-BAT chart configuration
Parameters :
Returns :
ChartConfiguration<polarArea, number[], string>
|
| getBandOuterValue | ||||||
getBandOuterValue(colorClass: string)
|
||||||
|
Ring outer value: 25-50% Foundation, 50-75% Intermediate, 75-100% Advanced, 100-125% Expert.
Parameters :
Returns :
number
|
| getCenterColorHexFromIpq | ||||||
getCenterColorHexFromIpq(ipq: number)
|
||||||
|
Inner ring (center circle) hex from IPQ – same bands as legend (Average → gray).
Parameters :
Returns :
string
|
| getColorHex | ||||||
getColorHex(colorClass: string)
|
||||||
|
Parameters :
Returns :
string
|
| Async renderPostBatChart | |||||||||
renderPostBatChart(skills: Array
|
|||||||||
|
Render POST-BAT chart (for the pre or post assessment). Uses the same shared polar-bat chart options/plugins as renderPreBatChart so the module labels match the pre-BAT chart: bold, layout-scaled font with a white halo. The previous inline config drew them at 12px normal weight, which looked thin/washed-out (grey) in the PDF. No title is baked into the image — the report template renders the chart header label above the
chart (
Parameters :
Returns :
Promise<string | null>
|
| Private Readonly logger |
Type : unknown
|
Default value : new Logger(BatChartService.name)
|
import {
BAT_COLOR_HEX,
getColorClassFromScore,
} from "@api/shared/constants/assessment-scores.constants";
import {
buildPolarBatChartOptions,
getMaxLabelLength,
getPolarBatChartLayout,
getPolarBatExportScale,
registerPolarBatChartPlugins,
} from "@api/shared/polar-bat-chart/polar-bat-chart.config";
import { Injectable, Logger } from "@nestjs/common";
import {
ArcElement,
ChartConfiguration,
Legend,
PolarAreaController,
RadialLinearScale,
Title,
Tooltip,
} from "chart.js";
import { ChartJSNodeCanvas } from "chartjs-node-canvas";
/**
* BatChartService
*
* Integration API — Bat Chart. Business Logic Service for RecallAssess.
*/
@Injectable()
export class BatChartService {
private readonly logger = new Logger(BatChartService.name);
/**
* Render PRE-BAT chart.
* @param skills - Module/skill data for the rings
* @param options.centerColor - Hex color for the center circle (inner ring), e.g. from IPQ band
* @param options.centerLabel - Text to draw in center (e.g. IPQ value)
*/
async renderPreBatChart(
skills: Array<{ name: string; percentage: number; colorClass: string; score: number }>,
options?: { centerColor?: string; centerLabel?: string },
): Promise<string | null> {
if (skills.length === 0) {
return null;
}
const labels = skills.map((skill) => skill.name);
const maxLabelLength = getMaxLabelLength(labels);
const layout = getPolarBatChartLayout(skills.length, maxLabelLength);
const exportScale = getPolarBatExportScale(skills.length);
const configuration = this.createPreBatChartConfiguration(skills, options, exportScale);
try {
const preBatCanvas = new ChartJSNodeCanvas({
width: layout.width * exportScale,
height: layout.height * exportScale,
backgroundColour: "#ffffff",
chartCallback: (ChartJS) => {
ChartJS.register(ArcElement, PolarAreaController, RadialLinearScale, Legend, Title, Tooltip);
registerPolarBatChartPlugins(ChartJS);
},
});
return await preBatCanvas.renderToDataURL(configuration);
} catch (error) {
this.logger.warn("Failed to render PRE-BAT skill chart image for SSR", error);
return null;
}
}
/**
* Render POST-BAT chart (for the pre or post assessment).
*
* Uses the **same** shared polar-bat chart options/plugins as {@link renderPreBatChart} so the
* module labels match the pre-BAT chart: bold, layout-scaled font with a white halo. The previous
* inline config drew them at 12px normal weight, which looked thin/washed-out (grey) in the PDF.
*
* No title is baked into the image — the report template renders the chart header label above the
* chart (`.pie-chart-label`), matching the pre-BAT report.
*
* @param options.centerColor - Hex for center circle (e.g. from IPQ band)
* @param options.centerLabel - Text in center (e.g. IPQ value)
*/
async renderPostBatChart(
skills: Array<{ name: string; percentage: number; colorClass: string; score: number }>,
options?: { centerColor?: string; centerLabel?: string },
): Promise<string | null> {
if (skills.length === 0) {
return null;
}
const labels = skills.map((skill) => skill.name);
const maxLabelLength = getMaxLabelLength(labels);
const layout = getPolarBatChartLayout(skills.length, maxLabelLength);
const exportScale = getPolarBatExportScale(skills.length);
const configuration = this.createPostBatChartConfiguration(skills, "", options, exportScale);
try {
const postBatCanvas = new ChartJSNodeCanvas({
width: layout.width * exportScale,
height: layout.height * exportScale,
backgroundColour: "#ffffff",
chartCallback: (ChartJS) => {
ChartJS.register(ArcElement, PolarAreaController, RadialLinearScale, Legend, Title, Tooltip);
registerPolarBatChartPlugins(ChartJS);
},
});
return await postBatCanvas.renderToDataURL(configuration);
} catch (error) {
this.logger.warn("Failed to render POST-BAT skill chart image for SSR", error);
return null;
}
}
/**
* Create PRE-BAT chart configuration
* @param options.centerColor - Hex for center circle (inner ring)
* @param options.centerLabel - Text in center (e.g. IPQ value)
*/
private createPreBatChartConfiguration(
skills: Array<{ name: string; percentage: number; colorClass: string; score: number }>,
options?: { centerColor?: string; centerLabel?: string },
renderScale = 1,
): ChartConfiguration<"polarArea", number[], string> {
const labels = skills.map((skill) => skill.name);
const maxLabelLength = getMaxLabelLength(labels);
return {
type: "polarArea",
data: {
labels,
datasets: [
{
label: "Course Modules",
data: skills.map((skill) => this.getBandOuterValue(skill.colorClass)),
backgroundColor: skills.map((skill) => this.getColorHex(skill.colorClass)),
borderColor: "#808080",
borderWidth: 1,
borderDash: [5, 5],
},
],
},
options: buildPolarBatChartOptions(
skills.length,
maxLabelLength,
"",
{
centerColor: options?.centerColor,
centerLabel: options?.centerLabel,
},
{ renderScale },
),
};
}
/**
* Create POST-BAT chart configuration — mirrors {@link createPreBatChartConfiguration} so the
* module labels render identically (bold, halo'd, dark) in the PDF.
*
* @param options.centerColor - Hex for center circle (e.g. from IPQ band)
* @param options.centerLabel - Text in center (e.g. IPQ value)
*/
private createPostBatChartConfiguration(
skills: Array<{ name: string; percentage: number; colorClass: string; score: number }>,
title: string,
options?: { centerColor?: string; centerLabel?: string },
renderScale = 1,
): ChartConfiguration<"polarArea", number[], string> {
const labels = skills.map((skill) => skill.name);
const maxLabelLength = getMaxLabelLength(labels);
return {
type: "polarArea",
data: {
labels,
datasets: [
{
label: "Course Modules",
// Clamp each segment to a minimum of 10 so it doesn't collapse at the center
data: skills.map((skill) => this.getBandOuterValue(skill.colorClass)),
backgroundColor: skills.map((skill) => this.getColorHex(skill.colorClass)),
borderColor: "#808080",
borderWidth: 1,
borderDash: [5, 5],
},
],
},
options: buildPolarBatChartOptions(
skills.length,
maxLabelLength,
title,
{
centerColor: options?.centerColor,
centerLabel: options?.centerLabel,
},
{ renderScale },
),
};
}
getColorHex(colorClass: string): string {
return BAT_COLOR_HEX[colorClass as keyof typeof BAT_COLOR_HEX] || BAT_COLOR_HEX.gray;
}
/** Inner ring (center circle) hex from IPQ – same bands as legend (Average → gray). */
getCenterColorHexFromIpq(ipq: number): string {
const cls = getColorClassFromScore(ipq);
const hexMap: Record<string, string> = {
...BAT_COLOR_HEX,
green: BAT_COLOR_HEX["light-green"],
};
return hexMap[cls] ?? BAT_COLOR_HEX.gray;
}
/** Ring outer value: 25-50% Foundation, 50-75% Intermediate, 75-100% Advanced, 100-125% Expert. */
getBandOuterValue(colorClass: string): number {
const band: Record<string, number> = {
red: 50,
orange: 75,
"light-green": 100,
green: 100,
"dark-green": 125,
gray: 75,
};
return band[colorClass] ?? 75;
}
}