Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/microservices/query-service
1 result
Show changes
Commits on Source (1)
import { scaleOrdinal, scaleQuantize } from "d3"; import { scaleOrdinal, scaleQuantize } from 'd3';
import { JSDOM } from "jsdom"; import { JSDOM } from 'jsdom';
import svg2img from "svg2img"; import svg2img from 'svg2img';
import "canvas"; import 'canvas';
import { visualizationColors, type GraphQueryResultFromBackend, type GraphQueryResultMetaFromBackend } from "ts-common"; import { visualizationColors, type GraphQueryResultFromBackend, type GraphQueryResultMetaFromBackend } from 'ts-common';
import { type PlotType } from "plotly.js"; import { type PlotType } from 'plotly.js';
const dom = new JSDOM(); const dom = new JSDOM();
...@@ -20,41 +20,41 @@ dom.window.HTMLCanvasElement.prototype.getContext = function () { ...@@ -20,41 +20,41 @@ dom.window.HTMLCanvasElement.prototype.getContext = function () {
return null; return null;
}; };
dom.window.URL.createObjectURL = function () { dom.window.URL.createObjectURL = function () {
return ""; return '';
}; };
export enum VariableType { export enum VariableType {
statistic = "statistic", statistic = 'statistic',
visualization = "visualization", visualization = 'visualization',
} }
async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) { async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) {
const replacements = await Promise.all(Array.from(string.matchAll(regexp), (match) => replacerFunction(...match))); const replacements = await Promise.all(Array.from(string.matchAll(regexp), match => replacerFunction(...match)));
let i = 0; let i = 0;
return string.replace(regexp, () => replacements[i++]); return string.replace(regexp, () => replacements[i++]);
} }
export async function populateTemplate(html: string, result: GraphQueryResultMetaFromBackend, openVisualizationArray: any[]) { export async function populateTemplate(html: string, result: GraphQueryResultMetaFromBackend, openVisualizationArray: any[]) {
const regex = / *?{\{ *?(\w*?):([\w ]*?) *?\}\} *?/gm; const regex = / *?{\{ *?(\w*?):([\w ]*?) *?\}\} *?/gm;
return replaceAllAsync(html, regex, async (_: string, _type: string, name: string) => { return replaceAllAsync(html, regex, async (_: string, _type: string, name: string) => {
const type = VariableType[_type as keyof typeof VariableType]; const type = VariableType[_type as keyof typeof VariableType];
switch (type) { switch (type) {
case VariableType.statistic: { case VariableType.statistic: {
const [nodeType, feature, statistic] = name.split(" "); const [nodeType, feature, statistic] = name.split('');
const node = result.metaData.nodes.types[nodeType]; const node = result.metaData.nodes.types[nodeType];
const attribute = node?.attributes[feature].statistics as any; const attribute = node?.attributes[feature].statistics as any;
if (attribute == null) return ""; if (attribute == null) return '';
const value = attribute[statistic]; const value = attribute[statistic];
return ` ${value} `; return ` ${value} `;
} }
case VariableType.visualization: { case VariableType.visualization: {
const activeVisualization = openVisualizationArray.find((x) => x.name == name); // TODO: enforce type const activeVisualization = openVisualizationArray.find(x => x.name == name); // TODO: enforce type
if (!activeVisualization) { if (!activeVisualization) {
throw new Error("Tried to render non-existing visualization"); throw new Error('Tried to render non-existing visualization');
} }
const xAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.xAxisLabel!); const xAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.xAxisLabel!);
let yAxisData: (string | number)[] = []; let yAxisData: (string | number)[] = [];
...@@ -72,9 +72,9 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet ...@@ -72,9 +72,9 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
const stack = activeVisualization.stack; const stack = activeVisualization.stack;
const showAxis = true; const showAxis = true;
const xAxisLabel = ""; const xAxisLabel = '';
const yAxisLabel = ""; const yAxisLabel = '';
const zAxisLabel = ""; const zAxisLabel = '';
const plotType = activeVisualization.plotType; const plotType = activeVisualization.plotType;
...@@ -88,7 +88,7 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet ...@@ -88,7 +88,7 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
zAxisLabel, zAxisLabel,
showAxis, showAxis,
groupBy, groupBy,
stack stack,
); );
const layout2 = { const layout2 = {
...@@ -98,11 +98,11 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet ...@@ -98,11 +98,11 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
title: activeVisualization.title, title: activeVisualization.title,
}; };
const { newPlot } = await import("plotly.js"); const { newPlot } = await import('plotly.js');
const plot = await newPlot(dom.window.document.createElement("div"), plotData, layout2); const plot = await newPlot(dom.window.document.createElement('div'), plotData, layout2);
const svgString = plot.querySelector("svg")?.outerHTML; const svgString = plot.querySelector('svg')?.outerHTML;
if (!svgString) { if (!svgString) {
return ""; return '';
} }
const dataURI = await svgToBase64(svgString); const dataURI = await svgToBase64(svgString);
...@@ -116,19 +116,19 @@ const svgToBase64 = (svgString: string) => { ...@@ -116,19 +116,19 @@ const svgToBase64 = (svgString: string) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
svg2img(svgString, (error: any, buffer: Buffer) => { svg2img(svgString, (error: any, buffer: Buffer) => {
if (error != null) reject(error); if (error != null) reject(error);
resolve(buffer.toString("base64")); resolve(buffer.toString('base64'));
}); });
}); });
}; };
export const plotTypeOptions = ["bar", "scatter", "line", "histogram", "pie"] as const; export const plotTypeOptions = ['bar', 'scatter', 'line', 'histogram', 'pie'] as const;
export type SupportedPlotType = (typeof plotTypeOptions)[number]; export type SupportedPlotType = (typeof plotTypeOptions)[number];
const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableData?: (string | number)[]) => { const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableData?: (string | number)[]) => {
// Function to parse the date-time string into a JavaScript Date object // Function to parse the date-time string into a JavaScript Date object
const parseDate = (dateStr: string) => { const parseDate = (dateStr: string) => {
// Remove nanoseconds part and use just the standard "YYYY-MM-DD HH:MM:SS" part // Remove nanoseconds part and use just the standard "YYYY-MM-DD HH:MM:SS" part
const cleanedDateStr = dateStr.split(".")[0]; const cleanedDateStr = dateStr.split('.')[0];
return new Date(cleanedDateStr); return new Date(cleanedDateStr);
}; };
...@@ -137,15 +137,15 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat ...@@ -137,15 +137,15 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
const date = parseDate(dateStr); const date = parseDate(dateStr);
let groupKey: string; let groupKey: string;
if (groupBy === "yearly") { if (groupBy === 'yearly') {
groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012") groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012")
} else if (groupBy === "quarterly") { } else if (groupBy === 'quarterly') {
const month = date.getMonth() + 1; // Adjust month for zero-indexed months const month = date.getMonth() + 1; // Adjust month for zero-indexed months
const quarter = Math.floor((month - 1) / 3) + 1; // Calculate quarter (Q1-Q4) const quarter = Math.floor((month - 1) / 3) + 1; // Calculate quarter (Q1-Q4)
groupKey = `${date.getFullYear()}-Q${quarter}`; groupKey = `${date.getFullYear()}-Q${quarter}`;
} else if (groupBy === "monthly") { } else if (groupBy === 'monthly') {
// Group by month, e.g., "2012-07" // Group by month, e.g., "2012-07"
groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}`; groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
} else { } else {
// Default case: group by year (or some other grouping logic) // Default case: group by year (or some other grouping logic)
groupKey = date.getFullYear().toString(); groupKey = date.getFullYear().toString();
...@@ -154,7 +154,7 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat ...@@ -154,7 +154,7 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
// Initialize the group if it doesn't exist // Initialize the group if it doesn't exist
if (!acc[groupKey]) { if (!acc[groupKey]) {
acc[groupKey] = additionalVariableData acc[groupKey] = additionalVariableData
? typeof additionalVariableData[0] === "number" ? typeof additionalVariableData[0] === 'number'
? 0 // Initialize sum for numbers ? 0 // Initialize sum for numbers
: [] // Initialize array for strings : [] // Initialize array for strings
: 0; // Initialize count for no additional data : 0; // Initialize count for no additional data
...@@ -162,9 +162,9 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat ...@@ -162,9 +162,9 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
// Aggregate additional variable if provided // Aggregate additional variable if provided
if (additionalVariableData) { if (additionalVariableData) {
if (typeof additionalVariableData[index] === "number") { if (typeof additionalVariableData[index] === 'number') {
acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number); acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number);
} else if (typeof additionalVariableData[index] === "string") { } else if (typeof additionalVariableData[index] === 'string') {
acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string]; acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string];
} }
} else { } else {
...@@ -183,7 +183,7 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat ...@@ -183,7 +183,7 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
}; };
const computeStringTickValues = (xValues: any[], maxTicks: number, maxLabelLength: number): any[] => { const computeStringTickValues = (xValues: any[], maxTicks: number, maxLabelLength: number): any[] => {
const truncatedValues = xValues.map((label) => (label.length > maxLabelLength ? `${label.slice(0, maxLabelLength)}…` : label)); const truncatedValues = xValues.map(label => (label.length > maxLabelLength ? `${label.slice(0, maxLabelLength)}…` : label));
return truncatedValues; return truncatedValues;
}; };
...@@ -198,17 +198,17 @@ export const preparePlotData = ( ...@@ -198,17 +198,17 @@ export const preparePlotData = (
zAxisLabel?: string, zAxisLabel?: string,
showAxis = true, showAxis = true,
groupBy?: string, groupBy?: string,
stack?: boolean stack?: boolean,
): { plotData: Partial<Plotly.PlotData>[]; layout: Partial<Plotly.Layout> } => { ): { plotData: Partial<Plotly.PlotData>[]; layout: Partial<Plotly.Layout> } => {
const primaryColor = "#a2aab9"; // '--clr-sec--400' const primaryColor = '#a2aab9'; // '--clr-sec--400'
const lengthLabelsX = 7; // !TODO computed number of elements based const lengthLabelsX = 7; // !TODO computed number of elements based
const lengthLabelsY = 8; // !TODO computed number of elements based const lengthLabelsY = 8; // !TODO computed number of elements based
const mainColors = visualizationColors.GPCat.colors[14]; const mainColors = visualizationColors.GPCat.colors[14];
const sharedTickFont = { const sharedTickFont = {
family: "monospace", family: 'monospace',
size: 12, size: 12,
color: "#374151", // !TODO get GP value color: '#374151', // !TODO get GP value
}; };
let xValues: (string | number)[] = []; let xValues: (string | number)[] = [];
...@@ -218,19 +218,19 @@ export const preparePlotData = ( ...@@ -218,19 +218,19 @@ export const preparePlotData = (
let colorDataZ: string[] = []; let colorDataZ: string[] = [];
let colorbar: any = {}; let colorbar: any = {};
if (zAxisData && zAxisData.length > 0 && typeof zAxisData[0] === "number") { if (zAxisData && zAxisData.length > 0 && typeof zAxisData[0] === 'number') {
const mainColorsSeq = visualizationColors.GPSeq.colors[9]; const mainColorsSeq = visualizationColors.GPSeq.colors[9];
const numericZAxisData = zAxisData.filter((item): item is number => typeof item === "number"); const numericZAxisData = zAxisData.filter((item): item is number => typeof item === 'number');
const zMin = numericZAxisData.reduce((min, val) => (val < min ? val : min), zAxisData[0]); const zMin = numericZAxisData.reduce((min, val) => (val < min ? val : min), zAxisData[0]);
const zMax = numericZAxisData.reduce((max, val) => (val > max ? val : max), zAxisData[0]); const zMax = numericZAxisData.reduce((max, val) => (val > max ? val : max), zAxisData[0]);
// !TODO: option to have a linear or quantize scale // !TODO: option to have a linear or quantize scale
colorScale = scaleQuantize<string>().domain([zMin, zMax]).range(mainColorsSeq); colorScale = scaleQuantize<string>().domain([zMin, zMax]).range(mainColorsSeq);
colorDataZ = zAxisData?.map((item) => colorScale(item) || primaryColor); colorDataZ = zAxisData?.map(item => colorScale(item) || primaryColor);
colorbar = { colorbar = {
title: "Color Legend", title: 'Color Legend',
tickvals: [zMin, zMax], tickvals: [zMin, zMax],
ticktext: [`${zMin}`, `${zMax}`], ticktext: [`${zMin}`, `${zMax}`],
}; };
...@@ -240,13 +240,13 @@ export const preparePlotData = ( ...@@ -240,13 +240,13 @@ export const preparePlotData = (
if (zAxisData && uniqueZAxisData) { if (zAxisData && uniqueZAxisData) {
colorScale = scaleOrdinal<string>().domain(uniqueZAxisData.map(String)).range(mainColors); colorScale = scaleOrdinal<string>().domain(uniqueZAxisData.map(String)).range(mainColors);
colorDataZ = zAxisData?.map((item) => colorScale(String(item)) || primaryColor); colorDataZ = zAxisData?.map(item => colorScale(String(item)) || primaryColor);
const sortedDomain = uniqueZAxisData.sort(); const sortedDomain = uniqueZAxisData.sort();
colorbar = { colorbar = {
title: "Color Legend", title: 'Color Legend',
tickvals: sortedDomain, tickvals: sortedDomain,
ticktext: sortedDomain.map((val) => String(val)), ticktext: sortedDomain.map(val => String(val)),
tickmode: "array", tickmode: 'array',
}; };
} }
} }
...@@ -286,17 +286,17 @@ export const preparePlotData = ( ...@@ -286,17 +286,17 @@ export const preparePlotData = (
let truncatedYLabels: string[] = []; let truncatedYLabels: string[] = [];
let yAxisRange: number[] = []; let yAxisRange: number[] = [];
if (typeof xValues[0] === "string") { if (typeof xValues[0] === 'string') {
truncatedXLabels = computeStringTickValues(xValues, 2, lengthLabelsX); truncatedXLabels = computeStringTickValues(xValues, 2, lengthLabelsX);
} }
if (typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line")) { if (typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line')) {
truncatedYLabels = computeStringTickValues(yValues, 2, lengthLabelsY); truncatedYLabels = computeStringTickValues(yValues, 2, lengthLabelsY);
} }
const plotData = (() => { const plotData = (() => {
switch (plotType) { switch (plotType) {
case "bar": case 'bar':
if (typeof xAxisData[0] === "string" && groupBy == undefined) { if (typeof xAxisData[0] === 'string' && groupBy == undefined) {
const frequencyMap = xAxisData.reduce((acc, item) => { const frequencyMap = xAxisData.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1; acc[item] = (acc[item] || 0) + 1;
return acc; return acc;
...@@ -315,36 +315,36 @@ export const preparePlotData = ( ...@@ -315,36 +315,36 @@ export const preparePlotData = (
return [ return [
{ {
type: "bar" as PlotType, type: 'bar' as PlotType,
x: xValues, x: xValues,
y: yValues, y: yValues,
marker: { marker: {
color: colorDataZ?.length != 0 ? colorDataZ : primaryColor, color: colorDataZ?.length != 0 ? colorDataZ : primaryColor,
}, },
customdata: sortedLabels, customdata: sortedLabels,
hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>", hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
}, },
]; ];
} else { } else {
return [ return [
{ {
type: "bar" as PlotType, type: 'bar' as PlotType,
x: xValues, x: xValues,
y: yValues, y: yValues,
marker: { color: primaryColor }, marker: { color: primaryColor },
customdata: xValues, customdata: xValues,
hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>", hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
}, },
]; ];
} }
case "scatter": case 'scatter':
return [ return [
{ {
type: "scatter" as PlotType, type: 'scatter' as PlotType,
x: xValues, x: xValues,
y: yValues, y: yValues,
mode: "markers" as const, mode: 'markers' as const,
marker: { marker: {
color: zAxisData && zAxisData.length > 0 ? colorDataZ : primaryColor, color: zAxisData && zAxisData.length > 0 ? colorDataZ : primaryColor,
size: 7, size: 7,
...@@ -352,30 +352,30 @@ export const preparePlotData = ( ...@@ -352,30 +352,30 @@ export const preparePlotData = (
}, },
customdata: customdata:
xValues.length === 0 xValues.length === 0
? yValues.map((y) => `Y: ${y}`) ? yValues.map(y => `Y: ${y}`)
: yValues.length === 0 : yValues.length === 0
? xValues.map((x) => `X: ${x}`) ? xValues.map(x => `X: ${x}`)
: xValues.map((x, index) => { : xValues.map((x, index) => {
const zValue = zAxisData && zAxisData.length > 0 ? zAxisData[index] : null; const zValue = zAxisData && zAxisData.length > 0 ? zAxisData[index] : null;
return zValue ? `X: ${x} | Y: ${yValues[index]} | Color: ${zValue}` : `X: ${x} | Y: ${yValues[index]}`; return zValue ? `X: ${x} | Y: ${yValues[index]} | Color: ${zValue}` : `X: ${x} | Y: ${yValues[index]}`;
}), }),
hovertemplate: "<b>%{customdata}</b><extra></extra>", hovertemplate: '<b>%{customdata}</b><extra></extra>',
}, },
]; ];
case "line": case 'line':
return [ return [
{ {
type: "scatter" as PlotType, type: 'scatter' as PlotType,
x: xValues, x: xValues,
y: yValues, y: yValues,
mode: "lines" as const, mode: 'lines' as const,
line: { color: primaryColor }, line: { color: primaryColor },
customdata: xValues.map((label) => (label === "undefined" || label === "null" || label === "" ? "nonData" : "")), customdata: xValues.map(label => (label === 'undefined' || label === 'null' || label === '' ? 'nonData' : '')),
hovertemplate: "<b>%{customdata}</b><extra></extra>", hovertemplate: '<b>%{customdata}</b><extra></extra>',
}, },
]; ];
case "histogram": case 'histogram':
if (typeof xAxisData[0] === "string") { if (typeof xAxisData[0] === 'string') {
if (zAxisData && zAxisData?.length > 0) { if (zAxisData && zAxisData?.length > 0) {
const frequencyMap = xAxisData.reduce( const frequencyMap = xAxisData.reduce(
(acc, item, index) => { (acc, item, index) => {
...@@ -394,7 +394,7 @@ export const preparePlotData = ( ...@@ -394,7 +394,7 @@ export const preparePlotData = (
acc[item].colors.push(color); acc[item].colors.push(color);
acc[item].zValues.push(zAxisData[index].toString()); acc[item].zValues.push(zAxisData[index].toString());
// Group and count zValues // Group and count zValues
const zValue = zAxisData[index] || "(Empty)"; const zValue = zAxisData[index] || '(Empty)';
acc[item].zValueCounts[zValue] = (acc[item].zValueCounts[zValue] || 0) + 1; acc[item].zValueCounts[zValue] = (acc[item].zValueCounts[zValue] || 0) + 1;
return acc; return acc;
...@@ -407,7 +407,7 @@ export const preparePlotData = ( ...@@ -407,7 +407,7 @@ export const preparePlotData = (
zValues: string[]; zValues: string[];
zValueCounts: Record<string, number>; // To store grouped counts zValueCounts: Record<string, number>; // To store grouped counts
} }
> >,
); );
const colorToLegendName = new Map(); const colorToLegendName = new Map();
const sortedCategories = Object.entries(frequencyMap).sort((a, b) => b[1].count - a[1].count); const sortedCategories = Object.entries(frequencyMap).sort((a, b) => b[1].count - a[1].count);
...@@ -430,13 +430,13 @@ export const preparePlotData = ( ...@@ -430,13 +430,13 @@ export const preparePlotData = (
}); });
}); });
sortedLabels = sortedCategories.map((element) => element[0]); sortedLabels = sortedCategories.map(element => element[0]);
const traces = Array.from(colorToLegendName.entries()).map(([color, legendName]) => { const traces = Array.from(colorToLegendName.entries()).map(([color, legendName]) => {
const colorData = tracesByColor[color]; const colorData = tracesByColor[color];
const categoryCountMap: Record<string, number> = {}; const categoryCountMap: Record<string, number> = {};
sortedLabels.forEach((label) => { sortedLabels.forEach(label => {
categoryCountMap[label] = frequencyMap[label].count; categoryCountMap[label] = frequencyMap[label].count;
}); });
const yValues = colorData.x.map((label, idx) => { const yValues = colorData.x.map((label, idx) => {
...@@ -446,20 +446,20 @@ export const preparePlotData = ( ...@@ -446,20 +446,20 @@ export const preparePlotData = (
}); });
const customdata = colorData.x.map((label, idx) => { const customdata = colorData.x.map((label, idx) => {
const colorTranslation = colorToLegendName.get(color) === " " ? "(Empty)" : colorToLegendName.get(color); const colorTranslation = colorToLegendName.get(color) === ' ' ? '(Empty)' : colorToLegendName.get(color);
const percentage = ((100 * frequencyMap[label].zValueCounts[colorTranslation]) / frequencyMap[label].count).toFixed(1); const percentage = ((100 * frequencyMap[label].zValueCounts[colorTranslation]) / frequencyMap[label].count).toFixed(1);
return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || " "]; return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || ' '];
}); });
return { return {
x: colorData.x, x: colorData.x,
y: yValues, y: yValues,
type: "bar" as PlotType, type: 'bar' as PlotType,
name: legendName, name: legendName,
marker: { color: color }, marker: { color: color },
customdata: customdata, customdata: customdata,
hovertemplate: hovertemplate:
"<b>X: %{customdata[0]}</b><br>" + "<b>Y: %{customdata[1]}</b><br>" + "<b>Color: %{customdata[2]}</b><extra></extra>", '<b>X: %{customdata[0]}</b><br>' + '<b>Y: %{customdata[1]}</b><br>' + '<b>Color: %{customdata[2]}</b><extra></extra>',
...(stack ? { stackgroup: "one" } : {}), ...(stack ? { stackgroup: 'one' } : {}),
}; };
}); });
...@@ -477,19 +477,19 @@ export const preparePlotData = ( ...@@ -477,19 +477,19 @@ export const preparePlotData = (
return [ return [
{ {
type: "bar" as PlotType, type: 'bar' as PlotType,
x: sortedLabels, x: sortedLabels,
y: sortedFrequencies, y: sortedFrequencies,
marker: { color: primaryColor }, marker: { color: primaryColor },
customdata: sortedLabels, customdata: sortedLabels,
hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>", hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
}, },
]; ];
} }
} else { } else {
if (zAxisData && zAxisData?.length > 0) { if (zAxisData && zAxisData?.length > 0) {
const binCount = 20; // Number of bins (you can make this configurable) const binCount = 20; // Number of bins (you can make this configurable)
const numericXAxisData = xAxisData.map((val) => Number(val)).filter((val) => !isNaN(val)); const numericXAxisData = xAxisData.map(val => Number(val)).filter(val => !isNaN(val));
const xMin = numericXAxisData.reduce((min, val) => Math.min(min, val), Infinity); const xMin = numericXAxisData.reduce((min, val) => Math.min(min, val), Infinity);
const xMax = numericXAxisData.reduce((max, val) => Math.max(max, val), -Infinity); const xMax = numericXAxisData.reduce((max, val) => Math.max(max, val), -Infinity);
...@@ -505,7 +505,7 @@ export const preparePlotData = ( ...@@ -505,7 +505,7 @@ export const preparePlotData = (
// Assign data points to bins // Assign data points to bins
numericXAxisData.forEach((xValue, index) => { numericXAxisData.forEach((xValue, index) => {
const zValue = zAxisData ? zAxisData[index] || "(Empty)" : "(Empty)"; const zValue = zAxisData ? zAxisData[index] || '(Empty)' : '(Empty)';
const binIndex = Math.floor((xValue - xMin) / binSize); const binIndex = Math.floor((xValue - xMin) / binSize);
const bin = bins[Math.min(binIndex, bins.length - 1)]; // Ensure the last value falls into the final bin const bin = bins[Math.min(binIndex, bins.length - 1)]; // Ensure the last value falls into the final bin
...@@ -539,23 +539,23 @@ export const preparePlotData = ( ...@@ -539,23 +539,23 @@ export const preparePlotData = (
const colorData = tracesByColor[color]; const colorData = tracesByColor[color];
const customdata = colorData.x.map((binLabel, idx) => { const customdata = colorData.x.map((binLabel, idx) => {
const countForColor = colorData.y[idx]; const countForColor = colorData.y[idx];
const percentage = stack ? countForColor.toFixed(1) + "%" : countForColor.toFixed(0); const percentage = stack ? countForColor.toFixed(1) + '%' : countForColor.toFixed(0);
return [binLabel, countForColor, percentage, legendName]; return [binLabel, countForColor, percentage, legendName];
}); });
return { return {
x: colorData.x, x: colorData.x,
y: colorData.y, y: colorData.y,
type: "bar" as PlotType, type: 'bar' as PlotType,
name: legendName, name: legendName,
marker: { color }, marker: { color },
customdata, customdata,
autobinx: true, autobinx: true,
hovertemplate: hovertemplate:
"<b>Bin: %{customdata[0]}</b><br>" + '<b>Bin: %{customdata[0]}</b><br>' +
"<b>Count/Percentage: %{customdata[2]}</b><br>" + '<b>Count/Percentage: %{customdata[2]}</b><br>' +
"<b>Group: %{customdata[3]}</b><extra></extra>", '<b>Group: %{customdata[3]}</b><extra></extra>',
...(stack ? { stackgroup: "one" } : {}), ...(stack ? { stackgroup: 'one' } : {}),
}; };
}); });
...@@ -564,7 +564,7 @@ export const preparePlotData = ( ...@@ -564,7 +564,7 @@ export const preparePlotData = (
// No zAxisData, simple histogram logic // No zAxisData, simple histogram logic
return [ return [
{ {
type: "histogram" as PlotType, type: 'histogram' as PlotType,
x: xAxisData, x: xAxisData,
marker: { color: primaryColor }, marker: { color: primaryColor },
customdata: xAxisData, customdata: xAxisData,
...@@ -572,10 +572,10 @@ export const preparePlotData = ( ...@@ -572,10 +572,10 @@ export const preparePlotData = (
]; ];
} }
} }
case "pie": case 'pie':
return [ return [
{ {
type: "pie" as PlotType, type: 'pie' as PlotType,
labels: xValues.map(String), labels: xValues.map(String),
values: xAxisData, values: xAxisData,
marker: { colors: mainColors }, marker: { colors: mainColors },
...@@ -587,22 +587,22 @@ export const preparePlotData = ( ...@@ -587,22 +587,22 @@ export const preparePlotData = (
})(); })();
const layout: Partial<Plotly.Layout> = { const layout: Partial<Plotly.Layout> = {
barmode: "stack", barmode: 'stack',
xaxis: { xaxis: {
title: { title: {
text: showAxis ? (xAxisLabel ? xAxisLabel : "") : "", text: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
standoff: 30, standoff: 30,
}, },
tickfont: sharedTickFont, tickfont: sharedTickFont,
showgrid: false, showgrid: false,
visible: showAxis, visible: showAxis,
...(typeof xAxisData[0] === "string" || (plotType === "histogram" && sortedLabels.length > 0) ...(typeof xAxisData[0] === 'string' || (plotType === 'histogram' && sortedLabels.length > 0)
? { type: "category", categoryarray: sortedLabels, categoryorder: "array" } ? { type: 'category', categoryarray: sortedLabels, categoryorder: 'array' }
: {}), : {}),
showline: true, showline: true,
zeroline: false, zeroline: false,
tickvals: typeof xValues[0] == "string" ? xValues : undefined, tickvals: typeof xValues[0] == 'string' ? xValues : undefined,
ticktext: typeof xValues[0] == "string" ? truncatedXLabels : undefined, ticktext: typeof xValues[0] == 'string' ? truncatedXLabels : undefined,
}, },
yaxis: { yaxis: {
...@@ -612,24 +612,24 @@ export const preparePlotData = ( ...@@ -612,24 +612,24 @@ export const preparePlotData = (
zeroline: false, zeroline: false,
tickfont: sharedTickFont, tickfont: sharedTickFont,
title: { title: {
text: showAxis ? (yAxisLabel ? yAxisLabel : "") : "", text: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
standoff: 30, standoff: 30,
}, },
tickvals: typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line") ? yValues : undefined, tickvals: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? yValues : undefined,
ticktext: typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line") ? truncatedYLabels : undefined, ticktext: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? truncatedYLabels : undefined,
}, },
font: { font: {
family: "Inter", family: 'Inter',
size: 12, size: 12,
color: "#374151", color: '#374151',
}, },
hoverlabel: { hoverlabel: {
bgcolor: "rgba(255, 255, 255, 0.8)", bgcolor: 'rgba(255, 255, 255, 0.8)',
bordercolor: "rgba(0, 0, 0, 0.2)", bordercolor: 'rgba(0, 0, 0, 0.2)',
font: { font: {
family: "monospace", family: 'monospace',
size: 14, size: 14,
color: "#374151", color: '#374151',
}, },
}, },
}; };
...@@ -639,21 +639,21 @@ export const preparePlotData = ( ...@@ -639,21 +639,21 @@ export const preparePlotData = (
export const getAttributeValues = ( export const getAttributeValues = (
query: GraphQueryResultFromBackend, query: GraphQueryResultFromBackend,
selectedEntity: string, selectedEntity: string,
attributeKey: string | number | undefined attributeKey: string | number | undefined,
): any[] => { ): any[] => {
if (!selectedEntity || !attributeKey) { if (!selectedEntity || !attributeKey) {
return []; return [];
} }
if (attributeKey == " ") { if (attributeKey == ' ') {
return []; return [];
} }
return query.nodes return query.nodes
.filter((item) => item.label === selectedEntity) .filter(item => item.label === selectedEntity)
.map((item) => { .map(item => {
// Check if the attribute exists, return its value if it does, or an empty string otherwise // Check if the attribute exists, return its value if it does, or an empty string otherwise
return item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] != "" return item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] != ''
? item.attributes[attributeKey] ? item.attributes[attributeKey]
: "NoData"; : 'NoData';
}); });
}; };