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 { JSDOM } from "jsdom";
import svg2img from "svg2img";
import "canvas";
import { visualizationColors, type GraphQueryResultFromBackend, type GraphQueryResultMetaFromBackend } from "ts-common";
import { scaleOrdinal, scaleQuantize } from 'd3';
import { JSDOM } from 'jsdom';
import svg2img from 'svg2img';
import 'canvas';
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();
......@@ -20,41 +20,41 @@ dom.window.HTMLCanvasElement.prototype.getContext = function () {
return null;
};
dom.window.URL.createObjectURL = function () {
return "";
return '';
};
export enum VariableType {
statistic = "statistic",
visualization = "visualization",
statistic = 'statistic',
visualization = 'visualization',
}
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;
return string.replace(regexp, () => replacements[i++]);
}
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) => {
const type = VariableType[_type as keyof typeof VariableType];
switch (type) {
case VariableType.statistic: {
const [nodeType, feature, statistic] = name.split(" ");
const [nodeType, feature, statistic] = name.split('');
const node = result.metaData.nodes.types[nodeType];
const attribute = node?.attributes[feature].statistics as any;
if (attribute == null) return "";
if (attribute == null) return '';
const value = attribute[statistic];
return ` ${value} `;
}
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) {
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!);
let yAxisData: (string | number)[] = [];
......@@ -72,9 +72,9 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
const stack = activeVisualization.stack;
const showAxis = true;
const xAxisLabel = "";
const yAxisLabel = "";
const zAxisLabel = "";
const xAxisLabel = '';
const yAxisLabel = '';
const zAxisLabel = '';
const plotType = activeVisualization.plotType;
......@@ -88,7 +88,7 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
zAxisLabel,
showAxis,
groupBy,
stack
stack,
);
const layout2 = {
......@@ -98,11 +98,11 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
title: activeVisualization.title,
};
const { newPlot } = await import("plotly.js");
const plot = await newPlot(dom.window.document.createElement("div"), plotData, layout2);
const svgString = plot.querySelector("svg")?.outerHTML;
const { newPlot } = await import('plotly.js');
const plot = await newPlot(dom.window.document.createElement('div'), plotData, layout2);
const svgString = plot.querySelector('svg')?.outerHTML;
if (!svgString) {
return "";
return '';
}
const dataURI = await svgToBase64(svgString);
......@@ -116,19 +116,19 @@ const svgToBase64 = (svgString: string) => {
return new Promise((resolve, reject) => {
svg2img(svgString, (error: any, buffer: Buffer) => {
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];
const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableData?: (string | number)[]) => {
// Function to parse the date-time string into a JavaScript Date object
const parseDate = (dateStr: string) => {
// 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);
};
......@@ -137,15 +137,15 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
const date = parseDate(dateStr);
let groupKey: string;
if (groupBy === "yearly") {
if (groupBy === 'yearly') {
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 quarter = Math.floor((month - 1) / 3) + 1; // Calculate quarter (Q1-Q4)
groupKey = `${date.getFullYear()}-Q${quarter}`;
} else if (groupBy === "monthly") {
} else if (groupBy === 'monthly') {
// 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 {
// Default case: group by year (or some other grouping logic)
groupKey = date.getFullYear().toString();
......@@ -154,7 +154,7 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
// Initialize the group if it doesn't exist
if (!acc[groupKey]) {
acc[groupKey] = additionalVariableData
? typeof additionalVariableData[0] === "number"
? typeof additionalVariableData[0] === 'number'
? 0 // Initialize sum for numbers
: [] // Initialize array for strings
: 0; // Initialize count for no additional data
......@@ -162,9 +162,9 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
// Aggregate additional variable if provided
if (additionalVariableData) {
if (typeof additionalVariableData[index] === "number") {
if (typeof additionalVariableData[index] === '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];
}
} else {
......@@ -183,7 +183,7 @@ const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableDat
};
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;
};
......@@ -198,17 +198,17 @@ export const preparePlotData = (
zAxisLabel?: string,
showAxis = true,
groupBy?: string,
stack?: boolean
stack?: boolean,
): { 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 lengthLabelsY = 8; // !TODO computed number of elements based
const mainColors = visualizationColors.GPCat.colors[14];
const sharedTickFont = {
family: "monospace",
family: 'monospace',
size: 12,
color: "#374151", // !TODO get GP value
color: '#374151', // !TODO get GP value
};
let xValues: (string | number)[] = [];
......@@ -218,19 +218,19 @@ export const preparePlotData = (
let colorDataZ: string[] = [];
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 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 zMax = numericZAxisData.reduce((max, val) => (val > max ? val : max), zAxisData[0]);
// !TODO: option to have a linear or quantize scale
colorScale = scaleQuantize<string>().domain([zMin, zMax]).range(mainColorsSeq);
colorDataZ = zAxisData?.map((item) => colorScale(item) || primaryColor);
colorDataZ = zAxisData?.map(item => colorScale(item) || primaryColor);
colorbar = {
title: "Color Legend",
title: 'Color Legend',
tickvals: [zMin, zMax],
ticktext: [`${zMin}`, `${zMax}`],
};
......@@ -240,13 +240,13 @@ export const preparePlotData = (
if (zAxisData && uniqueZAxisData) {
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();
colorbar = {
title: "Color Legend",
title: 'Color Legend',
tickvals: sortedDomain,
ticktext: sortedDomain.map((val) => String(val)),
tickmode: "array",
ticktext: sortedDomain.map(val => String(val)),
tickmode: 'array',
};
}
}
......@@ -286,17 +286,17 @@ export const preparePlotData = (
let truncatedYLabels: string[] = [];
let yAxisRange: number[] = [];
if (typeof xValues[0] === "string") {
if (typeof xValues[0] === 'string') {
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);
}
const plotData = (() => {
switch (plotType) {
case "bar":
if (typeof xAxisData[0] === "string" && groupBy == undefined) {
case 'bar':
if (typeof xAxisData[0] === 'string' && groupBy == undefined) {
const frequencyMap = xAxisData.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
......@@ -315,36 +315,36 @@ export const preparePlotData = (
return [
{
type: "bar" as PlotType,
type: 'bar' as PlotType,
x: xValues,
y: yValues,
marker: {
color: colorDataZ?.length != 0 ? colorDataZ : primaryColor,
},
customdata: sortedLabels,
hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>",
hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
},
];
} else {
return [
{
type: "bar" as PlotType,
type: 'bar' as PlotType,
x: xValues,
y: yValues,
marker: { color: primaryColor },
customdata: xValues,
hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>",
hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
},
];
}
case "scatter":
case 'scatter':
return [
{
type: "scatter" as PlotType,
type: 'scatter' as PlotType,
x: xValues,
y: yValues,
mode: "markers" as const,
mode: 'markers' as const,
marker: {
color: zAxisData && zAxisData.length > 0 ? colorDataZ : primaryColor,
size: 7,
......@@ -352,30 +352,30 @@ export const preparePlotData = (
},
customdata:
xValues.length === 0
? yValues.map((y) => `Y: ${y}`)
? yValues.map(y => `Y: ${y}`)
: yValues.length === 0
? xValues.map((x) => `X: ${x}`)
? xValues.map(x => `X: ${x}`)
: xValues.map((x, index) => {
const zValue = zAxisData && zAxisData.length > 0 ? zAxisData[index] : null;
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 [
{
type: "scatter" as PlotType,
type: 'scatter' as PlotType,
x: xValues,
y: yValues,
mode: "lines" as const,
mode: 'lines' as const,
line: { color: primaryColor },
customdata: xValues.map((label) => (label === "undefined" || label === "null" || label === "" ? "nonData" : "")),
hovertemplate: "<b>%{customdata}</b><extra></extra>",
customdata: xValues.map(label => (label === 'undefined' || label === 'null' || label === '' ? 'nonData' : '')),
hovertemplate: '<b>%{customdata}</b><extra></extra>',
},
];
case "histogram":
if (typeof xAxisData[0] === "string") {
case 'histogram':
if (typeof xAxisData[0] === 'string') {
if (zAxisData && zAxisData?.length > 0) {
const frequencyMap = xAxisData.reduce(
(acc, item, index) => {
......@@ -394,7 +394,7 @@ export const preparePlotData = (
acc[item].colors.push(color);
acc[item].zValues.push(zAxisData[index].toString());
// Group and count zValues
const zValue = zAxisData[index] || "(Empty)";
const zValue = zAxisData[index] || '(Empty)';
acc[item].zValueCounts[zValue] = (acc[item].zValueCounts[zValue] || 0) + 1;
return acc;
......@@ -407,7 +407,7 @@ export const preparePlotData = (
zValues: string[];
zValueCounts: Record<string, number>; // To store grouped counts
}
>
>,
);
const colorToLegendName = new Map();
const sortedCategories = Object.entries(frequencyMap).sort((a, b) => b[1].count - a[1].count);
......@@ -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 colorData = tracesByColor[color];
const categoryCountMap: Record<string, number> = {};
sortedLabels.forEach((label) => {
sortedLabels.forEach(label => {
categoryCountMap[label] = frequencyMap[label].count;
});
const yValues = colorData.x.map((label, idx) => {
......@@ -446,20 +446,20 @@ export const preparePlotData = (
});
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);
return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || " "];
return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || ' '];
});
return {
x: colorData.x,
y: yValues,
type: "bar" as PlotType,
type: 'bar' as PlotType,
name: legendName,
marker: { color: color },
customdata: customdata,
hovertemplate:
"<b>X: %{customdata[0]}</b><br>" + "<b>Y: %{customdata[1]}</b><br>" + "<b>Color: %{customdata[2]}</b><extra></extra>",
...(stack ? { stackgroup: "one" } : {}),
'<b>X: %{customdata[0]}</b><br>' + '<b>Y: %{customdata[1]}</b><br>' + '<b>Color: %{customdata[2]}</b><extra></extra>',
...(stack ? { stackgroup: 'one' } : {}),
};
});
......@@ -477,19 +477,19 @@ export const preparePlotData = (
return [
{
type: "bar" as PlotType,
type: 'bar' as PlotType,
x: sortedLabels,
y: sortedFrequencies,
marker: { color: primaryColor },
customdata: sortedLabels,
hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>",
hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
},
];
}
} else {
if (zAxisData && zAxisData?.length > 0) {
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 xMax = numericXAxisData.reduce((max, val) => Math.max(max, val), -Infinity);
......@@ -505,7 +505,7 @@ export const preparePlotData = (
// Assign data points to bins
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 bin = bins[Math.min(binIndex, bins.length - 1)]; // Ensure the last value falls into the final bin
......@@ -539,23 +539,23 @@ export const preparePlotData = (
const colorData = tracesByColor[color];
const customdata = colorData.x.map((binLabel, 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 {
x: colorData.x,
y: colorData.y,
type: "bar" as PlotType,
type: 'bar' as PlotType,
name: legendName,
marker: { color },
customdata,
autobinx: true,
hovertemplate:
"<b>Bin: %{customdata[0]}</b><br>" +
"<b>Count/Percentage: %{customdata[2]}</b><br>" +
"<b>Group: %{customdata[3]}</b><extra></extra>",
...(stack ? { stackgroup: "one" } : {}),
'<b>Bin: %{customdata[0]}</b><br>' +
'<b>Count/Percentage: %{customdata[2]}</b><br>' +
'<b>Group: %{customdata[3]}</b><extra></extra>',
...(stack ? { stackgroup: 'one' } : {}),
};
});
......@@ -564,7 +564,7 @@ export const preparePlotData = (
// No zAxisData, simple histogram logic
return [
{
type: "histogram" as PlotType,
type: 'histogram' as PlotType,
x: xAxisData,
marker: { color: primaryColor },
customdata: xAxisData,
......@@ -572,10 +572,10 @@ export const preparePlotData = (
];
}
}
case "pie":
case 'pie':
return [
{
type: "pie" as PlotType,
type: 'pie' as PlotType,
labels: xValues.map(String),
values: xAxisData,
marker: { colors: mainColors },
......@@ -587,22 +587,22 @@ export const preparePlotData = (
})();
const layout: Partial<Plotly.Layout> = {
barmode: "stack",
barmode: 'stack',
xaxis: {
title: {
text: showAxis ? (xAxisLabel ? xAxisLabel : "") : "",
text: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
standoff: 30,
},
tickfont: sharedTickFont,
showgrid: false,
visible: showAxis,
...(typeof xAxisData[0] === "string" || (plotType === "histogram" && sortedLabels.length > 0)
? { type: "category", categoryarray: sortedLabels, categoryorder: "array" }
...(typeof xAxisData[0] === 'string' || (plotType === 'histogram' && sortedLabels.length > 0)
? { type: 'category', categoryarray: sortedLabels, categoryorder: 'array' }
: {}),
showline: true,
zeroline: false,
tickvals: typeof xValues[0] == "string" ? xValues : undefined,
ticktext: typeof xValues[0] == "string" ? truncatedXLabels : undefined,
tickvals: typeof xValues[0] == 'string' ? xValues : undefined,
ticktext: typeof xValues[0] == 'string' ? truncatedXLabels : undefined,
},
yaxis: {
......@@ -612,24 +612,24 @@ export const preparePlotData = (
zeroline: false,
tickfont: sharedTickFont,
title: {
text: showAxis ? (yAxisLabel ? yAxisLabel : "") : "",
text: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
standoff: 30,
},
tickvals: typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line") ? yValues : undefined,
ticktext: typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line") ? truncatedYLabels : undefined,
tickvals: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? yValues : undefined,
ticktext: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? truncatedYLabels : undefined,
},
font: {
family: "Inter",
family: 'Inter',
size: 12,
color: "#374151",
color: '#374151',
},
hoverlabel: {
bgcolor: "rgba(255, 255, 255, 0.8)",
bordercolor: "rgba(0, 0, 0, 0.2)",
bgcolor: 'rgba(255, 255, 255, 0.8)',
bordercolor: 'rgba(0, 0, 0, 0.2)',
font: {
family: "monospace",
family: 'monospace',
size: 14,
color: "#374151",
color: '#374151',
},
},
};
......@@ -639,21 +639,21 @@ export const preparePlotData = (
export const getAttributeValues = (
query: GraphQueryResultFromBackend,
selectedEntity: string,
attributeKey: string | number | undefined
attributeKey: string | number | undefined,
): any[] => {
if (!selectedEntity || !attributeKey) {
return [];
}
if (attributeKey == " ") {
if (attributeKey == ' ') {
return [];
}
return query.nodes
.filter((item) => item.label === selectedEntity)
.map((item) => {
.filter(item => item.label === selectedEntity)
.map(item => {
// 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]
: "NoData";
: 'NoData';
});
};