window.timelineGenerator = { generate(items, cfg, template) { const monthsRange = (cfg.settings && cfg.settings.timelineMonths) || 18; // Determine time window from earliest due date const sorted = [...items].sort((a, b) => a.due - b.due); const minDue = sorted[0].due; const start = new Date(minDue.getFullYear(), minDue.getMonth(), 1); const end = new Date(start); end.setMonth(end.getMonth() + monthsRange); // Build months array const months = []; const cursor = new Date(start); while (cursor <= end) { months.push(new Date(cursor)); cursor.setMonth(cursor.getMonth() + 1); } // Group items by lane const laneMap = new Map(); for (const it of items) { const laneName = it.lane || "Ohne Epic"; if (!laneMap.has(laneName)) laneMap.set(laneName, []); laneMap.get(laneName).push(it); } const laneNames = Array.from(laneMap.keys()).sort((a, b) => a.localeCompare(b, "de") ); // Layout constants const left = 220; const top = 140; const monthWidth = 120; const laneHeight = 80; const laneGap = 16; // Check if template uses new template-based approach if (template && this.hasTemplateElements(template)) { return this.generateFromTemplates(items, cfg, template, { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap }); } // Use hardcoded generation for backward compatibility return this.generateHardcoded(items, cfg, template, { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap }); }, escapeXml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }, // Check if template has template elements hasTemplateElements(template) { return template.includes('id="month-template"') && template.includes('id="lane-template"') && template.includes('id="task-template"'); }, // Extract a template element from SVG extractTemplate(template, id) { const regex = new RegExp(`]*>([\\s\\S]*?)`, 'i'); const match = template.match(regex); return match ? match[0] : null; }, // Replace placeholders in template string replacePlaceholders(template, values) { let result = template; for (const [key, value] of Object.entries(values)) { result = result.replace(new RegExp(`{{${key}}}`, 'g'), value); } return result; }, // Generate SVG using template-based approach generateFromTemplates(items, cfg, template, layout) { const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout; // Extract template elements const monthTemplate = this.extractTemplate(template, 'month-template'); const laneTemplate = this.extractTemplate(template, 'lane-template'); const taskTemplate = this.extractTemplate(template, 'task-template'); if (!monthTemplate || !laneTemplate || !taskTemplate) { console.warn('Template elements not found, falling back to hardcoded generation'); return this.generateHardcoded(items, cfg, template, layout); } const monthLabelY = 90; const gridTop = top - 20; const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40; // Generate months using template let monthGraphics = ""; months.forEach((m, i) => { const x = left + i * monthWidth; const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" }); const monthValues = { MONTH_X: x, GRID_TOP: gridTop, GRID_BOTTOM: gridBottom, MONTH_X_OFFSET: x - 30, MONTH_LABEL_Y_OFFSET: monthLabelY - 20, MONTH_TEXT_X: x + 4, MONTH_LABEL_Y: monthLabelY, MONTH_LABEL: label, MONTH_SEP_X: x - 1, GRID_HEIGHT: gridBottom - gridTop }; let monthElement = this.replacePlaceholders(monthTemplate, monthValues); // Remove display:none and id to make element visible monthElement = monthElement.replace(/style="display:\s*none;?"/, ''); monthElement = monthElement.replace(/id="month-template"/, ''); monthGraphics += monthElement; }); // Helper to compute month index function monthIndexForDate(d) { return (d.getFullYear() - start.getFullYear()) * 12 + (d.getMonth() - start.getMonth()); } // Generate lanes using template let laneBlocks = ""; laneNames.forEach((laneName, laneIdx) => { const laneY = top + laneIdx * (laneHeight + laneGap); const laneWidth = left + months.length * monthWidth; const laneValues = { LANE_X: 40, LANE_Y: laneY - 24, LANE_WIDTH: laneWidth, LANE_HEIGHT: laneHeight, LABEL_X: 56, LABEL_Y: laneY - 4, LANE_NAME: this.escapeXml(laneName) }; let laneElement = this.replacePlaceholders(laneTemplate, laneValues); laneElement = laneElement.replace(/style="display:\s*none;?"/, ''); laneElement = laneElement.replace(/id="lane-template"/, ''); laneBlocks += laneElement; // Generate tasks for this lane const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due); laneItems.forEach((it, idx) => { const mi = monthIndexForDate(it.due); const clampedMi = Math.max(0, Math.min(months.length - 1, mi)); const cx = left + clampedMi * monthWidth + monthWidth * 0.5; const cy = laneY + 10 + idx * 18; const taskValues = { TASK_X: cx, TASK_Y: cy, TEXT_X: cx + 12, TEXT_Y: cy + 4, TASK_ID: this.escapeXml(it.id || ""), TASK_TITLE: this.escapeXml(it.title || "") }; let taskElement = this.replacePlaceholders(taskTemplate, taskValues); taskElement = taskElement.replace(/style="display:\s*none;?"/, ''); taskElement = taskElement.replace(/id="task-template"/, ''); laneBlocks += taskElement; }); }); // Calculate dimensions const height = top + laneNames.length * (laneHeight + laneGap) + 80; const width = left + months.length * monthWidth + 100; // Replace placeholders and inject calculated dimensions let processedTemplate = template .replace("{{MONTHS}}", monthGraphics) .replace("{{LANES}}", laneBlocks); // Add width and height attributes to the SVG element processedTemplate = processedTemplate.replace( /]*?)>/, `` ); return processedTemplate; }, // Original hardcoded generation (for backward compatibility) generateHardcoded(items, cfg, template, layout) { const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout; // This is the original hardcoded generation that was in the generate() method // Month grid (labels + vertical lines) let monthGraphics = ""; const monthLabelY = 90; const gridTop = top - 20; const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40; months.forEach((m, i) => { const x = left + i * monthWidth; const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" }); // Enhanced styling when using external template if (template && template.includes('Enhanced')) { // Determine color scheme based on template content const isBlueTheme = template.includes('My Project'); const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659'; const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b'; // More prominent month indicators for external template monthGraphics += ``; monthGraphics += ``; monthGraphics += `${label}`; // Add month separator if (i > 0) { monthGraphics += ``; } } else { // Default styling monthGraphics += ``; monthGraphics += `${label}`; } }); // Helper to compute month index function monthIndexForDate(d) { return (d.getFullYear() - start.getFullYear()) * 12 + (d.getMonth() - start.getMonth()); } // Lane blocks let laneBlocks = ""; laneNames.forEach((laneName, laneIdx) => { const laneY = top + laneIdx * (laneHeight + laneGap); // Enhanced styling when using external template if (template && template.includes('Enhanced')) { // Determine color scheme based on template content const isBlueTheme = template.includes('My Project'); const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659'; const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b'; // More subtle lane borders for enhanced template laneBlocks += ``; // Enhanced lane label laneBlocks += `${this.escapeXml(laneName)}`; } else { // Default lane styling laneBlocks += ``; // Default lane label laneBlocks += `${this.escapeXml(laneName)}`; } const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due); laneItems.forEach((it, idx) => { const mi = monthIndexForDate(it.due); const clampedMi = Math.max(0, Math.min(months.length - 1, mi)); const cx = left + clampedMi * monthWidth + monthWidth * 0.5; const cy = laneY + 10 + idx * 18; // Enhanced task item styling for external template if (template && template.includes('Enhanced')) { // Determine color scheme based on template content const isBlueTheme = template.includes('My Project'); const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659'; const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b'; const darkColor = isBlueTheme ? '#1e40af' : '#1e5a3d'; laneBlocks += ``; laneBlocks += ` ${this.escapeXml(it.id || "")}: ${this.escapeXml(it.title || "")} `; } else { // Default task item styling laneBlocks += ``; laneBlocks += ` ${this.escapeXml(it.id || "")}: ${this.escapeXml(it.title || "")} `; } }); }); if (template && template.includes("{{MONTHS}}") && template.includes("{{LANES}}")) { // Calculate dimensions for template const height = top + laneNames.length * (laneHeight + laneGap) + 80; const width = left + months.length * monthWidth + 100; // Replace placeholders and inject calculated dimensions let processedTemplate = template .replace("{{MONTHS}}", monthGraphics) .replace("{{LANES}}", laneBlocks); // Add width and height attributes to the SVG element processedTemplate = processedTemplate.replace( /]*?)>/, `` ); return processedTemplate; } // Fallback: embed directly in simple SVG const height = top + laneNames.length * (laneHeight + laneGap) + 80; const width = left + months.length * monthWidth + 100; return ` ${monthGraphics} ${laneBlocks} `; } };