Skip to content

Conversation

@Chitransh31
Copy link

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.

Saurabh Bagade added 3 commits January 22, 2026 00:10
…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
Copilot AI review requested due to automatic review settings January 21, 2026 23:27
Copy link
Contributor

Copilot AI left a 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 exportGraphsToExcel from 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";
Copy link

Copilot AI Jan 21, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +736 to +787
// 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;
}
Copy link

Copilot AI Jan 21, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +600 to +625
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(),
"",
"",
"",
"",
"",
]);

Copy link

Copilot AI Jan 21, 2026

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +278 to +282
// Wrap to next line if needed
if (legendX > 1100 && index < allSheetData.length - 1) {
legendX = 20;
legendY += 20;
}
Copy link

Copilot AI Jan 21, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +380 to +404
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;
}
Copy link

Copilot AI Jan 21, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +467 to +505
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);
}
});
Copy link

Copilot AI Jan 21, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +30
export async function exportGraphsToExcel(
graphDataMap: Map<string, () => GraphExportData | null>,
groupId: string,
): void {
): Promise<void> {
Copy link

Copilot AI Jan 21, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +382 to +386
// Wait for render
await new Promise((resolve) => setTimeout(resolve, 100));

// Get the canvas element
const canvas = container.querySelector("canvas");
Copy link

Copilot AI Jan 21, 2026

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
Comment on lines +665 to +719
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,
"",
"",
"",
"",
"",
"",
]);

Copy link

Copilot AI Jan 21, 2026

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.

Suggested change
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,
),
);

Copilot uses AI. Check for mistakes.

// Determine column header based on sheet name and unit
// Extract base name from sheet name (e.g., "Nozzle Temp, Watt" -> "Nozzle")
let baseName = sheetName;
Copy link

Copilot AI Jan 21, 2026

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.

Suggested change
let baseName = sheetName;
let baseName: string;

Copilot uses AI. Check for mistakes.
@MilanVDB
Copy link
Collaborator

@TheBest6337 pleaase review, and give me an update what you think about styl & ability

@MilanVDB MilanVDB requested a review from TheBest6337 January 22, 2026 07:11
@TheBest6337
Copy link
Member

@Chitransh31 please fix the build issues that come from code formatting

Copy link
Member

@TheBest6337 TheBest6337 left a 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..

Comment on lines +143 to +149
const formatDateTime = (date: Date) =>
date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
Copy link
Member

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, {
Copy link
Member

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!

Comment on lines +216 to +217
// Color mapping based on data type to match reference chart
function getSeriesColor(name: string): string {
Copy link
Member

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.

Comment on lines +233 to +235
// Pressure and RPM - blue shades from reference
if (lowerName === "bar") return "#2980b9"; // Blue
if (lowerName === "rpm" || lowerName.includes("rpm")) return "#16a085"; // Teal
Copy link
Member

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

German...

Comment on lines +600 to +603
sheetData.push(["Software Information", "", "", "", "", "", ""]);
sheetData.push(["Software", "QiTech Control", "", "", "", "", ""]);
sheetData.push(["Version", "1.0.0", "", "", "", "", ""]);
sheetData.push([
Copy link
Member

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.

Comment on lines +736 to +787
// 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;
}
Copy link
Member

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

Comment on lines +867 to +874
new Date().toLocaleString("de-DE", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate

Comment on lines +883 to +904
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",
}),
]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again

Comment on lines +986 to +993
const formattedDate = date.toLocaleString("de-DE", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dupe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants