Skip to content

Commit 68d5274

Browse files
authored
Merge pull request #115 from jongalloway/feature/matrix-cell-icons
Add matrix cell icon support
2 parents 9df955e + 60b6f07 commit 68d5274

7 files changed

Lines changed: 360 additions & 11 deletions

File tree

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ DiagramForge currently supports more than a dozen diagram types across Mermaid a
8686
</tr>
8787
<tr>
8888
<td align="center" valign="top" width="33%">
89-
<a href="https://github.com/jongalloway/DiagramForge/blob/main/tests/DiagramForge.E2ETests/Fixtures/conceptual-matrix.expected.svg">
90-
<img src="https://raw.githubusercontent.com/jongalloway/DiagramForge/main/tests/DiagramForge.E2ETests/Fixtures/conceptual-matrix.expected.svg" alt="Conceptual matrix" height="96" />
89+
<a href="https://github.com/jongalloway/DiagramForge/blob/main/tests/DiagramForge.E2ETests/Fixtures/conceptual-matrix-icons.expected.svg">
90+
<img src="https://raw.githubusercontent.com/jongalloway/DiagramForge/main/tests/DiagramForge.E2ETests/Fixtures/conceptual-matrix-icons.expected.svg" alt="Conceptual matrix with icons" height="96" />
9191
</a>
9292
<br />
93-
<sub>Conceptual Matrix</sub>
93+
<sub>Conceptual Matrix + Icons</sub>
9494
</td>
9595
<td align="center" valign="top" width="33%">
9696
<a href="https://github.com/jongalloway/DiagramForge/blob/main/tests/DiagramForge.E2ETests/Fixtures/conceptual-pillars-icons.expected.svg">
@@ -365,13 +365,27 @@ Supported icon-bearing diagram types today:
365365
- Conceptual cycle
366366
- Conceptual chevrons
367367
- Conceptual funnel
368+
- Conceptual matrix
368369
- Conceptual pillars
369370
- Conceptual pyramid
370371
- Conceptual radial
371372

372-
Not yet supported:
373+
For conceptual matrix diagrams, icons are attached per cell through an optional `cells:` list in row-major order. Non-empty cell entries must contain an icon directive; use a blank `-` to leave a cell without an icon.
373374

374-
- Conceptual matrix
375+
```yaml
376+
diagram: matrix
377+
rows:
378+
- Important
379+
- Not Important
380+
columns:
381+
- Urgent
382+
- Not Urgent
383+
cells:
384+
- icon:builtin:cloud
385+
-
386+
- Lower-left [icon:heroicons:shield-check]
387+
- [icon:builtin:database]
388+
```
375389
376390
#### Registering an icon pack
377391

