diff --git a/TEMPLATE_V2_GUIDE.md b/TEMPLATE_V2_GUIDE.md
index 9b6c040..f2db0cc 100644
--- a/TEMPLATE_V2_GUIDE.md
+++ b/TEMPLATE_V2_GUIDE.md
@@ -12,7 +12,7 @@ A valid template-v2.svg file must contain:
2. **`` section** containing three required template elements:
- `` - Defines how each month column is rendered
- `` - Defines how each lane (epic/swimlane) is rendered
- - `` - Defines how each task item is rendered
+ - `` - Defines how each task item is rendered
3. **Main content area** with `{{MONTHS}}` and `{{LANES}}` placeholders
4. **Optional styling** (gradients, filters, patterns, etc.)
@@ -37,11 +37,11 @@ A valid template-v2.svg file must contain:
font-family="Arial" font-size="14" font-weight="bold" fill="#212121">{{LANE_NAME}}
-
-
-
+
+
+
{{TASK_ID}} {{TASK_TITLE}}
+ font-family="Arial" font-size="11" fill="#424242">{{ITEM_ID}} {{ITEM_TITLE}}
@@ -80,16 +80,38 @@ A valid template-v2.svg file must contain:
| `{{LABEL_Y}}` | Y position for lane label | 140 |
| `{{LANE_NAME}}` | Lane name text (XML-escaped) | "Development" |
-### Task Template Placeholders
+### Item Template Placeholders
| Placeholder | Description | Example Value |
|------------|-------------|---------------|
-| `{{TASK_X}}` | X position of task marker | 400 |
-| `{{TASK_Y}}` | Y position of task marker | 150 |
-| `{{TEXT_X}}` | X position for task text | 412 |
-| `{{TEXT_Y}}` | Y position for task text | 154 |
-| `{{TASK_ID}}` | Task ID (XML-escaped) | "T-123" |
-| `{{TASK_TITLE}}` | Task title (XML-escaped) | "Implement feature" |
+| `{{ITEM_X}}` | X position of item marker | 400 |
+| `{{ITEM_Y}}` | Y position of item marker | 150 |
+| `{{TEXT_X}}` | X position for item text | 412 |
+| `{{TEXT_Y}}` | Y position for item text | 154 |
+| `{{ITEM_ID}}` | Item ID (XML-escaped) | "T-123" |
+| `{{ITEM_TITLE}}` | Item title (XML-escaped) | "Implement feature" |
+
+**Dynamic Data Placeholders:**
+
+The generator automatically creates placeholders for **all properties** in your CSV data using the naming convention: `ITEM_{PROPERTY_UPPERCASE}`.
+
+For example, if your `fieldMapping` includes:
+```json
+{
+ "id": "ID",
+ "title": "Title",
+ "assignee": "Assignee",
+ "priority": "Priority"
+}
+```
+
+The following placeholders become available:
+- `{{ITEM_ID}}` - from the `id` field
+- `{{ITEM_TITLE}}` - from the `title` field
+- `{{ITEM_ASSIGNEE}}` - from the `assignee` field
+- `{{ITEM_PRIORITY}}` - from the `priority` field
+
+**Note:** The `due` field is used for positioning and is not available as a placeholder (use `{{MONTH_LABEL}}` for date display).
### Global Placeholders
@@ -107,7 +129,7 @@ These appear in the main template body (not in template elements):
1. Open template-v2.svg in Inkscape
2. Locate template elements in the Layers panel (inside ``)
3. Edit shapes, colors, fonts, etc. as needed
-4. **Important**: Keep `id="month-template"`, `id="lane-template"`, `id="task-template"` unchanged
+4. **Important**: Keep `id="month-template"`, `id="lane-template"`, `id="item-template"` unchanged
5. **Important**: Keep `{{PLACEHOLDER}}` text exactly as is - these are replaced at runtime
6. Save file (keep SVG format, avoid Inkscape-specific extensions)
@@ -190,8 +212,8 @@ Use SVG filters:
-
-
+
+
...
@@ -229,7 +251,7 @@ To change the overall layout, you would need to modify generator.js. Templates c
- Ensure template has all three required elements in ``:
- ``
- ``
- - ``
+ - ``
- Check that IDs are exactly as shown (case-sensitive)
- Verify elements are inside `` section
diff --git a/example/template-v2.svg b/example/template-v2.svg
index cc4a6c2..e2e053c 100644
--- a/example/template-v2.svg
+++ b/example/template-v2.svg
@@ -36,12 +36,12 @@
font-size="14" font-weight="700">{{LANE_NAME}}
-
-
+
- {{TASK_ID}}:
- {{TASK_TITLE}}
+ {{ITEM_ID}}:
+ {{ITEM_TITLE}}
diff --git a/generator.js b/generator.js
index 68cc9d0..d097b11 100644
--- a/generator.js
+++ b/generator.js
@@ -54,13 +54,13 @@ window.timelineGenerator = {
validateTemplate(template) {
const hasMonthTemplate = template.includes('id="month-template"');
const hasLaneTemplate = template.includes('id="lane-template"');
- const hasTaskTemplate = template.includes('id="task-template"');
+ const hasItemTemplate = template.includes('id="item-template"');
- if (!hasMonthTemplate || !hasLaneTemplate || !hasTaskTemplate) {
+ if (!hasMonthTemplate || !hasLaneTemplate || !hasItemTemplate) {
const missing = [];
if (!hasMonthTemplate) missing.push('month-template');
if (!hasLaneTemplate) missing.push('lane-template');
- if (!hasTaskTemplate) missing.push('task-template');
+ if (!hasItemTemplate) missing.push('item-template');
throw new Error(
`Template is missing required elements: ${missing.join(', ')}. ` +
@@ -97,7 +97,7 @@ window.timelineGenerator = {
// Extract template elements (will throw if not found)
const monthTemplate = this.extractTemplate(template, 'month-template');
const laneTemplate = this.extractTemplate(template, 'lane-template');
- const taskTemplate = this.extractTemplate(template, 'task-template');
+ const itemTemplate = this.extractTemplate(template, 'item-template');
const monthLabelY = 90;
const gridTop = top - 20;
@@ -156,7 +156,7 @@ window.timelineGenerator = {
laneElement = laneElement.replace(/id="lane-template"/, '');
laneBlocks += laneElement;
- // Generate tasks for this lane
+ // Generate items for this lane
const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due);
laneItems.forEach((it, idx) => {
const mi = monthIndexForDate(it.due);
@@ -164,19 +164,26 @@ window.timelineGenerator = {
const cx = left + clampedMi * monthWidth + monthWidth * 0.5;
const cy = laneY + 10 + idx * 18;
- const taskValues = {
- TASK_X: cx,
- TASK_Y: cy,
+ // Start with layout placeholders
+ const itemValues = {
+ ITEM_X: cx,
+ ITEM_Y: cy,
TEXT_X: cx + 12,
- TEXT_Y: cy + 4,
- TASK_ID: this.escapeXml(it.id || ""),
- TASK_TITLE: this.escapeXml(it.title || "")
+ TEXT_Y: cy + 4
};
- let taskElement = this.replacePlaceholders(taskTemplate, taskValues);
- taskElement = taskElement.replace(/style="display:\s*none;?"/, '');
- taskElement = taskElement.replace(/id="task-template"/, '');
- laneBlocks += taskElement;
+ // Dynamically add data placeholders from all item properties
+ for (const [key, value] of Object.entries(it)) {
+ if (key !== 'due') { // Skip due date as it's used for positioning
+ const placeholderName = `ITEM_${key.toUpperCase()}`;
+ itemValues[placeholderName] = this.escapeXml(value || "");
+ }
+ }
+
+ let itemElement = this.replacePlaceholders(itemTemplate, itemValues);
+ itemElement = itemElement.replace(/style="display:\s*none;?"/, '');
+ itemElement = itemElement.replace(/id="item-template"/, '');
+ laneBlocks += itemElement;
});
});
@@ -193,7 +200,7 @@ window.timelineGenerator = {
processedTemplate = processedTemplate
.replace(/]*>[\s\S]*?<\/g>/, '')
.replace(/]*>[\s\S]*?<\/g>/, '')
- .replace(/]*>[\s\S]*?<\/g>/, '');
+ .replace(/]*>[\s\S]*?<\/g>/, '');
// Add width and height attributes to the SVG element
processedTemplate = processedTemplate.replace(
diff --git a/my-project/template-v2.svg b/my-project/template-v2.svg
index a05b2ed..96d4856 100644
--- a/my-project/template-v2.svg
+++ b/my-project/template-v2.svg
@@ -36,12 +36,12 @@
font-size="14" font-weight="700">{{LANE_NAME}}
-
-
+
- {{TASK_ID}}:
- {{TASK_TITLE}}
+ {{ITEM_ID}}:
+ {{ITEM_TITLE}}
diff --git a/test/generator.test.js b/test/generator.test.js
index dc63a36..9632c56 100644
--- a/test/generator.test.js
+++ b/test/generator.test.js
@@ -66,12 +66,12 @@ describe('Timeline Generator', () => {
}).toThrow('Template is missing required elements: lane-template')
})
- it('should throw error when template is missing task-template', () => {
- const malformedTemplate = createMalformedTemplate('task-template')
+ it('should throw error when template is missing item-template', () => {
+ const malformedTemplate = createMalformedTemplate('item-template')
expect(() => {
timelineGenerator.generate(items, config, malformedTemplate)
- }).toThrow('Template is missing required elements: task-template')
+ }).toThrow('Template is missing required elements: item-template')
})
it('should validate template with proper error message', () => {
@@ -179,8 +179,8 @@ describe('Timeline Generator', () => {
expect(result).not.toContain('{{MONTH_X}}')
expect(result).not.toContain('{{LANE_Y}}')
- expect(result).not.toContain('{{TASK_X}}')
- expect(result).not.toContain('{{TASK_TITLE}}')
+ expect(result).not.toContain('{{ITEM_X}}')
+ expect(result).not.toContain('{{ITEM_TITLE}}')
})
it('should contain task data in generated SVG', () => {
@@ -248,7 +248,7 @@ describe('Timeline Generator', () => {
expect(result).not.toContain('id="month-template"')
expect(result).not.toContain('id="lane-template"')
- expect(result).not.toContain('id="task-template"')
+ expect(result).not.toContain('id="item-template"')
expect(result).not.toContain('style="display:none"')
})
diff --git a/test/integration.test.js b/test/integration.test.js
index f4593db..04d1e28 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -58,7 +58,7 @@ describe('Timeline Integration', () => {
// Verify no template placeholders remain
expect(viewer.innerHTML).not.toContain('{{MONTH_X}}')
expect(viewer.innerHTML).not.toContain('{{LANE_Y}}')
- expect(viewer.innerHTML).not.toContain('{{TASK_X}}')
+ expect(viewer.innerHTML).not.toContain('{{ITEM_X}}')
// Verify download button enabled
const downloadBtn = document.getElementById('downloadSvg')
@@ -257,9 +257,9 @@ describe('Timeline Integration', () => {
expect(viewer).toBeTruthy()
})
- it('should reject malformed template-v2 (missing task-template)', async () => {
+ it('should reject malformed template-v2 (missing item-template)', async () => {
const config = createSampleProject()
- const malformedTemplate = createMalformedTemplate('task-template')
+ const malformedTemplate = createMalformedTemplate('item-template')
mockFetch(malformedTemplate)
mockFetch(createSampleCSV())
diff --git a/test/testHelpers.js b/test/testHelpers.js
index 6cff178..858dc2d 100644
--- a/test/testHelpers.js
+++ b/test/testHelpers.js
@@ -61,9 +61,9 @@ export const createSampleTemplate = () => ``,
- 'task-template': `