File

apps/recallassess/recallassess-api/src/api/integration/bat-chart.service.ts

Description

BatChartService

Integration API — Bat Chart. Business Logic Service for RecallAssess.

Index

Properties
Methods

Methods

Private createPostBatChartConfiguration
createPostBatChartConfiguration(skills: Array, title: string, options?: literal type, renderScale: number)

Create POST-BAT chart configuration — mirrors createPreBatChartConfiguration so the module labels render identically (bold, halo'd, dark) in the PDF.

Parameters :
Name Type Optional Default value
skills Array<literal type> No
title string No
options literal type Yes
renderScale number No 1
Returns : ChartConfiguration<polarArea, number[], string>
Private createPreBatChartConfiguration
createPreBatChartConfiguration(skills: Array, options?: literal type, renderScale: number)

Create PRE-BAT chart configuration

Parameters :
Name Type Optional Default value
skills Array<literal type> No
options literal type Yes
renderScale number No 1
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 :
Name Type Optional
colorClass string No
Returns : number
getCenterColorHexFromIpq
getCenterColorHexFromIpq(ipq: number)

Inner ring (center circle) hex from IPQ – same bands as legend (Average → gray).

Parameters :
Name Type Optional
ipq number No
Returns : string
getColorHex
getColorHex(colorClass: string)
Parameters :
Name Type Optional
colorClass string No
Returns : string
Async renderPostBatChart
renderPostBatChart(skills: Array, options?: literal type)

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 (.pie-chart-label), matching the pre-BAT report.

Parameters :
Name Type Optional
skills Array<literal type> No
options literal type Yes
Returns : Promise<string | null>
Async renderPreBatChart
renderPreBatChart(skills: Array, options?: literal type)

Render PRE-BAT chart.

Parameters :
Name Type Optional Description
skills Array<literal type> No
  • Module/skill data for the rings
options literal type Yes
Returns : Promise<string | null>

Properties

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;
  }
}

results matching ""

    No results matching ""