Skip to content

Commit 79e7b69

Browse files
committed
legend
1 parent 27d60d9 commit 79e7b69

File tree

6 files changed

+1375
-1
lines changed

6 files changed

+1375
-1
lines changed

d2compiler/compile.go

+38
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
9696
c.validateEdges(g)
9797
c.validatePositionsCompatibility(g)
9898

99+
c.compileLegend(g, ir)
100+
99101
c.compileBoardsField(g, ir, "layers")
100102
c.compileBoardsField(g, ir, "scenarios")
101103
c.compileBoardsField(g, ir, "steps")
@@ -110,6 +112,42 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
110112
return g
111113
}
112114

115+
func (c *compiler) compileLegend(g *d2graph.Graph, m *d2ir.Map) {
116+
varsField := m.GetField(d2ast.FlatUnquotedString("vars"))
117+
if varsField == nil || varsField.Map() == nil {
118+
return
119+
}
120+
121+
legendField := varsField.Map().GetField(d2ast.FlatUnquotedString("d2-legend"))
122+
if legendField == nil || legendField.Map() == nil {
123+
return
124+
}
125+
126+
legendGraph := d2graph.NewGraph()
127+
128+
c.compileMap(legendGraph.Root, legendField.Map())
129+
c.setDefaultShapes(legendGraph)
130+
131+
objects := make([]*d2graph.Object, 0)
132+
for _, obj := range legendGraph.Objects {
133+
if obj.Style.Opacity != nil {
134+
if opacity, err := strconv.ParseFloat(obj.Style.Opacity.Value, 64); err == nil && opacity == 0 {
135+
continue
136+
}
137+
}
138+
objects = append(objects, obj)
139+
}
140+
141+
legend := &d2graph.Legend{
142+
Objects: objects,
143+
Edges: legendGraph.Edges,
144+
}
145+
146+
if len(legend.Objects) > 0 || len(legend.Edges) > 0 {
147+
g.Legend = legend
148+
}
149+
}
150+
113151
func (c *compiler) compileBoardsField(g *d2graph.Graph, ir *d2ir.Map, fieldName string) {
114152
boards := ir.GetField(d2ast.FlatUnquotedString(fieldName))
115153
if boards.Map() == nil {

d2compiler/compile_test.go

+136
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,142 @@ x: {
720720
}
721721
},
722722
},
723+
{
724+
name: "legend",
725+
726+
text: `
727+
vars: {
728+
d2-legend: {
729+
User: "A person who interacts with the system" {
730+
shape: person
731+
style: {
732+
fill: "#f5f5f5"
733+
}
734+
}
735+
736+
Database: "Stores application data" {
737+
shape: cylinder
738+
style.fill: "#b5d3ff"
739+
}
740+
741+
HiddenShape: "This should not appear in the legend" {
742+
style.opacity: 0
743+
}
744+
745+
User -> Database: "Reads data" {
746+
style.stroke: "blue"
747+
}
748+
749+
Database -> User: "Returns results" {
750+
style.stroke-dash: 5
751+
}
752+
}
753+
}
754+
755+
user: User
756+
db: Database
757+
user -> db: Uses
758+
`,
759+
assertions: func(t *testing.T, g *d2graph.Graph) {
760+
if g.Legend == nil {
761+
t.Fatal("Expected Legend to be non-nil")
762+
return
763+
}
764+
765+
// 2. Verify the correct objects are in the legend
766+
if len(g.Legend.Objects) != 2 {
767+
t.Errorf("Expected 2 objects in legend, got %d", len(g.Legend.Objects))
768+
}
769+
770+
// Check for User object
771+
hasUser := false
772+
hasDatabase := false
773+
for _, obj := range g.Legend.Objects {
774+
if obj.ID == "User" {
775+
hasUser = true
776+
if obj.Shape.Value != "person" {
777+
t.Errorf("User shape incorrect, expected 'person', got: %s", obj.Shape.Value)
778+
}
779+
} else if obj.ID == "Database" {
780+
hasDatabase = true
781+
if obj.Shape.Value != "cylinder" {
782+
t.Errorf("Database shape incorrect, expected 'cylinder', got: %s", obj.Shape.Value)
783+
}
784+
} else if obj.ID == "HiddenShape" {
785+
t.Errorf("HiddenShape should not be in legend due to opacity: 0")
786+
}
787+
}
788+
789+
if !hasUser {
790+
t.Errorf("User object missing from legend")
791+
}
792+
if !hasDatabase {
793+
t.Errorf("Database object missing from legend")
794+
}
795+
796+
// 3. Verify the correct edges are in the legend
797+
if len(g.Legend.Edges) != 2 {
798+
t.Errorf("Expected 2 edges in legend, got %d", len(g.Legend.Edges))
799+
}
800+
801+
// Check for expected edges
802+
hasReadsEdge := false
803+
hasReturnsEdge := false
804+
for _, edge := range g.Legend.Edges {
805+
if edge.Label.Value == "Reads data" {
806+
hasReadsEdge = true
807+
// Check edge properties
808+
if edge.Style.Stroke == nil {
809+
t.Errorf("Reads edge stroke is nil")
810+
} else if edge.Style.Stroke.Value != "blue" {
811+
t.Errorf("Reads edge stroke incorrect, expected 'blue', got: %s", edge.Style.Stroke.Value)
812+
}
813+
} else if edge.Label.Value == "Returns results" {
814+
hasReturnsEdge = true
815+
// Check edge properties
816+
if edge.Style.StrokeDash == nil {
817+
t.Errorf("Returns edge stroke-dash is nil")
818+
} else if edge.Style.StrokeDash.Value != "5" {
819+
t.Errorf("Returns edge stroke-dash incorrect, expected '5', got: %s", edge.Style.StrokeDash.Value)
820+
}
821+
} else if edge.Label.Value == "Hidden connection" {
822+
t.Errorf("Hidden connection should not be in legend due to opacity: 0")
823+
}
824+
}
825+
826+
if !hasReadsEdge {
827+
t.Errorf("'Reads data' edge missing from legend")
828+
}
829+
if !hasReturnsEdge {
830+
t.Errorf("'Returns results' edge missing from legend")
831+
}
832+
833+
// 4. Verify the regular diagram content is still there
834+
userObj, hasUserObj := g.Root.HasChild([]string{"user"})
835+
if !hasUserObj {
836+
t.Errorf("Main diagram missing 'user' object")
837+
} else if userObj.Label.Value != "User" {
838+
t.Errorf("User label incorrect, expected 'User', got: %s", userObj.Label.Value)
839+
}
840+
841+
dbObj, hasDBObj := g.Root.HasChild([]string{"db"})
842+
if !hasDBObj {
843+
t.Errorf("Main diagram missing 'db' object")
844+
} else if dbObj.Label.Value != "Database" {
845+
t.Errorf("DB label incorrect, expected 'Database', got: %s", dbObj.Label.Value)
846+
}
847+
848+
// Check the main edge
849+
if len(g.Edges) == 0 {
850+
t.Errorf("No edges found in main diagram")
851+
} else {
852+
mainEdge := g.Edges[0]
853+
if mainEdge.Label.Value != "Uses" {
854+
t.Errorf("Main edge label incorrect, expected 'Uses', got: %s", mainEdge.Label.Value)
855+
}
856+
}
857+
},
858+
},
723859
{
724860
name: "underscore_edge_nested",
725861

d2exporter/export.go

+20
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ func Export(ctx context.Context, g *d2graph.Graph, fontFamily *d2fonts.FontFamil
4747
diagram.Connections[i] = toConnection(g.Edges[i], g.Theme)
4848
}
4949

50+
if g.Legend != nil {
51+
legend := &d2target.Legend{}
52+
53+
if len(g.Legend.Objects) > 0 {
54+
legend.Shapes = make([]d2target.Shape, len(g.Legend.Objects))
55+
for i, obj := range g.Legend.Objects {
56+
legend.Shapes[i] = toShape(obj, g)
57+
}
58+
}
59+
60+
if len(g.Legend.Edges) > 0 {
61+
legend.Connections = make([]d2target.Connection, len(g.Legend.Edges))
62+
for i, edge := range g.Legend.Edges {
63+
legend.Connections[i] = toConnection(edge, g.Theme)
64+
}
65+
}
66+
67+
diagram.Legend = legend
68+
}
69+
5070
return diagram, nil
5171
}
5272

