@@ -44,6 +44,13 @@ const (
44
44
DEFAULT_PADDING = 100
45
45
46
46
appendixIconRadius = 16
47
+
48
+ // Legend constants
49
+ LEGEND_PADDING = 20
50
+ LEGEND_ITEM_SPACING = 15
51
+ LEGEND_ICON_SIZE = 24
52
+ LEGEND_FONT_SIZE = 14
53
+ LEGEND_CORNER_PADDING = 10
47
54
)
48
55
49
56
var multipleOffset = geo .NewVector (d2target .MULTIPLE_OFFSET , - d2target .MULTIPLE_OFFSET )
@@ -101,6 +108,262 @@ func dimensions(diagram *d2target.Diagram, pad int) (left, top, width, height in
101
108
return left , top , width , height
102
109
}
103
110
111
+ func renderLegend (buf * bytes.Buffer , diagram * d2target.Diagram , diagramHash string , theme * d2themes.Theme ) error {
112
+ if diagram .Legend == nil || (len (diagram .Legend .Shapes ) == 0 && len (diagram .Legend .Connections ) == 0 ) {
113
+ return nil
114
+ }
115
+
116
+ _ , br := diagram .BoundingBox ()
117
+
118
+ ruler , err := textmeasure .NewRuler ()
119
+ if err != nil {
120
+ return err
121
+ }
122
+
123
+ totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
124
+ maxLabelWidth := 0
125
+
126
+ itemCount := 0
127
+
128
+ for _ , s := range diagram .Legend .Shapes {
129
+ if s .Label == "" {
130
+ continue
131
+ }
132
+
133
+ mtext := & d2target.MText {
134
+ Text : s .Label ,
135
+ FontSize : LEGEND_FONT_SIZE ,
136
+ }
137
+
138
+ dims := d2graph .GetTextDimensions (nil , ruler , mtext , nil )
139
+ maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
140
+ totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
141
+ itemCount ++
142
+ }
143
+
144
+ for _ , c := range diagram .Legend .Connections {
145
+ if c .Label == "" {
146
+ continue
147
+ }
148
+
149
+ mtext := & d2target.MText {
150
+ Text : c .Label ,
151
+ FontSize : LEGEND_FONT_SIZE ,
152
+ }
153
+
154
+ dims := d2graph .GetTextDimensions (nil , ruler , mtext , nil )
155
+ maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
156
+ totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
157
+ itemCount ++
158
+ }
159
+
160
+ if itemCount > 0 {
161
+ totalHeight -= LEGEND_ITEM_SPACING / 2
162
+ }
163
+
164
+ if itemCount > 0 && len (diagram .Legend .Connections ) > 0 {
165
+ totalHeight += LEGEND_PADDING * 1.5
166
+ } else {
167
+ totalHeight += LEGEND_PADDING * 1.2
168
+ }
169
+
170
+ legendWidth := LEGEND_PADDING * 2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
171
+ legendX := br .X + LEGEND_CORNER_PADDING
172
+ tl , _ := diagram .BoundingBox ()
173
+ legendY := br .Y - totalHeight
174
+ if legendY < tl .Y {
175
+ legendY = tl .Y
176
+ }
177
+
178
+ shadowEl := d2themes .NewThemableElement ("rect" , theme )
179
+ shadowEl .Fill = "#F7F7FA"
180
+ shadowEl .Stroke = "#DEE1EB"
181
+ shadowEl .Style = "stroke-width: 1px; filter: drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.1))"
182
+ shadowEl .X = float64 (legendX )
183
+ shadowEl .Y = float64 (legendY )
184
+ shadowEl .Width = float64 (legendWidth )
185
+ shadowEl .Height = float64 (totalHeight )
186
+ shadowEl .Rx = 4
187
+ fmt .Fprint (buf , shadowEl .Render ())
188
+
189
+ legendEl := d2themes .NewThemableElement ("rect" , theme )
190
+ legendEl .Fill = "#ffffff"
191
+ legendEl .Stroke = "#DEE1EB"
192
+ legendEl .Style = "stroke-width: 1px"
193
+ legendEl .X = float64 (legendX )
194
+ legendEl .Y = float64 (legendY )
195
+ legendEl .Width = float64 (legendWidth )
196
+ legendEl .Height = float64 (totalHeight )
197
+ legendEl .Rx = 4
198
+ fmt .Fprint (buf , legendEl .Render ())
199
+
200
+ fmt .Fprintf (buf , `<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;">Legend</text>` ,
201
+ legendX + LEGEND_PADDING , legendY + LEGEND_PADDING + LEGEND_FONT_SIZE , LEGEND_FONT_SIZE + 2 )
202
+
203
+ currentY := legendY + LEGEND_PADDING * 2 + LEGEND_FONT_SIZE
204
+
205
+ shapeCount := 0
206
+ for _ , s := range diagram .Legend .Shapes {
207
+ if s .Label == "" {
208
+ continue
209
+ }
210
+
211
+ iconX := legendX + LEGEND_PADDING
212
+ iconY := currentY
213
+
214
+ shapeIcon , err := renderLegendShapeIcon (s , iconX , iconY , diagramHash , theme )
215
+ if err != nil {
216
+ return err
217
+ }
218
+ fmt .Fprint (buf , shapeIcon )
219
+
220
+ mtext := & d2target.MText {
221
+ Text : s .Label ,
222
+ FontSize : LEGEND_FONT_SIZE ,
223
+ }
224
+
225
+ dims := d2graph .GetTextDimensions (nil , ruler , mtext , nil )
226
+
227
+ rowHeight := go2 .IntMax (dims .Height , LEGEND_ICON_SIZE )
228
+ textY := currentY + rowHeight / 2 + int (float64 (dims .Height )* 0.3 )
229
+
230
+ fmt .Fprintf (buf , `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>` ,
231
+ iconX + LEGEND_ICON_SIZE + LEGEND_PADDING , textY , LEGEND_FONT_SIZE ,
232
+ html .EscapeString (s .Label ))
233
+
234
+ currentY += rowHeight + LEGEND_ITEM_SPACING
235
+ shapeCount ++
236
+ }
237
+
238
+ if shapeCount > 0 && len (diagram .Legend .Connections ) > 0 {
239
+ currentY += LEGEND_ITEM_SPACING / 2
240
+
241
+ separatorEl := d2themes .NewThemableElement ("line" , theme )
242
+ separatorEl .X1 = float64 (legendX + LEGEND_PADDING )
243
+ separatorEl .Y1 = float64 (currentY )
244
+ separatorEl .X2 = float64 (legendX + legendWidth - LEGEND_PADDING )
245
+ separatorEl .Y2 = float64 (currentY )
246
+ separatorEl .Stroke = "#DEE1EB"
247
+ separatorEl .StrokeDashArray = "2,2"
248
+ fmt .Fprint (buf , separatorEl .Render ())
249
+
250
+ currentY += LEGEND_ITEM_SPACING
251
+ }
252
+
253
+ for _ , c := range diagram .Legend .Connections {
254
+ if c .Label == "" {
255
+ continue
256
+ }
257
+
258
+ iconX := legendX + LEGEND_PADDING
259
+ iconY := currentY + LEGEND_ICON_SIZE / 2
260
+
261
+ connIcon , err := renderLegendConnectionIcon (c , iconX , iconY , theme )
262
+ if err != nil {
263
+ return err
264
+ }
265
+ fmt .Fprint (buf , connIcon )
266
+
267
+ mtext := & d2target.MText {
268
+ Text : c .Label ,
269
+ FontSize : LEGEND_FONT_SIZE ,
270
+ }
271
+
272
+ dims := d2graph .GetTextDimensions (nil , ruler , mtext , nil )
273
+
274
+ rowHeight := go2 .IntMax (dims .Height , LEGEND_ICON_SIZE )
275
+ textY := currentY + rowHeight / 2 + int (float64 (dims .Height )* 0.2 )
276
+
277
+ fmt .Fprintf (buf , `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>` ,
278
+ iconX + LEGEND_ICON_SIZE + LEGEND_PADDING , textY , LEGEND_FONT_SIZE ,
279
+ html .EscapeString (c .Label ))
280
+
281
+ currentY += rowHeight + LEGEND_ITEM_SPACING
282
+ }
283
+
284
+ if shapeCount > 0 && len (diagram .Legend .Connections ) > 0 {
285
+ currentY += LEGEND_PADDING / 2
286
+ } else {
287
+ currentY += LEGEND_PADDING / 4
288
+ }
289
+
290
+ return nil
291
+ }
292
+
293
+ func renderLegendShapeIcon (s d2target.Shape , x , y int , diagramHash string , theme * d2themes.Theme ) (string , error ) {
294
+ iconShape := s
295
+ const sizeFactor = 5
296
+ iconShape .Pos .X = 0
297
+ iconShape .Pos .Y = 0
298
+ iconShape .Width = LEGEND_ICON_SIZE * sizeFactor
299
+ iconShape .Height = LEGEND_ICON_SIZE * sizeFactor
300
+ iconShape .Label = ""
301
+ buf := & bytes.Buffer {}
302
+ appendixBuf := & bytes.Buffer {}
303
+ finalBuf := & bytes.Buffer {}
304
+ fmt .Fprintf (finalBuf , `<g transform="translate(%d, %d) scale(%f)">` ,
305
+ x , y , 1.0 / sizeFactor )
306
+ _ , err := drawShape (buf , appendixBuf , diagramHash , iconShape , nil , theme )
307
+ if err != nil {
308
+ return "" , err
309
+ }
310
+
311
+ fmt .Fprint (finalBuf , buf .String ())
312
+
313
+ fmt .Fprint (finalBuf , `</g>` )
314
+
315
+ return finalBuf .String (), nil
316
+ }
317
+
318
+ func renderLegendConnectionIcon (c d2target.Connection , x , y int , theme * d2themes.Theme ) (string , error ) {
319
+ finalBuf := & bytes.Buffer {}
320
+
321
+ buf := & bytes.Buffer {}
322
+
323
+ const sizeFactor = 2
324
+
325
+ legendConn := * d2target .BaseConnection ()
326
+
327
+ legendConn .ID = c .ID
328
+ legendConn .SrcArrow = c .SrcArrow
329
+ legendConn .DstArrow = c .DstArrow
330
+ legendConn .StrokeDash = c .StrokeDash
331
+ legendConn .StrokeWidth = c .StrokeWidth
332
+ legendConn .Stroke = c .Stroke
333
+ legendConn .Fill = c .Fill
334
+ legendConn .BorderRadius = c .BorderRadius
335
+ legendConn .Opacity = c .Opacity
336
+ legendConn .Animated = c .Animated
337
+
338
+ startX := 0.0
339
+ midY := 0.0
340
+ width := float64 (LEGEND_ICON_SIZE * sizeFactor )
341
+
342
+ legendConn .Route = []* geo.Point {
343
+ {X : startX , Y : midY },
344
+ {X : startX + width , Y : midY },
345
+ }
346
+
347
+ legendHash := fmt .Sprintf ("legend-%s" , hash (fmt .Sprintf ("%s-%d-%d" , c .ID , x , y )))
348
+
349
+ markers := make (map [string ]struct {})
350
+ idToShape := make (map [string ]d2target.Shape )
351
+
352
+ fmt .Fprintf (finalBuf , `<g transform="translate(%d, %d) scale(%f)">` ,
353
+ x , y , 1.0 / sizeFactor )
354
+
355
+ _ , err := drawConnection (buf , legendHash , legendConn , markers , idToShape , nil , theme )
356
+ if err != nil {
357
+ return "" , err
358
+ }
359
+
360
+ fmt .Fprint (finalBuf , buf .String ())
361
+
362
+ fmt .Fprint (finalBuf , `</g>` )
363
+
364
+ return finalBuf .String (), nil
365
+ }
366
+
104
367
func arrowheadMarkerID (diagramHash string , isTarget bool , connection d2target.Connection ) string {
105
368
var arrowhead d2target.Arrowhead
106
369
if isTarget {
@@ -2085,8 +2348,85 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
2085
2348
// add all appendix items afterwards so they are always on top
2086
2349
fmt .Fprint (buf , appendixItemBuf )
2087
2350
2351
+ if diagram .Legend != nil && (len (diagram .Legend .Shapes ) > 0 || len (diagram .Legend .Connections ) > 0 ) {
2352
+ legendBuf := & bytes.Buffer {}
2353
+ err := renderLegend (legendBuf , diagram , diagramHash , inlineTheme )
2354
+ if err != nil {
2355
+ return nil , err
2356
+ }
2357
+ fmt .Fprint (buf , legendBuf )
2358
+ }
2359
+
2088
2360
// Note: we always want this since we reference it on connections even if there end up being no masked labels
2089
2361
left , top , w , h := dimensions (diagram , pad )
2362
+
2363
+ if diagram .Legend != nil && (len (diagram .Legend .Shapes ) > 0 || len (diagram .Legend .Connections ) > 0 ) {
2364
+ tl , br := diagram .BoundingBox ()
2365
+ totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
2366
+ maxLabelWidth := 0
2367
+ itemCount := 0
2368
+ ruler , _ := textmeasure .NewRuler ()
2369
+ if ruler != nil {
2370
+ for _ , s := range diagram .Legend .Shapes {
2371
+ if s .Label == "" {
2372
+ continue
2373
+ }
2374
+ mtext := & d2target.MText {
2375
+ Text : s .Label ,
2376
+ FontSize : LEGEND_FONT_SIZE ,
2377
+ }
2378
+ dims := d2graph .GetTextDimensions (nil , ruler , mtext , nil )
2379
+ maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
2380
+ totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
2381
+ itemCount ++
2382
+ }
2383
+
2384
+ for _ , c := range diagram .Legend .Connections {
2385
+ if c .Label == "" {
2386
+ continue
2387
+ }
2388
+ mtext := & d2target.MText {
2389
+ Text : c .Label ,
2390
+ FontSize : LEGEND_FONT_SIZE ,
2391
+ }
2392
+ dims := d2graph .GetTextDimensions (nil , ruler , mtext , nil )
2393
+ maxLabelWidth = go2 .IntMax (maxLabelWidth , dims .Width )
2394
+ totalHeight += go2 .IntMax (dims .Height , LEGEND_ICON_SIZE ) + LEGEND_ITEM_SPACING
2395
+ itemCount ++
2396
+ }
2397
+
2398
+ if itemCount > 0 {
2399
+ totalHeight -= LEGEND_ITEM_SPACING / 2
2400
+ }
2401
+
2402
+ totalHeight += LEGEND_PADDING
2403
+
2404
+ if totalHeight > 0 && maxLabelWidth > 0 {
2405
+ legendWidth := LEGEND_PADDING * 2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
2406
+
2407
+ legendY := br .Y - totalHeight
2408
+ if legendY < tl .Y {
2409
+ legendY = tl .Y
2410
+ }
2411
+
2412
+ legendRight := br .X + LEGEND_CORNER_PADDING + legendWidth
2413
+ if left + w < legendRight {
2414
+ w = legendRight - left + pad / 2
2415
+ }
2416
+
2417
+ if legendY < top {
2418
+ diffY := top - legendY
2419
+ top -= diffY
2420
+ h += diffY
2421
+ }
2422
+
2423
+ legendBottom := legendY + totalHeight
2424
+ if top + h < legendBottom {
2425
+ h = legendBottom - top + pad / 2
2426
+ }
2427
+ }
2428
+ }
2429
+ }
2090
2430
fmt .Fprint (buf , strings .Join ([]string {
2091
2431
fmt .Sprintf (`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">` ,
2092
2432
isolatedDiagramHash , left , top , w , h ,
0 commit comments