d2svg: implement legend

This commit is contained in:
Alexander Wang 2025-03-11 15:30:05 -06:00
parent 38851ef88d
commit 3e4397f6b6
No known key found for this signature in database
GPG key ID: BE3937D0D52D8927
8 changed files with 5005 additions and 0 deletions

View file

@ -44,6 +44,13 @@ const (
DEFAULT_PADDING = 100
appendixIconRadius = 16
// Legend constants
LEGEND_PADDING = 20
LEGEND_ITEM_SPACING = 15
LEGEND_ICON_SIZE = 24
LEGEND_FONT_SIZE = 14
LEGEND_CORNER_PADDING = 10
)
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
return left, top, width, height
}
func renderLegend(buf *bytes.Buffer, diagram *d2target.Diagram, diagramHash string, theme *d2themes.Theme) error {
if diagram.Legend == nil || (len(diagram.Legend.Shapes) == 0 && len(diagram.Legend.Connections) == 0) {
return nil
}
_, br := diagram.BoundingBox()
ruler, err := textmeasure.NewRuler()
if err != nil {
return err
}
totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
maxLabelWidth := 0
itemCount := 0
for _, s := range diagram.Legend.Shapes {
if s.Label == "" {
continue
}
mtext := &d2target.MText{
Text: s.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
for _, c := range diagram.Legend.Connections {
if c.Label == "" {
continue
}
mtext := &d2target.MText{
Text: c.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
if itemCount > 0 {
totalHeight -= LEGEND_ITEM_SPACING / 2
}
if itemCount > 0 && len(diagram.Legend.Connections) > 0 {
totalHeight += LEGEND_PADDING * 1.5
} else {
totalHeight += LEGEND_PADDING * 1.2
}
legendWidth := LEGEND_PADDING*2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
legendX := br.X + LEGEND_CORNER_PADDING
tl, _ := diagram.BoundingBox()
legendY := br.Y - totalHeight
if legendY < tl.Y {
legendY = tl.Y
}
shadowEl := d2themes.NewThemableElement("rect", theme)
shadowEl.Fill = "#F7F7FA"
shadowEl.Stroke = "#DEE1EB"
shadowEl.Style = "stroke-width: 1px; filter: drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.1))"
shadowEl.X = float64(legendX)
shadowEl.Y = float64(legendY)
shadowEl.Width = float64(legendWidth)
shadowEl.Height = float64(totalHeight)
shadowEl.Rx = 4
fmt.Fprint(buf, shadowEl.Render())
legendEl := d2themes.NewThemableElement("rect", theme)
legendEl.Fill = "#ffffff"
legendEl.Stroke = "#DEE1EB"
legendEl.Style = "stroke-width: 1px"
legendEl.X = float64(legendX)
legendEl.Y = float64(legendY)
legendEl.Width = float64(legendWidth)
legendEl.Height = float64(totalHeight)
legendEl.Rx = 4
fmt.Fprint(buf, legendEl.Render())
fmt.Fprintf(buf, `<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;">Legend</text>`,
legendX+LEGEND_PADDING, legendY+LEGEND_PADDING+LEGEND_FONT_SIZE, LEGEND_FONT_SIZE+2)
currentY := legendY + LEGEND_PADDING*2 + LEGEND_FONT_SIZE
shapeCount := 0
for _, s := range diagram.Legend.Shapes {
if s.Label == "" {
continue
}
iconX := legendX + LEGEND_PADDING
iconY := currentY
shapeIcon, err := renderLegendShapeIcon(s, iconX, iconY, diagramHash, theme)
if err != nil {
return err
}
fmt.Fprint(buf, shapeIcon)
mtext := &d2target.MText{
Text: s.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
rowHeight := go2.IntMax(dims.Height, LEGEND_ICON_SIZE)
textY := currentY + rowHeight/2 + int(float64(dims.Height)*0.3)
fmt.Fprintf(buf, `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
iconX+LEGEND_ICON_SIZE+LEGEND_PADDING, textY, LEGEND_FONT_SIZE,
html.EscapeString(s.Label))
currentY += rowHeight + LEGEND_ITEM_SPACING
shapeCount++
}
if shapeCount > 0 && len(diagram.Legend.Connections) > 0 {
currentY += LEGEND_ITEM_SPACING / 2
separatorEl := d2themes.NewThemableElement("line", theme)
separatorEl.X1 = float64(legendX + LEGEND_PADDING)
separatorEl.Y1 = float64(currentY)
separatorEl.X2 = float64(legendX + legendWidth - LEGEND_PADDING)
separatorEl.Y2 = float64(currentY)
separatorEl.Stroke = "#DEE1EB"
separatorEl.StrokeDashArray = "2,2"
fmt.Fprint(buf, separatorEl.Render())
currentY += LEGEND_ITEM_SPACING
}
for _, c := range diagram.Legend.Connections {
if c.Label == "" {
continue
}
iconX := legendX + LEGEND_PADDING
iconY := currentY + LEGEND_ICON_SIZE/2
connIcon, err := renderLegendConnectionIcon(c, iconX, iconY, theme)
if err != nil {
return err
}
fmt.Fprint(buf, connIcon)
mtext := &d2target.MText{
Text: c.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
rowHeight := go2.IntMax(dims.Height, LEGEND_ICON_SIZE)
textY := currentY + rowHeight/2 + int(float64(dims.Height)*0.2)
fmt.Fprintf(buf, `<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
iconX+LEGEND_ICON_SIZE+LEGEND_PADDING, textY, LEGEND_FONT_SIZE,
html.EscapeString(c.Label))
currentY += rowHeight + LEGEND_ITEM_SPACING
}
if shapeCount > 0 && len(diagram.Legend.Connections) > 0 {
currentY += LEGEND_PADDING / 2
} else {
currentY += LEGEND_PADDING / 4
}
return nil
}
func renderLegendShapeIcon(s d2target.Shape, x, y int, diagramHash string, theme *d2themes.Theme) (string, error) {
iconShape := s
const sizeFactor = 5
iconShape.Pos.X = 0
iconShape.Pos.Y = 0
iconShape.Width = LEGEND_ICON_SIZE * sizeFactor
iconShape.Height = LEGEND_ICON_SIZE * sizeFactor
iconShape.Label = ""
buf := &bytes.Buffer{}
appendixBuf := &bytes.Buffer{}
finalBuf := &bytes.Buffer{}
fmt.Fprintf(finalBuf, `<g transform="translate(%d, %d) scale(%f)">`,
x, y, 1.0/sizeFactor)
_, err := drawShape(buf, appendixBuf, diagramHash, iconShape, nil, theme)
if err != nil {
return "", err
}
fmt.Fprint(finalBuf, buf.String())
fmt.Fprint(finalBuf, `</g>`)
return finalBuf.String(), nil
}
func renderLegendConnectionIcon(c d2target.Connection, x, y int, theme *d2themes.Theme) (string, error) {
finalBuf := &bytes.Buffer{}
buf := &bytes.Buffer{}
const sizeFactor = 2
legendConn := *d2target.BaseConnection()
legendConn.ID = c.ID
legendConn.SrcArrow = c.SrcArrow
legendConn.DstArrow = c.DstArrow
legendConn.StrokeDash = c.StrokeDash
legendConn.StrokeWidth = c.StrokeWidth
legendConn.Stroke = c.Stroke
legendConn.Fill = c.Fill
legendConn.BorderRadius = c.BorderRadius
legendConn.Opacity = c.Opacity
legendConn.Animated = c.Animated
startX := 0.0
midY := 0.0
width := float64(LEGEND_ICON_SIZE * sizeFactor)
legendConn.Route = []*geo.Point{
{X: startX, Y: midY},
{X: startX + width, Y: midY},
}
legendHash := fmt.Sprintf("legend-%s", hash(fmt.Sprintf("%s-%d-%d", c.ID, x, y)))
markers := make(map[string]struct{})
idToShape := make(map[string]d2target.Shape)
fmt.Fprintf(finalBuf, `<g transform="translate(%d, %d) scale(%f)">`,
x, y, 1.0/sizeFactor)
_, err := drawConnection(buf, legendHash, legendConn, markers, idToShape, nil, theme)
if err != nil {
return "", err
}
fmt.Fprint(finalBuf, buf.String())
fmt.Fprint(finalBuf, `</g>`)
return finalBuf.String(), nil
}
func arrowheadMarkerID(diagramHash string, isTarget bool, connection d2target.Connection) string {
var arrowhead d2target.Arrowhead
if isTarget {
@ -2085,8 +2348,85 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
// add all appendix items afterwards so they are always on top
fmt.Fprint(buf, appendixItemBuf)
if diagram.Legend != nil && (len(diagram.Legend.Shapes) > 0 || len(diagram.Legend.Connections) > 0) {
legendBuf := &bytes.Buffer{}
err := renderLegend(legendBuf, diagram, diagramHash, inlineTheme)
if err != nil {
return nil, err
}
fmt.Fprint(buf, legendBuf)
}
// Note: we always want this since we reference it on connections even if there end up being no masked labels
left, top, w, h := dimensions(diagram, pad)
if diagram.Legend != nil && (len(diagram.Legend.Shapes) > 0 || len(diagram.Legend.Connections) > 0) {
tl, br := diagram.BoundingBox()
totalHeight := LEGEND_PADDING + LEGEND_FONT_SIZE + LEGEND_ITEM_SPACING
maxLabelWidth := 0
itemCount := 0
ruler, _ := textmeasure.NewRuler()
if ruler != nil {
for _, s := range diagram.Legend.Shapes {
if s.Label == "" {
continue
}
mtext := &d2target.MText{
Text: s.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
for _, c := range diagram.Legend.Connections {
if c.Label == "" {
continue
}
mtext := &d2target.MText{
Text: c.Label,
FontSize: LEGEND_FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
maxLabelWidth = go2.IntMax(maxLabelWidth, dims.Width)
totalHeight += go2.IntMax(dims.Height, LEGEND_ICON_SIZE) + LEGEND_ITEM_SPACING
itemCount++
}
if itemCount > 0 {
totalHeight -= LEGEND_ITEM_SPACING / 2
}
totalHeight += LEGEND_PADDING
if totalHeight > 0 && maxLabelWidth > 0 {
legendWidth := LEGEND_PADDING*2 + LEGEND_ICON_SIZE + LEGEND_PADDING + maxLabelWidth
legendY := br.Y - totalHeight
if legendY < tl.Y {
legendY = tl.Y
}
legendRight := br.X + LEGEND_CORNER_PADDING + legendWidth
if left+w < legendRight {
w = legendRight - left + pad/2
}
if legendY < top {
diffY := top - legendY
top -= diffY
h += diffY
}
legendBottom := legendY + totalHeight
if top+h < legendBottom {
h = legendBottom - top + pad/2
}
}
}
}
fmt.Fprint(buf, strings.Join([]string{
fmt.Sprintf(`<mask id="%s" maskUnits="userSpaceOnUse" x="%d" y="%d" width="%d" height="%d">`,
isolatedDiagramHash, left, top, w, h,

2992
d2renderers/d2svg/d2svg.go-e Normal file

File diff suppressed because it is too large Load diff

View file

@ -456,6 +456,16 @@ func (diagram Diagram) GetCorpus() string {
}
}
if diagram.Legend != nil {
corpus += "Legend"
for _, s := range diagram.Legend.Shapes {
corpus += s.Label
}
for _, c := range diagram.Legend.Connections {
corpus += c.Label
}
}
return corpus
}

725
e2etests/testdata/txtar/legend/dagre/board.exp.json generated vendored Normal file
View file

@ -0,0 +1,725 @@
{
"name": "",
"config": {
"sketch": false,
"themeID": 0,
"darkThemeID": null,
"pad": null,
"center": null,
"layoutEngine": null
},
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "api-1",
"type": "rectangle",
"pos": {
"x": 0,
"y": 0
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "api-1",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "api-2",
"type": "rectangle",
"pos": {
"x": 141,
"y": 166
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "api-2",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "postgres",
"type": "cylinder",
"pos": {
"x": 18,
"y": 332
},
"width": 106,
"height": 118,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "AA4",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "postgres",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 61,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "external",
"type": "rectangle",
"pos": {
"x": 18,
"y": 550
},
"width": 105,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "external",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 60,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "api-3",
"type": "rectangle",
"pos": {
"x": 0,
"y": 166
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "api-3",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(api-1 -> postgres)[0]",
"src": "api-1",
"srcArrow": "none",
"dst": "postgres",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 8,
"y": 66
},
{
"x": -30.399999618530273,
"y": 106
},
{
"x": -40,
"y": 132.60000610351562
},
{
"x": -40,
"y": 157.5
},
{
"x": -40,
"y": 182.39999389648438
},
{
"x": -27.399999618530273,
"y": 294.3999938964844
},
{
"x": 23,
"y": 344
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(api-2 -> postgres)[0]",
"src": "api-2",
"srcArrow": "none",
"dst": "postgres",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 181.5,
"y": 232
},
{
"x": 181.5,
"y": 272
},
{
"x": 169,
"y": 294.3999938964844
},
{
"x": 119,
"y": 344
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(postgres -> external)[0]",
"src": "postgres",
"srcArrow": "none",
"dst": "external",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "black",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 71,
"y": 450
},
{
"x": 70.80000305175781,
"y": 490
},
{
"x": 70.75,
"y": 510
},
{
"x": 70.75,
"y": 550
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(api-1 <-> api-2)[0]",
"src": "api-1",
"srcArrow": "triangle",
"dst": "api-2",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 2,
"strokeWidth": 2,
"stroke": "red",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 80.5,
"y": 57
},
{
"x": 161.3000030517578,
"y": 104.19999694824219
},
{
"x": 181.5,
"y": 126
},
{
"x": 181.5,
"y": 166
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(api-1 -> api-3)[0]",
"src": "api-1",
"srcArrow": "none",
"dst": "api-3",
"dstArrow": "circle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 40.5,
"y": 66
},
{
"x": 40.5,
"y": 106
},
{
"x": 40.5,
"y": 126
},
{
"x": 40.5,
"y": 166
}
],
"isCurve": true,
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
],
"root": {
"id": "",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 0,
"height": 0,
"opacity": 0,
"strokeDash": 0,
"strokeWidth": 0,
"borderRadius": 0,
"fill": "N7",
"stroke": "",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 0,
"fontFamily": "",
"language": "",
"color": "",
"italic": false,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 0
},
"legend": {
"shapes": [
{
"id": "a",
"type": "rectangle",
"pos": {
"x": 10,
"y": 10
},
"width": 100,
"height": 100,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "Microservice",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 1
},
{
"id": "b",
"type": "cylinder",
"pos": {
"x": 10,
"y": 10
},
"width": 100,
"height": 100,
"opacity": 1,
"strokeDash": 2,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "AA4",
"stroke": "B2",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "Database",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(a <-> b)[0]",
"src": "a",
"srcArrow": "triangle",
"dst": "b",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 2,
"strokeWidth": 1,
"stroke": "red",
"borderRadius": 10,
"label": "Good relationship",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 10,
"y": 10
},
{
"x": 110,
"y": 10
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a -> b)[0]",
"src": "a",
"srcArrow": "none",
"dst": "b",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "Bad relationship",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 10,
"y": 10
},
{
"x": 110,
"y": 10
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a -> b)[1]",
"src": "a",
"srcArrow": "none",
"dst": "b",
"dstArrow": "circle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "Tenuous",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 10,
"y": 10
},
{
"x": 110,
"y": 10
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
]
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

684
e2etests/testdata/txtar/legend/elk/board.exp.json generated vendored Normal file
View file

@ -0,0 +1,684 @@
{
"name": "",
"config": {
"sketch": false,
"themeID": 0,
"darkThemeID": null,
"pad": null,
"center": null,
"layoutEngine": null
},
"isFolderOnly": false,
"fontFamily": "SourceSansPro",
"shapes": [
{
"id": "api-1",
"type": "rectangle",
"pos": {
"x": 45,
"y": 12
},
"width": 120,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "api-1",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "api-2",
"type": "rectangle",
"pos": {
"x": 65,
"y": 158
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "api-2",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "postgres",
"type": "cylinder",
"pos": {
"x": 12,
"y": 304
},
"width": 106,
"height": 118,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "AA4",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "postgres",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 61,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "external",
"type": "rectangle",
"pos": {
"x": 12,
"y": 492
},
"width": 105,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "external",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 60,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "api-3",
"type": "rectangle",
"pos": {
"x": 166,
"y": 158
},
"width": 81,
"height": 66,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "api-3",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(api-1 -> postgres)[0]",
"src": "api-1",
"srcArrow": "none",
"dst": "postgres",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 75.75,
"y": 78
},
{
"x": 75.75,
"y": 118
},
{
"x": 24.249000549316406,
"y": 118
},
{
"x": 24,
"y": 311
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(api-2 -> postgres)[0]",
"src": "api-2",
"srcArrow": "none",
"dst": "postgres",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 82.66600036621094,
"y": 224
},
{
"x": 83,
"y": 305
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(postgres -> external)[0]",
"src": "postgres",
"srcArrow": "none",
"dst": "external",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "black",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 65,
"y": 422
},
{
"x": 65,
"y": 492
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(api-1 <-> api-2)[0]",
"src": "api-1",
"srcArrow": "triangle",
"dst": "api-2",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 2,
"strokeWidth": 2,
"stroke": "red",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 105.75,
"y": 78
},
{
"x": 105.75,
"y": 158
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(api-1 -> api-3)[0]",
"src": "api-1",
"srcArrow": "none",
"dst": "api-3",
"dstArrow": "circle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 135.75,
"y": 78
},
{
"x": 135.75,
"y": 118
},
{
"x": 206.75,
"y": 118
},
{
"x": 206.75,
"y": 158
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
],
"root": {
"id": "",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 0,
"height": 0,
"opacity": 0,
"strokeDash": 0,
"strokeWidth": 0,
"borderRadius": 0,
"fill": "N7",
"stroke": "",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 0,
"fontFamily": "",
"language": "",
"color": "",
"italic": false,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 0
},
"legend": {
"shapes": [
{
"id": "a",
"type": "rectangle",
"pos": {
"x": 10,
"y": 10
},
"width": 100,
"height": 100,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "B6",
"stroke": "B1",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "Microservice",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 1
},
{
"id": "b",
"type": "cylinder",
"pos": {
"x": 10,
"y": 10
},
"width": 100,
"height": 100,
"opacity": 1,
"strokeDash": 2,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "AA4",
"stroke": "B2",
"animated": false,
"shadow": false,
"3d": false,
"multiple": false,
"double-border": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"blend": false,
"fields": null,
"methods": null,
"columns": null,
"label": "Database",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N1",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(a <-> b)[0]",
"src": "a",
"srcArrow": "triangle",
"dst": "b",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 2,
"strokeWidth": 1,
"stroke": "red",
"borderRadius": 10,
"label": "Good relationship",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 10,
"y": 10
},
{
"x": 110,
"y": 10
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a -> b)[0]",
"src": "a",
"srcArrow": "none",
"dst": "b",
"dstArrow": "triangle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "Bad relationship",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 10,
"y": 10
},
{
"x": 110,
"y": 10
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(a -> b)[1]",
"src": "a",
"srcArrow": "none",
"dst": "b",
"dstArrow": "circle",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "B1",
"borderRadius": 10,
"label": "Tenuous",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "N2",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"link": "",
"route": [
{
"x": 10,
"y": 10
},
{
"x": 110,
"y": 10
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
]
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1127,3 +1127,45 @@ customer -> email_system: "Sends e-mails to"
internet_banking_system.api_app -> email_system: "Sends e-mail using"
internet_banking_system.database <-> internet_banking_system.api_app: "Reads from and writes to\n[SQL/TCP]"
-- legend --
vars: {
d2-legend: {
a: {
label: Microservice
}
b: Database {
shape: cylinder
style.stroke-dash: 2
}
a <-> b: Good relationship {
style.stroke: red
style.stroke-dash: 2
style.stroke-width: 1
}
a -> b: Bad relationship
a -> b: Tenuous {
target-arrowhead.shape: circle
}
}
}
api-1
api-2
api-1 -> postgres
api-2 -> postgres
postgres: {
shape: cylinder
}
postgres -> external: {
style.stroke: black
}
api-1 <-> api-2: {
style.stroke: red
style.stroke-dash: 2
}
api-1 -> api-3: {
target-arrowhead.shape: circle
}