Skip to content

Commit 7f3984a

Browse files
authored
Merge pull request #2405 from alixander/legend
export diagram.Legend
2 parents a776023 + 79e7b69 commit 7f3984a

File tree

9 files changed

+1385
-450
lines changed

9 files changed

+1385
-450
lines changed

d2ast/keywords.go

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ var ReservedKeywords map[string]struct{}
88
// Non Style/Holder keywords.
99
var SimpleReservedKeywords = map[string]struct{}{
1010
"label": {},
11-
"legend-label": {},
1211
"shape": {},
1312
"icon": {},
1413
"constraint": {},

d2compiler/compile.go

+38-4
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 {
@@ -543,10 +581,6 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
543581
attrs.Tooltip = &d2graph.Scalar{}
544582
attrs.Tooltip.Value = scalar.ScalarString()
545583
attrs.Tooltip.MapKey = f.LastPrimaryKey()
546-
case "legend-label":
547-
attrs.LegendLabel = &d2graph.Scalar{}
548-
attrs.LegendLabel.Value = scalar.ScalarString()
549-
attrs.LegendLabel.MapKey = f.LastPrimaryKey()
550584
case "width":
551585
_, err := strconv.Atoi(scalar.ScalarString())
552586
if err != nil {

d2compiler/compile_test.go

+136-18
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

@@ -5433,31 +5569,13 @@ b -> c
54335569
assert.Equal(t, "red", g.Edges[0].Style.Stroke.Value)
54345570
},
54355571
},
5436-
{
5437-
name: "legend-label",
5438-
run: func(t *testing.T) {
5439-
g, _ := assertCompile(t, `
5440-
a.legend-label: This is A
5441-
b: {legend-label: This is B}
5442-
a -> b: {
5443-
legend-label: "This is a->b"
5444-
}
5445-
`, ``)
5446-
assert.Equal(t, "a", g.Objects[0].ID)
5447-
assert.Equal(t, "This is A", g.Objects[0].LegendLabel.Value)
5448-
assert.Equal(t, "b", g.Objects[1].ID)
5449-
assert.Equal(t, "This is B", g.Objects[1].LegendLabel.Value)
5450-
assert.Equal(t, "This is a->b", g.Edges[0].LegendLabel.Value)
5451-
},
5452-
},
54535572
{
54545573
name: "merge-glob-values",
54555574
run: func(t *testing.T) {
54565575
assertCompile(t, `
54575576
"a"
54585577
*.style.stroke-width: 2
54595578
*.style.font-size: 14
5460-
54615579
a.width: 339
54625580
`, ``)
54635581
},

d2exporter/export.go

+20-3
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

@@ -243,9 +263,6 @@ func toShape(obj *d2graph.Object, g *d2graph.Graph) d2target.Shape {
243263
if obj.Tooltip != nil {
244264
shape.Tooltip = obj.Tooltip.Value
245265
}
246-
if obj.LegendLabel != nil {
247-
shape.LegendLabel = obj.LegendLabel.Value
248-
}
249266
if obj.Style.Animated != nil {
250267
shape.Animated, _ = strconv.ParseBool(obj.Style.Animated.Value)
251268
}

d2graph/d2graph.go

+7-2
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{
@@ -222,8 +228,7 @@ type Attributes struct {
222228

223229
// These names are attached to the rendered elements in SVG
224230
// so that users can target them however they like outside of D2
225-
Classes []string `json:"classes,omitempty"`
226-
LegendLabel *Scalar `json:"legendLabel,omitempty"`
231+
Classes []string `json:"classes,omitempty"`
227232
}
228233

229234
// ApplyTextTransform will alter the `Label.Value` of the current object based

d2target/d2target.go

+7-3
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
@@ -492,7 +498,6 @@ type Shape struct {
492498
PrettyLink string `json:"prettyLink,omitempty"`
493499
Icon *url.URL `json:"icon"`
494500
IconPosition string `json:"iconPosition"`
495-
LegendLabel string `json:"legendLabel,omitempty"`
496501

497502
// Whether the shape should allow shapes behind it to bleed through
498503
// Currently just used for sequence diagram groups
@@ -621,7 +626,6 @@ type Connection struct {
621626

622627
Animated bool `json:"animated"`
623628
Tooltip string `json:"tooltip"`
624-
LegendLabel string `json:"legendLabel,omitempty"`
625629
Icon *url.URL `json:"icon"`
626630
IconPosition string `json:"iconPosition,omitempty"`
627631

0 commit comments

Comments
 (0)