d2graph/d2graph.go

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type Graph struct {
4949
BaseAST *d2ast.Map `json:"-"`
5050

5151
Root *Object `json:"root"`
52+
Legend *Legend `json:"legend,omitempty"`
5253
Edges []*Edge `json:"edges"`
5354
Objects []*Object `json:"objects"`
5455

@@ -67,6 +68,11 @@ type Graph struct {
6768
Data map[string]interface{} `json:"data,omitempty"`
6869
}
6970

71+
type Legend struct {
72+
Objects []*Object `json:"objects,omitempty"`
73+
Edges []*Edge `json:"edges,omitempty"`
74+
}
75+
7076
func NewGraph() *Graph {
7177
d := &Graph{}
7278
d.Root = &Object{

d2target/d2target.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,20 @@ type Diagram struct {
8686
Shapes []Shape `json:"shapes"`
8787
Connections []Connection `json:"connections"`
8888

89-
Root Shape `json:"root"`
89+
Root Shape `json:"root"`
90+
Legend *Legend `json:"legend,omitempty"`
9091
// Maybe Icon can be used as a watermark in the root shape
9192

9293
Layers []*Diagram `json:"layers,omitempty"`
9394
Scenarios []*Diagram `json:"scenarios,omitempty"`
9495
Steps []*Diagram `json:"steps,omitempty"`
9596
}
9697

98+
type Legend struct {
99+
Shapes []Shape `json:"shapes,omitempty"`
100+
Connections []Connection `json:"connections,omitempty"`
101+
}
102+
97103
func (d *Diagram) GetBoard(boardPath []string) *Diagram {
98104
if len(boardPath) == 0 {
99105
return d

0 commit comments

Comments
 (0)