-
Notifications
You must be signed in to change notification settings - Fork 22
1002 data export wrong data types for timestamps #1064
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
1002 data export wrong data types for timestamps #1064
Conversation
…hart image generation using uPlot. Scatter chart with ExcelJs did not work because of limited support and xlsx-chart creates an entirely new excel file for chart generation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR addresses issue #1002 regarding incorrect data types for timestamps in Excel exports. The changes consolidate redundant sheets into a unified format, add a new "Auswertung" (Analysis) sheet with embedded chart visualization using uPlot, and fix sheet naming conventions.
Changes:
- Converted
exportGraphsToExcelfrom synchronous to async to support chart image generation - Merged separate data and stats sheets into combined sheets with side-by-side layout
- Added new "Auswertung" sheet with embedded chart images and legends generated via uPlot
- Improved sheet naming logic based on graph title, series title, and unit type
| import { TimeSeries, seriesToUPlotData } from "@/lib/timeseries"; | ||
| import { renderUnitSymbol, Unit } from "@/control/units"; | ||
| import { GraphConfig, SeriesData, GraphLine } from "./types"; | ||
| import { useLogsStore } from "@/stores/logsStore"; |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import of useLogsStore is at the module level, which is correct, but this should not be used directly in non-React context. The call on line 440 uses useLogsStore.getState() which is the correct pattern for using Zustand stores outside of React components. However, this creates a tight coupling between the export utility and the logs store. Consider passing the relevant logs as a parameter to make the function more testable and decoupled.
| // For temperature/power graphs (Nozzle, Front, Middle, Back, Total, Motor) | ||
| if (["Nozzle", "Front", "Middle", "Back"].includes(seriesTitle)) { | ||
| if (unitSymbol === "°C") { | ||
| sheetName = `${seriesTitle} Temp`; | ||
| } else if (unitSymbol === "W") { | ||
| sheetName = `${seriesTitle} Watt`; | ||
| } else { | ||
| sheetName = `${seriesTitle}`; | ||
| } | ||
| } else if (seriesTitle === "Total") { | ||
| if (unitSymbol === "W") { | ||
| sheetName = "Total Watt"; | ||
| } else { | ||
| sheetName = "Total"; | ||
| } | ||
| } else if (seriesTitle === "Motor") { | ||
| sheetName = "Motor"; | ||
| } | ||
| // For current/pressure/speed graphs (Series 1, 2, 3 fallback) | ||
| else if (seriesTitle === "Series 1") { | ||
| if (unitSymbol === "A") { | ||
| sheetName = "Ampere"; | ||
| } else if (unitSymbol === "bar") { | ||
| sheetName = "Bar"; | ||
| } else if (unitSymbol === "rpm" || unitSymbol === "1/min") { | ||
| sheetName = "Rpm"; | ||
| } else { | ||
| sheetName = "Series 1"; | ||
| } | ||
| } else if (seriesTitle === "Series 2") { | ||
| if (unitSymbol === "bar") { | ||
| sheetName = "Bar"; | ||
| } else if (unitSymbol === "rpm" || unitSymbol === "1/min") { | ||
| sheetName = "Rpm"; | ||
| } else { | ||
| sheetName = "Series 2"; | ||
| } | ||
| } else if (seriesTitle === "Series 3") { | ||
| if (unitSymbol === "rpm" || unitSymbol === "1/min") { | ||
| sheetName = "Rpm"; | ||
| } else { | ||
| sheetName = "Series 3"; | ||
| } | ||
| } | ||
| // For laser measurement graphs | ||
| else if (["Diameter", "X-Diameter", "Y-Diameter", "Roundness"].includes(seriesTitle)) { | ||
| sheetName = seriesTitle; | ||
| } | ||
| // Default fallback | ||
| else { | ||
| sheetName = seriesTitle; | ||
| } |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generateSheetName function has a complex series of if-else conditions (lines 736-787) that could be simplified using a more structured approach. Consider using a lookup table or strategy pattern to map (seriesTitle, unitSymbol) pairs to sheet names, which would improve readability and maintainability.
| sheetData.push(["Software Information", "", "", "", "", "", ""]); | ||
| sheetData.push(["Software", "QiTech Control", "", "", "", "", ""]); | ||
| sheetData.push(["Version", "1.0.0", "", "", "", "", ""]); | ||
| sheetData.push([ | ||
| "Export Date", | ||
| new Date().toLocaleString("de-DE"), | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
|
|
||
| // Comment statistics | ||
| sheetData.push(Array(columns.length).fill("")); // Empty row | ||
| sheetData.push(["Comment Statistics", "", "", "", "", "", ""]); | ||
| sheetData.push([ | ||
| "Total Comments", | ||
| relevantComments.length.toString(), | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
|
|
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the createAuswertungSheet function, metadata rows (lines 600-611) are hardcoded with seven empty string elements, but the actual number of columns may vary based on availableColumns. This inconsistency could cause layout issues in the Excel sheet. Use Array(columns.length).fill("") to ensure the correct number of columns, or destructure the first element and fill the rest dynamically.
| sheetData.push(["Software Information", "", "", "", "", "", ""]); | |
| sheetData.push(["Software", "QiTech Control", "", "", "", "", ""]); | |
| sheetData.push(["Version", "1.0.0", "", "", "", "", ""]); | |
| sheetData.push([ | |
| "Export Date", | |
| new Date().toLocaleString("de-DE"), | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| // Comment statistics | |
| sheetData.push(Array(columns.length).fill("")); // Empty row | |
| sheetData.push(["Comment Statistics", "", "", "", "", "", ""]); | |
| sheetData.push([ | |
| "Total Comments", | |
| relevantComments.length.toString(), | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| const softwareInfoTitleRow = Array(columns.length).fill(""); | |
| softwareInfoTitleRow[0] = "Software Information"; | |
| sheetData.push(softwareInfoTitleRow); | |
| const softwareRow = Array(columns.length).fill(""); | |
| softwareRow[0] = "Software"; | |
| softwareRow[1] = "QiTech Control"; | |
| sheetData.push(softwareRow); | |
| const versionRow = Array(columns.length).fill(""); | |
| versionRow[0] = "Version"; | |
| versionRow[1] = "1.0.0"; | |
| sheetData.push(versionRow); | |
| const exportDateRow = Array(columns.length).fill(""); | |
| exportDateRow[0] = "Export Date"; | |
| exportDateRow[1] = new Date().toLocaleString("de-DE"); | |
| sheetData.push(exportDateRow); | |
| // Comment statistics | |
| sheetData.push(Array(columns.length).fill("")); // Empty row | |
| const commentStatsTitleRow = Array(columns.length).fill(""); | |
| commentStatsTitleRow[0] = "Comment Statistics"; | |
| sheetData.push(commentStatsTitleRow); | |
| const totalCommentsRow = Array(columns.length).fill(""); | |
| totalCommentsRow[0] = "Total Comments"; | |
| totalCommentsRow[1] = relevantComments.length.toString(); | |
| sheetData.push(totalCommentsRow); |
| // Wrap to next line if needed | ||
| if (legendX > 1100 && index < allSheetData.length - 1) { | ||
| legendX = 20; | ||
| legendY += 20; | ||
| } |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The legend wrapping logic on lines 278-282 calculates when to wrap to the next line but doesn't properly check if the canvas height is sufficient for multiple rows of legend items. If there are many series, the legend could overflow the 50px canvas height, causing items to be cut off. Consider dynamically calculating the required height based on the number of items and rows needed.
| const plot = new uPlot(opts, chartData as uPlot.AlignedData, container); | ||
|
|
||
| // Wait for render | ||
| await new Promise((resolve) => setTimeout(resolve, 100)); | ||
|
|
||
| // Get the canvas element | ||
| const canvas = container.querySelector("canvas"); | ||
| if (!canvas) { | ||
| document.body.removeChild(container); | ||
| return null; | ||
| } | ||
|
|
||
| // Get the image data directly from uPlot's canvas | ||
| const imageData = canvas.toDataURL("image/png"); | ||
|
|
||
| // Clean up | ||
| plot.destroy(); | ||
| document.body.removeChild(container); | ||
|
|
||
| // Return base64 data (remove the data:image/png;base64, prefix) | ||
| return imageData.split(",")[1]; | ||
| } catch (error) { | ||
| console.error("Error generating chart image:", error); | ||
| return null; | ||
| } |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the generateChartImage function, if the canvas element is not found, the container is removed from the document but the uPlot instance is never created, so there's no plot.destroy() call needed. However, if an error occurs after the plot is created but before the canvas is captured, the cleanup code won't execute and the plot won't be destroyed properly. Consider using a try-finally block to ensure proper cleanup of the plot instance and container removal.
| const columnMapping: { [key: string]: string } = { | ||
| Bar: "Bar", | ||
| Rpm: "rpm", | ||
| "Front Temp": "Temp Front", | ||
| "Middle Temp": "Temp Middle", | ||
| "Back Temp": "Temp Back", | ||
| "Nozzle Temp": "Temp Nozzle", | ||
| "Total Watt": "Total W", | ||
| "Front Watt": "W Front", | ||
| "Middle Watt": "W Middle", | ||
| "Back Watt": "W Back", | ||
| "Nozzle Watt": "W Nozzle", | ||
| }; | ||
|
|
||
| // Determine columns in desired order | ||
| const desiredOrder = [ | ||
| "Bar", | ||
| "Rpm", | ||
| "Temp Front", | ||
| "Temp Middle", | ||
| "Temp Back", | ||
| "Temp Nozzle", | ||
| "Total W", | ||
| "W Front", | ||
| "W Middle", | ||
| "W Back", | ||
| "W Nozzle", | ||
| ]; | ||
|
|
||
| // Find which columns we actually have data for | ||
| const availableColumns: string[] = []; | ||
| desiredOrder.forEach((colName) => { | ||
| const sheetName = Object.entries(columnMapping).find( | ||
| ([, label]) => label === colName | ||
| )?.[0]; | ||
| if (sheetName && allSheetData.some((d) => d.sheetName === sheetName)) { | ||
| availableColumns.push(colName); | ||
| } | ||
| }); |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The column mapping on lines 467-479 creates a bidirectional mapping that's later searched in reverse (line 499-501). This is inefficient and error-prone. Consider creating a proper reverse mapping or restructuring the data to avoid the need for repeated Object.entries() calls and array finds in the loop.
| export async function exportGraphsToExcel( | ||
| graphDataMap: Map<string, () => GraphExportData | null>, | ||
| groupId: string, | ||
| ): void { | ||
| ): Promise<void> { |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function changed from synchronous to async but the handleExport callback in useGraphSync.ts (line 129) doesn't await the result. This means errors from the async function won't be properly caught by the caller. Consider updating the calling code to handle the promise returned by exportGraphsToExcel, or at least add .catch() to handle rejections.
| // Wait for render | ||
| await new Promise((resolve) => setTimeout(resolve, 100)); | ||
|
|
||
| // Get the canvas element | ||
| const canvas = container.querySelector("canvas"); |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded timeout of 100ms on line 383 is used to wait for the uPlot chart to render, but this is a brittle approach. Rendering times can vary depending on system load and data complexity. Consider using a more robust mechanism like checking if the canvas is populated or using requestAnimationFrame to ensure the chart is fully rendered before capturing the image.
| // Wait for render | |
| await new Promise((resolve) => setTimeout(resolve, 100)); | |
| // Get the canvas element | |
| const canvas = container.querySelector("canvas"); | |
| // Wait for the chart canvas to be rendered and populated. | |
| const waitForCanvasRender = ( | |
| parent: HTMLElement, | |
| timeoutMs = 1000, | |
| ): Promise<HTMLCanvasElement | null> => { | |
| return new Promise((resolve) => { | |
| const start = performance.now(); | |
| const check = () => { | |
| const canvas = parent.querySelector("canvas") as HTMLCanvasElement | null; | |
| if (canvas && canvas.width > 0 && canvas.height > 0) { | |
| resolve(canvas); | |
| return; | |
| } | |
| if (performance.now() - start >= timeoutMs) { | |
| resolve(canvas); | |
| return; | |
| } | |
| requestAnimationFrame(check); | |
| }; | |
| requestAnimationFrame(check); | |
| }); | |
| }; | |
| const canvas = await waitForCanvasRender(container); |
| sheetData.push([ | ||
| "2. Insert > Chart > Scatter Chart with Straight Lines and Markers", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
| sheetData.push([ | ||
| "3. X-axis: Time (seconds), Y-axis: All measurement columns", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
| sheetData.push([ | ||
| "4. Set X-axis range: 0 to " + maxSeconds, | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
| sheetData.push([ | ||
| "5. Set Y-axis range: 0 to 1000", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
| sheetData.push([ | ||
| "6. Position legend at bottom", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
| sheetData.push([ | ||
| "7. Chart Title: " + groupId + " - " + timeRangeTitle, | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| "", | ||
| ]); | ||
|
|
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chart instructions section (lines 644-718) creates hardcoded array entries filled with empty strings. This approach is fragile and doesn't properly utilize the columns.length variable for consistent sizing. Consider simplifying this by creating properly sized arrays programmatically or by using a helper function to avoid repetition.
| sheetData.push([ | |
| "2. Insert > Chart > Scatter Chart with Straight Lines and Markers", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| sheetData.push([ | |
| "3. X-axis: Time (seconds), Y-axis: All measurement columns", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| sheetData.push([ | |
| "4. Set X-axis range: 0 to " + maxSeconds, | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| sheetData.push([ | |
| "5. Set Y-axis range: 0 to 1000", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| sheetData.push([ | |
| "6. Position legend at bottom", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| sheetData.push([ | |
| "7. Chart Title: " + groupId + " - " + timeRangeTitle, | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| "", | |
| ]); | |
| const instructionColumnCount = sheetData[0]?.length ?? 1; | |
| const makeInstructionRow = (text: string): (string | number)[] => { | |
| const row = new Array(instructionColumnCount).fill(""); | |
| row[0] = text; | |
| return row; | |
| }; | |
| sheetData.push( | |
| makeInstructionRow( | |
| "2. Insert > Chart > Scatter Chart with Straight Lines and Markers", | |
| ), | |
| ); | |
| sheetData.push( | |
| makeInstructionRow( | |
| "3. X-axis: Time (seconds), Y-axis: All measurement columns", | |
| ), | |
| ); | |
| sheetData.push( | |
| makeInstructionRow("4. Set X-axis range: 0 to " + maxSeconds), | |
| ); | |
| sheetData.push( | |
| makeInstructionRow("5. Set Y-axis range: 0 to 1000"), | |
| ); | |
| sheetData.push( | |
| makeInstructionRow("6. Position legend at bottom"), | |
| ); | |
| sheetData.push( | |
| makeInstructionRow( | |
| "7. Chart Title: " + groupId + " - " + timeRangeTitle, | |
| ), | |
| ); |
|
|
||
| // Determine column header based on sheet name and unit | ||
| // Extract base name from sheet name (e.g., "Nozzle Temp, Watt" -> "Nozzle") | ||
| let baseName = sheetName; |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial value of baseName is unused, since it is always overwritten.
| let baseName = sheetName; | |
| let baseName: string; |
|
@TheBest6337 pleaase review, and give me an update what you think about styl & ability |
|
@Chitransh31 please fix the build issues that come from code formatting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im not a fan of this implementation. Its too much code that is A not necesarry or an duplicate of something which could have been done only once. B - too much hardcoded stuff... like the Control Version or all the if cases of string for the machines that can easily break because no one expects this stuff in an excel export.
You should use more object based programming, I think that it should be possible to remove most of the hardcoded stuff and get the data from the current graph implementation, which would be way cleaner. Some functions also seem to be pretty long and could be made into their own file, this would make an cleaner implementation and you need less duplicate code. If you need to create small changes to an machine file because of that you can do so, the namespaces are there for a reason or you can get the data from the graph page...
Copilots suggestions are also somewhat true..
| const formatDateTime = (date: Date) => | ||
| date.toLocaleString("de-DE", { | ||
| day: "2-digit", | ||
| month: "2-digit", | ||
| year: "numeric", | ||
| hour: "2-digit", | ||
| minute: "2-digit", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this function exist so often duplicat code is never good
| // Find a good position for the image (after the data and metadata) | ||
| const lastRow = auswertungWorksheet.rowCount; | ||
|
|
||
| auswertungWorksheet.addImage(chartImageId, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only use englisch, no german english mix!
| // Color mapping based on data type to match reference chart | ||
| function getSeriesColor(name: string): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you not using the data from the machine itself and instead hardcode fixed values?? If we have a new machine or change the naming you dont want to change something here. It has to be more modular and less hardcoded.
| // Pressure and RPM - blue shades from reference | ||
| if (lowerName === "bar") return "#2980b9"; // Blue | ||
| if (lowerName === "rpm" || lowerName.includes("rpm")) return "#16a085"; // Teal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same issue
| } | ||
|
|
||
| // Create Auswertung (Analysis) sheet with combined data from all sheets | ||
| async function createAuswertungSheet( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
German...
| sheetData.push(["Software Information", "", "", "", "", "", ""]); | ||
| sheetData.push(["Software", "QiTech Control", "", "", "", "", ""]); | ||
| sheetData.push(["Version", "1.0.0", "", "", "", "", ""]); | ||
| sheetData.push([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why tf is this hardcoded??? The Version should never be hardcoded you can get the current installed version in a similar way the ChooseVersionPage.tsx does it.
| // For temperature/power graphs (Nozzle, Front, Middle, Back, Total, Motor) | ||
| if (["Nozzle", "Front", "Middle", "Back"].includes(seriesTitle)) { | ||
| if (unitSymbol === "°C") { | ||
| sheetName = `${seriesTitle} Temp`; | ||
| } else if (unitSymbol === "W") { | ||
| sheetName = `${seriesTitle} Watt`; | ||
| } else { | ||
| sheetName = `${seriesTitle}`; | ||
| } | ||
| } else if (seriesTitle === "Total") { | ||
| if (unitSymbol === "W") { | ||
| sheetName = "Total Watt"; | ||
| } else { | ||
| sheetName = "Total"; | ||
| } | ||
| } else if (seriesTitle === "Motor") { | ||
| sheetName = "Motor"; | ||
| } | ||
| // For current/pressure/speed graphs (Series 1, 2, 3 fallback) | ||
| else if (seriesTitle === "Series 1") { | ||
| if (unitSymbol === "A") { | ||
| sheetName = "Ampere"; | ||
| } else if (unitSymbol === "bar") { | ||
| sheetName = "Bar"; | ||
| } else if (unitSymbol === "rpm" || unitSymbol === "1/min") { | ||
| sheetName = "Rpm"; | ||
| } else { | ||
| sheetName = "Series 1"; | ||
| } | ||
| } else if (seriesTitle === "Series 2") { | ||
| if (unitSymbol === "bar") { | ||
| sheetName = "Bar"; | ||
| } else if (unitSymbol === "rpm" || unitSymbol === "1/min") { | ||
| sheetName = "Rpm"; | ||
| } else { | ||
| sheetName = "Series 2"; | ||
| } | ||
| } else if (seriesTitle === "Series 3") { | ||
| if (unitSymbol === "rpm" || unitSymbol === "1/min") { | ||
| sheetName = "Rpm"; | ||
| } else { | ||
| sheetName = "Series 3"; | ||
| } | ||
| } | ||
| // For laser measurement graphs | ||
| else if (["Diameter", "X-Diameter", "Y-Diameter", "Roundness"].includes(seriesTitle)) { | ||
| sheetName = seriesTitle; | ||
| } | ||
| // Default fallback | ||
| else { | ||
| sheetName = seriesTitle; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a fan of having this harcoded here
| new Date().toLocaleString("de-DE", { | ||
| year: "numeric", | ||
| month: "2-digit", | ||
| day: "2-digit", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| second: "2-digit", | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
duplicate
| statsRows.push([ | ||
| "Time Range Start", | ||
| firstDate.toLocaleString("de-DE", { | ||
| year: "numeric", | ||
| month: "2-digit", | ||
| day: "2-digit", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| second: "2-digit", | ||
| }), | ||
| ]); | ||
| statsRows.push([ | ||
| "Time Range End", | ||
| lastDate.toLocaleString("de-DE", { | ||
| year: "numeric", | ||
| month: "2-digit", | ||
| day: "2-digit", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| second: "2-digit", | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again
| const formattedDate = date.toLocaleString("de-DE", { | ||
| year: "numeric", | ||
| month: "2-digit", | ||
| day: "2-digit", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| second: "2-digit", | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dupe
Fixed the sheets' names, merged the redundant sheets and created a new Auswertung sheet with chart image generation using uPlot. Scatter chart with ExcelJs did not work because of limited chart support and xlsx-chart creates an entirely new excel file for chart generation. Fixed colours in the chart and added legends below the chart for the parameters.