A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(xlsx): alternating row-band shading in Gantt plot area

Paint alternating white / light-gray horizontal bands behind the Gantt
bars, one band per category, so task rows are visually separated in the
exported workbook. Implemented as a hard-step gradient fill on the plot
area (<c:spPr>) with duplicated stop positions per band and a 90deg
top-to-bottom angle aligned to the catAx maxMin orientation.

Russ T. Fugal (Jun 4, 2026, 5:48 PM -0600) bd07df1b 58216c9a

+36
+32
src/xlsx/XlsxWriter.ts
··· 288 288 return zip.generateAsync({ type: "arraybuffer", compression: "DEFLATE" }); 289 289 } 290 290 291 + /** 292 + * Builds a gradient-fill `<c:spPr>` for the plot area that produces 293 + * alternating horizontal row bands (white / light gray), one band per 294 + * category. Each adjacent stop pair uses identical positions so the 295 + * gradient becomes a hard step transition. 296 + */ 297 + function buildPlotShadingXml(n: number): string { 298 + if (n <= 0) return ""; 299 + const EVEN = "FFFFFF"; 300 + const ODD = "F4F4F4"; 301 + const stops: string[] = []; 302 + for (let i = 0; i < n; i += 1) { 303 + const startPos = Math.round((i / n) * 100000); 304 + const endPos = Math.round(((i + 1) / n) * 100000); 305 + const color = i % 2 === 0 ? EVEN : ODD; 306 + stops.push(`<a:gs pos="${startPos}"><a:srgbClr val="${color}"/></a:gs>`); 307 + stops.push(`<a:gs pos="${endPos}"><a:srgbClr val="${color}"/></a:gs>`); 308 + } 309 + return ( 310 + "<c:spPr>" + 311 + '<a:gradFill rotWithShape="1">' + 312 + `<a:gsLst>${stops.join("")}</a:gsLst>` + 313 + // 5400000 = 90° in 60000ths of a degree = top-to-bottom gradient. 314 + // catAx uses orientation="maxMin", so band i=0 lands on the top row. 315 + '<a:lin ang="5400000" scaled="0"/>' + 316 + "</a:gradFill>" + 317 + "<a:ln><a:noFill/></a:ln>" + 318 + "</c:spPr>" 319 + ); 320 + } 321 + 291 322 function buildChartXml({ items, origin, end, title, dataSheetName }: InjectArgs): string { 292 323 const n = items.length; 293 324 const lastRow = n + 1; ··· 372 403 '<c:crossAx val="111111111"/>' + 373 404 '<c:majorUnit val="30"/>' + 374 405 "</c:valAx>" + 406 + buildPlotShadingXml(n) + 375 407 "</c:plotArea>" + 376 408 '<c:plotVisOnly val="1"/><c:dispBlanksAs val="gap"/>' + 377 409 "</c:chart></c:chartSpace>"
+4
test/xlsx.test.ts
··· 148 148 expect(chartXml).toContain('val="4472C4"'); 149 149 // Phase 2 color on the duration data points 150 150 expect(chartXml).toContain('val="ED7D31"'); 151 + // Plot area row shading: gradient with alternating white / light-gray bands. 152 + expect(chartXml).toContain("<a:gradFill"); 153 + expect(chartXml).toContain('val="FFFFFF"'); 154 + expect(chartXml).toContain('val="F4F4F4"'); 151 155 152 156 const drawingFile = zip.file("xl/drawings/drawing1.xml"); 153 157 expect(drawingFile).not.toBeNull();