diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..44b9cee3d325f3898b1f6350ba01be8658ca07e8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 140 +end_of_line = lf + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..1ced9beba06d663942e1896562cc6828a0bc97d3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 120, + "trailingComma": "all", + "singleQuote": true, + "tabWidth": 2, + "semi": true, + "jsxSingleQuote": false, + "quoteProps": "as-needed", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "avoid" +} diff --git a/bun.lockb b/bun.lockb index 4eb3dfd4c70e7f1bfd927fe0aa7a062f8d58152e..d671131c56372be563921e4e7d38461c9a77d221 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..48cc79349b89195d6b319ad69b3e2bf7e9b2497b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,29 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", + "react-hooks/exhaustive-deps": "off", + "react/display-name": "off", + }, + }, +); diff --git a/package.json b/package.json index 0bc5b33494d41456325ab3ae26c16ca069cfb54e..5091ba23a154aba3034a8f8616c129773769e7b9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "devDependencies": { "@types/bun": "latest", "@types/d3": "^7.4.3", - "@types/nodemailer": "^6.4.17" + "@types/nodemailer": "^6.4.17", + "@types/plotly.js": "^2.35.1" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/src/utils/insights.ts b/src/utils/insights.ts index b639f2f51136ee12f8c8def5cfef37dcdfbe1b2d..d80c793ca90f576e0699349929f86c992dc3fed1 100644 --- a/src/utils/insights.ts +++ b/src/utils/insights.ts @@ -1,10 +1,11 @@ -import type { PlotType } from 'plotly.js'; -import { scaleOrdinal, scaleQuantize } from 'd3'; -import { JSDOM } from 'jsdom'; -import svg2img from 'svg2img'; -import 'canvas'; +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"; + const dom = new JSDOM(); // @ts-ignore @@ -16,15 +17,16 @@ global.getComputedStyle = dom.window.getComputedStyle; global.Element = dom.window.Element; global.HTMLElement = dom.window.HTMLElement; -dom.window.HTMLCanvasElement.prototype.getContext = function () { return null; }; -dom.window.URL.createObjectURL = function () { return '' }; - -// @ts-ignore -const { newPlot } = await import('plotly.js'); +dom.window.HTMLCanvasElement.prototype.getContext = function () { + return null; +}; +dom.window.URL.createObjectURL = function () { + return ""; +}; export enum VariableType { - statistic = 'statistic', - visualization = 'visualization', + statistic = "statistic", + visualization = "visualization", } async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) { @@ -33,7 +35,6 @@ async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: return string.replace(regexp, () => replacements[i++]); } - export async function populateTemplate(html: string, result: GraphQueryResultMetaFromBackend, openVisualizationArray: any[]) { const regex = /\ *?{\{\ *?(\w*?):([\w ]*?)\ *?\}\}\ *?/gm; @@ -42,10 +43,10 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet 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} `; @@ -53,7 +54,7 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet 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"); } let xAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.xAxisLabel!); let yAxisData: (string | number)[] = []; @@ -71,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; @@ -87,7 +88,7 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet zAxisLabel, showAxis, groupBy, - stack, + stack ); const layout2 = { @@ -97,10 +98,11 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet title: activeVisualization.title, }; - 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); @@ -113,67 +115,64 @@ 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); }; // Grouping logic - const groupedData = xAxisData.reduce( - (acc, dateStr, index) => { - const date = parseDate(dateStr); - let groupKey: string; - - if (groupBy === 'yearly') { - groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012") - } 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') { - // Group by month, e.g., "2012-07" - 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(); - } + const groupedData = xAxisData.reduce((acc, dateStr, index) => { + const date = parseDate(dateStr); + let groupKey: string; + + if (groupBy === "yearly") { + groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012") + } 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") { + // Group by month, e.g., "2012-07" + 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(); + } - // Initialize the group if it doesn't exist - if (!acc[groupKey]) { - acc[groupKey] = additionalVariableData - ? typeof additionalVariableData[0] === 'number' - ? 0 // Initialize sum for numbers - : [] // Initialize array for strings - : 0; // Initialize count for no additional data - } + // Initialize the group if it doesn't exist + if (!acc[groupKey]) { + acc[groupKey] = additionalVariableData + ? typeof additionalVariableData[0] === "number" + ? 0 // Initialize sum for numbers + : [] // Initialize array for strings + : 0; // Initialize count for no additional data + } - // Aggregate additional variable if provided - if (additionalVariableData) { - if (typeof additionalVariableData[index] === 'number') { - acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number); - } else if (typeof additionalVariableData[index] === 'string') { - acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string]; - } - } else { - // Increment the count if no additionalVariableData - acc[groupKey] = (acc[groupKey] as number) + 1; + // Aggregate additional variable if provided + if (additionalVariableData) { + if (typeof additionalVariableData[index] === "number") { + acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number); + } else if (typeof additionalVariableData[index] === "string") { + acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string]; } + } else { + // Increment the count if no additionalVariableData + acc[groupKey] = (acc[groupKey] as number) + 1; + } - return acc; - }, - {} as Record<string, number | string[]>, - ); + return acc; + }, {} as Record<string, number | string[]>); // Extract grouped data into arrays for Plotly const xValuesGrouped = Object.keys(groupedData); @@ -198,17 +197,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,9 +217,9 @@ 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]); @@ -230,7 +229,7 @@ export const preparePlotData = ( colorDataZ = zAxisData?.map((item) => colorScale(item) || primaryColor); colorbar = { - title: 'Color Legend', + title: "Color Legend", tickvals: [zMin, zMax], ticktext: [`${zMin}`, `${zMax}`], }; @@ -243,10 +242,10 @@ export const preparePlotData = ( 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', + tickmode: "array", }; } } @@ -286,24 +285,21 @@ 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) { - const frequencyMap = xAxisData.reduce( - (acc, item) => { - acc[item] = (acc[item] || 0) + 1; - return acc; - }, - {} as Record<string, number>, - ); + case "bar": + if (typeof xAxisData[0] === "string" && groupBy == undefined) { + const frequencyMap = xAxisData.reduce((acc, item) => { + acc[item] = (acc[item] || 0) + 1; + return acc; + }, {} as Record<string, number>); const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]); @@ -318,36 +314,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 'markers', + mode: "markers" as "markers", marker: { color: zAxisData && zAxisData.length > 0 ? colorDataZ : primaryColor, size: 7, @@ -357,28 +353,28 @@ export const preparePlotData = ( xValues.length === 0 ? yValues.map((y) => `Y: ${y}`) : yValues.length === 0 - ? xValues.map((x) => `X: ${x}`) - : xValues.map((x, index) => { + ? 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 'lines', + mode: "lines" as "lines", 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) => { @@ -397,7 +393,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; @@ -410,7 +406,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); @@ -449,32 +445,29 @@ 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" } : {}), }; }); return traces; } else { - const frequencyMap = xAxisData.reduce( - (acc, item) => { - acc[item] = (acc[item] || 0) + 1; - return acc; - }, - {} as Record<string, number>, - ); + const frequencyMap = xAxisData.reduce((acc, item) => { + acc[item] = (acc[item] || 0) + 1; + return acc; + }, {} as Record<string, number>); const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]); @@ -483,12 +476,12 @@ 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>", }, ]; } @@ -511,7 +504,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 @@ -545,23 +538,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" } : {}), }; }); @@ -570,7 +563,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, @@ -578,10 +571,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 }, @@ -593,22 +586,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: { @@ -618,44 +611,48 @@ 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", }, }, }; return { plotData, layout }; }; -export const getAttributeValues = (query: GraphQueryResultFromBackend, selectedEntity: string, attributeKey: string | number | undefined): any[] => { +export const getAttributeValues = ( + query: GraphQueryResultFromBackend, + selectedEntity: string, + attributeKey: string | number | undefined +): any[] => { if (!selectedEntity || !attributeKey) { return []; } - if (attributeKey == ' ') { + if (attributeKey == " ") { return []; } return query.nodes .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"; }); -}; \ No newline at end of file +};