src/DiagramForge/Layout/DefaultLayoutEngine.Matrix.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@ private static void LayoutMatrixDiagram(
2222

2323
double titleOffset = !string.IsNullOrWhiteSpace(diagram.Title) ? theme.TitleFontSize + 8 : 0;
2424
double baseFontSize = cells.Max(node => node.Label.FontSize ?? theme.FontSize);
25-
int maxLineCount = cells.Max(node => GetTextLineCount(node.Label));
26-
double maxTextWidth = cells.Max(node => EstimateTextWidth(node.Label, node.Label.FontSize ?? theme.FontSize));
27-
28-
double cellWidth = Math.Max(minW + 24, maxTextWidth + theme.NodePadding * 2.5);
29-
double textBlockHeight = Math.Max(1, maxLineCount) * baseFontSize * 1.15;
30-
double cellHeight = Math.Max(nodeH + baseFontSize * 0.7, textBlockHeight + theme.NodePadding * 2.6);
25+
double cellWidth = cells.Max(node =>
26+
EnsureIconWidth(node, theme, Math.Max(minW + 24, EstimateTextWidth(node.Label, node.Label.FontSize ?? theme.FontSize) + theme.NodePadding * 2.5)));
27+
double cellHeight = cells.Max(node =>
28+
{
29+
double fontSize = node.Label.FontSize ?? theme.FontSize;
30+
int lineCount = Math.Max(1, GetTextLineCount(node.Label));
31+
double textBlockHeight = lineCount * fontSize * 1.15;
32+
double baseHeight = Math.Max(nodeH + baseFontSize * 0.7, textBlockHeight + theme.NodePadding * 2.6);
33+
return EnsureIconHeight(node, baseHeight);
34+
});
3135
double gap = Math.Max(theme.NodePadding, 18);
3236

3337
string[] palette = theme.NodePalette is { Count: > 0 }

src/DiagramForge/Parsers/Conceptual/ConceptualDslParser.Matrix.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,45 @@ private static void ParseMatrixDiagram(string[] lines, IDiagramSemanticModelBuil
99
{
1010
int rowsLine = FindSectionLine(lines, "rows");
1111
int colsLine = FindSectionLine(lines, "columns");
12+
int cellsLine = FindSectionLine(lines, "cells");
1213

1314
var rows = rowsLine >= 0 ? ReadListItems(lines, rowsLine + 1) : [];
1415
var cols = colsLine >= 0 ? ReadListItems(lines, colsLine + 1) : [];
16+
var cells = cellsLine >= 0 ? ReadListItems(lines, cellsLine + 1) : [];
1517

1618
if (rows.Count == 0 || cols.Count == 0)
1719
throw new DiagramParseException("Matrix diagram requires non-empty 'rows' and 'columns' sections.");
1820

1921
if (rows.Count != 2 || cols.Count != 2)
2022
throw new DiagramParseException("Matrix diagram currently supports exactly 2 rows and 2 columns.");
2123

24+
int maxCellCount = rows.Count * cols.Count;
25+
if (cells.Count > maxCellCount)
26+
throw new DiagramParseException($"Matrix diagram supports at most {maxCellCount} 'cells' entries, but {cells.Count} were provided.");
27+
2228
for (int r = 0; r < rows.Count; r++)
2329
{
2430
for (int c = 0; c < cols.Count; c++)
2531
{
2632
var nodeId = $"cell_{r}_{c}";
2733
var node = new Node(nodeId, $"{cols[c]}\n{rows[r]}");
34+
int cellIndex = (r * cols.Count) + c;
35+
if (cellIndex < cells.Count)
36+
{
37+
string cellValue = cells[cellIndex];
38+
if (!string.IsNullOrWhiteSpace(cellValue))
39+
{
40+
var spec = ParseIconLabeledText(cellValue);
41+
if (spec.IconRef is null)
42+
{
43+
throw new DiagramParseException(
44+
$"Matrix 'cells' entry {cellIndex + 1} must be blank or contain an icon directive like 'icon:pack:name'.");
45+
}
46+
47+
node.IconRef = spec.IconRef;
48+
}
49+
}
50+
2851
node.Metadata["matrix:row"] = r;
2952
node.Metadata["matrix:column"] = c;
3053
node.Metadata["matrix:rowLabel"] = rows[r];
Lines changed: 155 additions & 0 deletions
Loading
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
theme: presentation
3+
---
4+
diagram: matrix
5+
rows:
6+
- Important
7+
- Not Important
8+
columns:
9+
- Urgent
10+
- Not Urgent
11+
cells:
12+
- icon:builtin:cloud
13+
-
14+
- Lower-left [icon:heroicons:shield-check]
15+
- [icon:builtin:database]

tests/DiagramForge.Tests/Layout/DefaultLayoutEngineTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DiagramForge.Layout;
22
using DiagramForge.Models;
3+
using DiagramForge.Rendering;
34

45
namespace DiagramForge.Tests.Layout;
56

@@ -309,6 +310,30 @@ public void Layout_MatrixDiagram_PlacesQuadrantsInTwoByTwoGrid()
309310
Assert.Equal(topLeft.Height / 2, Convert.ToDouble(topLeft.Metadata["label:centerY"], System.Globalization.CultureInfo.InvariantCulture));
310311
}
311312

313+
[Fact]
314+
public void Layout_MatrixDiagram_WithIcons_MakesCellsLargeEnoughForIconArea()
315+
{
316+
var diagram = new Diagram { DiagramType = "matrix" };
317+
318+
var withIcon = MatrixCell("cell_0_0", "Urgent\nImportant", 0, 0);
319+
withIcon.ResolvedIcon = new DiagramIcon("builtin", "cloud", "0 0 24 24", "<path d=\"M0 0h24v24H0z\" />");
320+
321+
diagram.AddNode(withIcon)
322+
.AddNode(MatrixCell("cell_0_1", "Not Urgent\nImportant", 0, 1))
323+
.AddNode(MatrixCell("cell_1_0", "Urgent\nNot Important", 1, 0))
324+
.AddNode(MatrixCell("cell_1_1", "Not Urgent\nNot Important", 1, 1));
325+
326+
_engine.Layout(diagram, _theme);
327+
328+
double minIconWidth = SvgNodeWriter.DefaultIconSize + (2 * _theme.NodePadding);
329+
double minIconHeight = SvgNodeWriter.DefaultIconSize + SvgNodeWriter.IconLabelGap;
330+
331+
Assert.True(withIcon.Width >= minIconWidth,
332+
$"Expected icon-bearing matrix cell width {withIcon.Width} to be >= {minIconWidth}.");
333+
Assert.True(withIcon.Height >= diagram.LayoutHints.MinNodeHeight + minIconHeight,
334+
$"Expected icon-bearing matrix cell height {withIcon.Height} to include icon area of at least {minIconHeight}.");
335+
}
336+
312337
[Fact]
313338
public void Layout_Pyramid_AssignsTriangularSegmentMetadata()
314339
{

0 commit comments

Comments
 (0)