diff --git a/api/src/common/interfaces/utilities.interface.ts b/api/src/common/interfaces/utilities.interface.ts
index db02ef99e3f4c67f65f320f1d0bfe1a2a6b832f5..0a0978c5ab64ef2474bea31709697c3d002a5456 100644
--- a/api/src/common/interfaces/utilities.interface.ts
+++ b/api/src/common/interfaces/utilities.interface.ts
@@ -3,6 +3,7 @@ import { ResultUnion } from 'src/engine/models/result/common/result-union.model'
export type Dictionary<T> = { [key: string]: T };
export type ExperimentResult = typeof ResultUnion;
+export type AlgoResults = ExperimentResult[];
export enum MIME_TYPES {
ERROR = 'text/plain+error',
diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..198175a884dce0f40293a5cfec038f3de2920973
--- /dev/null
+++ b/api/src/engine/connectors/exareme/handlers/algorithms/area.handler.ts
@@ -0,0 +1,49 @@
+import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata'
+import { AlgoResults } from 'src/common/interfaces/utilities.interface';
+import { ResultChartExperiment } from '../../interfaces/experiment/result-chart-experiment.interface';
+import BaseHandler from '../base.handler';
+
+export default class AreaHandler extends BaseHandler {
+ static readonly transform = jsonata(`
+ ({
+ "name": data.title.text,
+ "xAxis": {
+ "label": data.xAxis.title.text
+ },
+ "yAxis": {
+ "label": data.yAxis.title.text
+ },
+ "lines": [
+ {
+ "label": "ROC curve",
+ "x": data.series.data.$[0],
+ "y": data.series.data.$[1],
+ "type": 0
+ }
+ ]
+ })
+ `);
+
+ canHandle(input: ResultChartExperiment): boolean {
+ return (
+ input.type === 'application/vnd.highcharts+json' &&
+ input.data.chart.type === 'area'
+ );
+ }
+
+ handle(algorithm: string, data: unknown, res: AlgoResults): void {
+ let req = data;
+ const inputs = data as ResultChartExperiment[];
+
+ if (inputs) {
+ inputs
+ .filter(this.canHandle)
+ .map((input) => AreaHandler.transform.evaluate(input))
+ .forEach((input) => res.push(input));
+
+ req = JSON.stringify(inputs.filter((input) => !this.canHandle(input)));
+ }
+
+ this.next?.handle(algorithm, req, res);
+ }
+}
diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a314a6bf6840e6b447a2ee5ddd881244b5a10793
--- /dev/null
+++ b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts
@@ -0,0 +1,131 @@
+import { AlgoResults } from 'src/common/interfaces/utilities.interface';
+import {
+ GroupResult,
+ GroupsResult,
+} from 'src/engine/models/result/groups-result.model';
+import { ResultExperiment } from '../../interfaces/experiment/result-experiment.interface';
+import BaseHandler from '../base.handler';
+import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata'
+
+export default class DescriptiveHandler extends BaseHandler {
+ private static readonly headerDescriptive = `
+$fnum := function($x) { $type($x) = 'number' ? $round($number($x),3) : $x };
+
+$e := function($x, $r) {($x != null) ? $fnum($x) : ($r ? $r : '')};
+
+$fn := function($o, $prefix) {
+ $type($o) = 'object' ?
+ $each($o, function($v, $k) {(
+ $type($v) = 'object' ? { $k: $v.count & ' (' & $v.percentage & '%)' } : {
+ $k: $v
+ }
+ )}) ~> $merge()
+ : {}
+};`;
+
+ static readonly descriptiveModelToTables = jsonata(`
+(
+ ${this.headerDescriptive}
+
+ $vars := $count($keys(data.model.*.data))-1;
+ $varNames := $keys(data.model.*.data);
+ $model := data.model;
+
+ [[0..$vars].(
+ $i := $;
+ $varName := $varNames[$i];
+ $ks := $keys($model.*.data.*[$i][$type($) = 'object']);
+ {
+ 'name': $varName,
+ 'headers': $append("", $keys($$.data.model)).{
+ 'name': $,
+ 'type': 'string'
+ },
+ 'data': [
+ [$varName, $model.*.($e(num_total))],
+ ['Datapoints', $model.*.($e(num_datapoints))],
+ ['Nulls', $model.*.($e(num_nulls))],
+ ($lookup($model.*.data, $varName).($fn($)) ~> $reduce(function($a, $b) {
+ $map($ks, function($k) {(
+ {
+ $k : [$e($lookup($a,$k), "No data"), $e($lookup($b,$k), "No data")]
+ }
+ )}) ~> $merge()
+ })).$each(function($v, $k) {$append($k,$v)})[]
+ ]
+ }
+ )]
+)`);
+
+ static readonly descriptiveSingleToTables = jsonata(`
+(
+ ${this.headerDescriptive}
+
+ data.[
+ $.single.*@$p#$i.(
+ $ks := $keys($p.*.data[$type($) = 'object']);
+ {
+ 'name': $keys(%)[$i],
+ 'headers': $append("", $keys(*)).{
+ 'name': $,
+ 'type': 'string'
+ },
+ 'data' : [
+ [$keys(%)[$i], $p.*.($e(num_total))],
+ ['Datapoints', $p.*.($e(num_datapoints))],
+ ['Nulls', $p.*.($e(num_nulls))],
+ ($p.*.data.($fn($)) ~> $reduce(function($a, $b) {
+ $map($ks, function($k) {(
+ {
+ $k : [$e($lookup($a,$k), "No data"), $e($lookup($b,$k), "No data")]
+ }
+ )}) ~> $merge()
+ })).$each(function($v, $k) {$append($k,$v)})[]
+ ]
+ })
+ ]
+)
+`);
+
+ descriptiveDataToTableResult(data: ResultExperiment): GroupsResult {
+ const result = new GroupsResult();
+
+ result.groups = [
+ new GroupResult({
+ name: 'Variables',
+ description: 'Descriptive statistics for the variables of interest.',
+ results: DescriptiveHandler.descriptiveSingleToTables.evaluate(data),
+ }),
+ ];
+
+ result.groups.push(
+ new GroupResult({
+ name: 'Model',
+ description:
+ 'Intersection table for the variables of interest as it appears in the experiment.',
+ results: DescriptiveHandler.descriptiveModelToTables.evaluate(data),
+ }),
+ );
+
+ return result;
+ }
+
+ handle(algorithm: string, data: unknown, res: AlgoResults): void {
+ let req = data;
+
+ if (algorithm.toLowerCase() === 'descriptive_stats') {
+ const inputs = data as ResultExperiment[];
+
+ inputs
+ .filter((input) => input.type === 'application/json')
+ .map((input) => this.descriptiveDataToTableResult(input))
+ .forEach((input) => res.push(input));
+
+ req = JSON.stringify(
+ inputs.filter((input) => input.type !== 'application/json'),
+ );
+ }
+
+ this.next?.handle(algorithm, req, res);
+ }
+}
diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..20591ad7589b9e075d2266a22ae5ccd7f0acd7a8
--- /dev/null
+++ b/api/src/engine/connectors/exareme/handlers/algorithms/heat-map.handler.ts
@@ -0,0 +1,46 @@
+import { AlgoResults } from 'src/common/interfaces/utilities.interface';
+import { ResultChartExperiment } from '../../interfaces/experiment/result-chart-experiment.interface';
+import BaseHandler from '../base.handler';
+import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata'
+
+export default class HeatMapHandler extends BaseHandler {
+ static readonly transform = jsonata(`
+ (
+ {
+ "name": data.title.text,
+ "xAxis": {
+ "categories": data.xAxis.categories,
+ "label": data.xAxis.label
+ },
+ "yAxis": {
+ "categories": data.yAxis.categories,
+ "label": data.yAxis.label
+ },
+ "matrix": $toMat(data.series.data)
+ }
+ )
+ `);
+
+ canHandle(input: ResultChartExperiment): boolean {
+ return (
+ input.type.toLowerCase() === 'application/vnd.highcharts+json' &&
+ input.data.chart.type.toLowerCase() === 'heatmap'
+ );
+ }
+
+ handle(algorithm: string, data: unknown, res: AlgoResults): void {
+ let req = data;
+ const inputs = data as ResultChartExperiment[];
+
+ if (inputs) {
+ inputs
+ .filter(this.canHandle)
+ .map((input) => HeatMapHandler.transform.evaluate(input))
+ .forEach((input) => res.push(input));
+
+ req = JSON.stringify(inputs.filter((input) => !this.canHandle(input)));
+ }
+
+ this.next?.handle(algorithm, req, res);
+ }
+}
diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts
index 731cbad2eb2434716dada2c1fcd435ca2c303c04..16702657df654d7c282efe54094ce46e4fb5e497 100644
--- a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts
+++ b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts
@@ -1,9 +1,11 @@
-import jsonata, { Expression } from 'jsonata';
-import { Results } from 'src/common/interfaces/utilities.interface';
+import { Expression } from 'jsonata';
+import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata'
+import { AlgoResults } from 'src/common/interfaces/utilities.interface';
+import { HeatMapResult } from 'src/engine/models/result/heat-map-result.model';
import BaseHandler from '../base.handler';
-class PearsonHandler extends BaseHandler {
- readonly pearsonCorellation: Expression = jsonata(`
+export default class PearsonHandler extends BaseHandler {
+ readonly transform: Expression = jsonata(`
(
$params := ['correlations', 'p-values', 'low_confidence_intervals', 'high_confidence_intervals'];
@@ -21,9 +23,25 @@ class PearsonHandler extends BaseHandler {
})
)`);
- handle(data: JSON, res: Results): void {
- try {
- const results = this.pearsonCorellation.evaluate(data);
- } catch (e) {}
+ canHandle(algorithm: string): boolean {
+ return algorithm.toLocaleLowerCase() === 'pearson';
+ }
+
+ handle(algorithm: string, data: unknown, res: AlgoResults): void {
+ if (this.canHandle(algorithm)) {
+ try {
+ const results = this.transform.evaluate(data) as HeatMapResult[];
+ results
+ .filter((heatMap) => heatMap.matrix.length > 0 && heatMap.name)
+ .forEach((heatMap) => res.push(heatMap));
+ } catch (e) {
+ PearsonHandler.logger.warn(
+ 'An error occur when converting result from Pearson',
+ );
+ PearsonHandler.logger.verbose(JSON.stringify(data));
+ }
+ }
+
+ this.next?.handle(algorithm, data, res);
}
}
diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..810be82cecd06bb193adaefb88fc50ccbdaa2fad
--- /dev/null
+++ b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts
@@ -0,0 +1,29 @@
+import {
+ AlgoResults,
+ MIME_TYPES,
+} from 'src/common/interfaces/utilities.interface';
+import { RawResult } from 'src/engine/models/result/raw-result.model';
+import { ResultExperiment } from '../../interfaces/experiment/result-experiment.interface';
+import BaseHandler from '../base.handler';
+
+export default class RawHandler extends BaseHandler {
+ dataToRaw = (algo: string, result: ResultExperiment): RawResult => {
+ let data = result;
+
+ if (algo === 'CART') {
+ data = { ...data, type: MIME_TYPES.JSONBTREE };
+ }
+
+ return { rawdata: data };
+ };
+
+ handle(algorithm: string, data: unknown, res: AlgoResults): void {
+ const inputs = data as ResultExperiment[];
+
+ inputs
+ .map((input) => this.dataToRaw(algorithm, input))
+ .forEach((input) => res.push(input));
+
+ this.next?.handle(algorithm, data, res);
+ }
+}
diff --git a/api/src/engine/connectors/exareme/handlers/base.handler.ts b/api/src/engine/connectors/exareme/handlers/base.handler.ts
index 75c19cfc29028cbf940988ac49d271b4f5242579..2f72f190258e37f1bd689cdd6163b32699586b52 100644
--- a/api/src/engine/connectors/exareme/handlers/base.handler.ts
+++ b/api/src/engine/connectors/exareme/handlers/base.handler.ts
@@ -1,14 +1,18 @@
-import { Results } from 'src/common/interfaces/utilities.interface';
+import { Logger } from '@nestjs/common';
+import { AlgoResults } from 'src/common/interfaces/utilities.interface';
import ResultHandler from './result-handler.interface';
export default abstract class BaseHandler implements ResultHandler {
+ protected static readonly logger = new Logger(this.name);
+
next: ResultHandler = null;
- setNext(h: ResultHandler): void {
+ setNext(h: ResultHandler): ResultHandler {
this.next = h;
+ return h;
}
- handle(data: JSON, res: Results): void {
- this.next?.handle(data, res);
+ handle(algorithm: string, data: unknown, res: AlgoResults): void {
+ this.next?.handle(algorithm, data, res);
}
}
diff --git a/api/src/engine/connectors/exareme/handlers/index.ts b/api/src/engine/connectors/exareme/handlers/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e153ece232bc26a5a2de18d65265bdc1b34f0eb
--- /dev/null
+++ b/api/src/engine/connectors/exareme/handlers/index.ts
@@ -0,0 +1,19 @@
+import { AlgoResults } from 'src/common/interfaces/utilities.interface';
+import AreaHandler from './algorithms/area.handler';
+import DescriptiveHandler from './algorithms/descriptive.handler';
+import HeatMapHandler from './algorithms/heat-map.handler';
+import {
+ default as PearsonHandler,
+ default as RawHandler,
+} from './algorithms/raw.handler';
+
+const last = new RawHandler(); // should be last handler as it works as a fallback (if other handlers could not process the results)
+const start = new PearsonHandler()
+ .setNext(new AreaHandler())
+ .setNext(new DescriptiveHandler())
+ .setNext(new HeatMapHandler())
+ .setNext(last);
+
+export default (algo: string, data: unknown, res: AlgoResults) => {
+ start.handle(algo, data, res);
+};
diff --git a/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts b/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts
index da929b9eb839199aa9c0d6b9c85a08b46c44fd39..93f98a9e3043a793a08b233ae66965ebc1e93495 100644
--- a/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts
+++ b/api/src/engine/connectors/exareme/handlers/result-handler.interface.ts
@@ -1,7 +1,7 @@
-import { Results } from 'src/common/interfaces/utilities.interface';
+import { AlgoResults } from 'src/common/interfaces/utilities.interface';
// produce algo handler
export default interface ResultHandler {
- setNext(h: ResultHandler): void;
- handle(data: JSON, res: Results): void;
+ setNext(h: ResultHandler): ResultHandler;
+ handle(algorithm: string, data: unknown, res: AlgoResults): void;
}
diff --git a/api/src/schema.gql b/api/src/schema.gql
index 4d248dc43d05871c60002fd581ba9a9a41f0ff07..b940e0ec06d23a0e87f41a791a669163fb88fe47 100644
--- a/api/src/schema.gql
+++ b/api/src/schema.gql
@@ -133,7 +133,10 @@ type LineChartResult {
}
type ChartAxis {
+ """label of the Axis"""
label: String
+
+ """label of each element on this Axis"""
categories: [String!]
}