diff --git a/.gitignore b/.gitignore index 12063ec..100a200 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ dist test-coverage node_modules +# outptu stats files el-stats*.json +el-stats*.txt diff --git a/src/index.ts b/src/index.ts index 8311492..812d0c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,8 @@ import { statsGeneralesDesc } from "./statistiques/v2/generales/StatsGenerales"; import { computeStatsSociales } from "./statistiques/v2/sociales/computeStatsSociales"; import { statsSocialesDesc } from "./statistiques/v2/sociales/StatsSociales"; import { checkDbSchemas } from "./notion/fetch/schemaCheck/checkDbSchemas"; +import { computeSequencEvtPenalSankeyData } from "./statistiques/v2/penales/computeSankeyData"; +import { sankeyDataToMermaidDiagram } from "./statistiques/v2/sankey/sankeyDataToMermaidDiagram"; type ProcessOptions = { dryRun: boolean; @@ -82,6 +84,11 @@ function buildProcessOptions(): ProcessOptions { const statsPenales = computeStatsPenales(familles); const statsSociales = computeStatsSociales(familles); + console.log("Sankey Diagram:"); + writeFileSync( + "./el-stats-penal-sankey-diagram.txt", + sankeyDataToMermaidDiagram(computeSequencEvtPenalSankeyData(familles)) + ); if (options.dryRun) { console.log( "Dry run => Skip Publishing. Stats are dumped in file el-stats-xxx.json" diff --git a/src/statistiques/v2/penales/computeSankeyData.ts b/src/statistiques/v2/penales/computeSankeyData.ts index a076cba..b177e1e 100644 --- a/src/statistiques/v2/penales/computeSankeyData.ts +++ b/src/statistiques/v2/penales/computeSankeyData.ts @@ -1,3 +1,4 @@ +import { EvenementFamille } from "../../../data/EvenementFamille"; import { Famille } from "../../../data/Famille"; import { SankeyData } from "../sankey/SankeyData"; export function computeSequencEvtPenalSankeyData(familles: Famille[]) { @@ -5,15 +6,35 @@ export function computeSequencEvtPenalSankeyData(familles: Famille[]) { familles.forEach((f) => { // Compute all transitions, Events are already sorted - for (let index = 0; index < f.Evenements.length - 1; index++) { - const fromEvt = f.Evenements[index]; - const toEvt = f.Evenements[index + 1]; - const fromTransitions = sankeyData[fromEvt.Type] || {}; - sankeyData[fromEvt.Type] = { - ...fromTransitions, - [toEvt.Type]: (fromTransitions[toEvt.Type] || 0) + 1, + const nonNullEvents = f.Evenements.filter( + (evt) => evt.Type !== null && (evt.Type as string) !== "null" + ); + + // remove duplicate types to avoid circular links that are not supported by mermaid + const evenements = removeDuplicateTypes(nonNullEvents); + + for (let index = 0; index < evenements.length - 1; index++) { + const sourceEvt = evenements[index]; + const targetEvt = evenements[index + 1]; + const sourceSankeyData = sankeyData[sourceEvt.Type] || {}; + sankeyData[sourceEvt.Type] = { + ...sourceSankeyData, + [targetEvt.Type]: (sourceSankeyData[targetEvt.Type] || 0) + 1, }; } }); return sankeyData; } + +function removeDuplicateTypes(events: EvenementFamille[]): EvenementFamille[] { + const seenTypes: { [key: string]: boolean } = {}; + return events.filter((evt) => { + const type = evt.Type as string; + if (seenTypes[type]) { + return false; + } else { + seenTypes[type] = true; + return true; + } + }); +} diff --git a/src/statistiques/v2/sankey/SankeyData.ts b/src/statistiques/v2/sankey/SankeyData.ts index 9e31946..742b365 100644 --- a/src/statistiques/v2/sankey/SankeyData.ts +++ b/src/statistiques/v2/sankey/SankeyData.ts @@ -2,7 +2,7 @@ * Sankey diagram transitions */ export type SankeyData = { - [fromKey: string]: { - [toKey: string]: number; + [source: string]: { + [target: string]: number; }; }; diff --git a/src/statistiques/v2/sankey/sankeyDataToMermaidDiagram.ts b/src/statistiques/v2/sankey/sankeyDataToMermaidDiagram.ts index 7a9fe08..ac36c01 100644 --- a/src/statistiques/v2/sankey/sankeyDataToMermaidDiagram.ts +++ b/src/statistiques/v2/sankey/sankeyDataToMermaidDiagram.ts @@ -1,5 +1,44 @@ import { SankeyData } from "./SankeyData"; -eport function sankeyDataToMermaidDiagram(sankeyData: SankeyData) { - -} \ No newline at end of file +export type SankeyDiagramOptions = { showValues: boolean }; +export function sankeyDataToMermaidDiagram( + sankeyData: SankeyData, + options: SankeyDiagramOptions = { showValues: true } +) { + const header = `--- +config: + sankey: + showValues: ${options.showValues} +--- +sankey-beta +`; + return header + arrayToCsv(dataLines(sankeyData)); +} + +function dataLines(sankeyData: SankeyData): Array<[string, string, number]> { + return Object.entries(sankeyData).flatMap(([source, targets]) => { + return Object.entries(targets).map<[string, string, number]>( + ([target, count]) => { + return [source, target, count]; + } + ); + }); +} + +function arrayToCsv(data: Array>): string { + return data.map((dataLine) => dataLine.map(toCsvValue).join(",")).join("\n"); +} + +function toCsvValue(value: string | number): string { + if (typeof value === "number") { + return value.toString(); + } else { + // Mermaid does not seem to support diacritics + const withoutDiacritics = value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + + const escaped = withoutDiacritics.replaceAll('"', '""'); + return `"${escaped}"`; + } +}