From 42636d0f40cbaffb386e9339df524933c67612fc Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Mon, 6 Jan 2025 19:39:09 +0100 Subject: [PATCH] fix: correct placement for plotly import --- .editorconfig | 15 +++ .prettierrc | 12 ++ bun.lockb | Bin 107362 -> 107724 bytes eslint.config.js | 29 ++++ package.json | 3 +- src/utils/insights.ts | 299 +++++++++++++++++++++--------------------- 6 files changed, 206 insertions(+), 152 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..44b9cee --- /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 0000000..1ced9be --- /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 GIT binary patch delta 15149 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 delta 14830 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 ZkQ`R9<v<R*zqfdGIkRq=w49A-@?Y)g4^998 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..48cc793 --- /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 0bc5b33..5091ba2 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 b639f2f..d80c793 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 +}; -- GitLab