diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index d3ade4923..50ccdb876 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -1,5 +1,7 @@ #### Features ๐Ÿš€ +- Tooltips on shapes. See [https://d2lang.com/tour/tooltips](https://d2lang.com/tour/tooltips). [#545](https://github.com/terrastruct/d2/pull/545) + #### Improvements ๐Ÿงน #### Bugfixes โ›‘๏ธ diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index da137cc12..6ab44e7f1 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -857,7 +857,7 @@ func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t return nil, fmt.Errorf("text not pre-measured and no ruler provided") } -func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) *d2target.TextDimensions { +func GetTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) *d2target.TextDimensions { if dims := findMeasured(mtexts, t); dims != nil { return dims } @@ -889,7 +889,7 @@ func getTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2 } func appendTextDedup(texts []*d2target.MText, t *d2target.MText) []*d2target.MText { - if getTextDimensions(texts, nil, t, nil) == nil { + if GetTextDimensions(texts, nil, t, nil) == nil { return append(texts, t) } return texts @@ -922,13 +922,13 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler return err } } else { - dims = getTextDimensions(mtexts, ruler, obj.Text(), fontFamily) + dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily) } innerLabelPadding = 0 } else if obj.Attributes.Shape.Value == d2target.ShapeClass { - dims = getTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro)) + dims = GetTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro)) } else { - dims = getTextDimensions(mtexts, ruler, obj.Text(), fontFamily) + dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily) } if dims == nil { if obj.Attributes.Shape.Value == d2target.ShapeImage { @@ -982,7 +982,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler maxWidth := dims.Width for _, f := range obj.Class.Fields { - fdims := getTextDimensions(mtexts, ruler, f.Text(), go2.Pointer(d2fonts.SourceCodePro)) + fdims := GetTextDimensions(mtexts, ruler, f.Text(), go2.Pointer(d2fonts.SourceCodePro)) if fdims == nil { return fmt.Errorf("dimensions for class field %#v not found", f.Text()) } @@ -992,7 +992,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler } } for _, m := range obj.Class.Methods { - mdims := getTextDimensions(mtexts, ruler, m.Text(), go2.Pointer(d2fonts.SourceCodePro)) + mdims := GetTextDimensions(mtexts, ruler, m.Text(), go2.Pointer(d2fonts.SourceCodePro)) if mdims == nil { return fmt.Errorf("dimensions for class method %#v not found", m.Text()) } @@ -1011,7 +1011,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler } if anyRowText != nil { // 10px of padding top and bottom so text doesn't look squished - rowHeight := getTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + 20 + rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + 20 obj.Height = float64(rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2)) } // Leave room for padding @@ -1027,7 +1027,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler c := &obj.SQLTable.Columns[i] ctexts := c.Texts() - nameDims := getTextDimensions(mtexts, ruler, ctexts[0], fontFamily) + nameDims := GetTextDimensions(mtexts, ruler, ctexts[0], fontFamily) if nameDims == nil { return fmt.Errorf("dimensions for sql_table name %#v not found", ctexts[0].Text) } @@ -1037,7 +1037,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler maxNameWidth = nameDims.Width } - typeDims := getTextDimensions(mtexts, ruler, ctexts[1], fontFamily) + typeDims := GetTextDimensions(mtexts, ruler, ctexts[1], fontFamily) if typeDims == nil { return fmt.Errorf("dimensions for sql_table type %#v not found", ctexts[1].Text) } @@ -1072,7 +1072,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler for _, label := range endpointLabels { t := edge.Text() t.Text = label - dims := getTextDimensions(mtexts, ruler, t, fontFamily) + dims := GetTextDimensions(mtexts, ruler, t, fontFamily) edge.MinWidth += dims.Width // Some padding as it's not totally near the end edge.MinHeight += dims.Height + 5 @@ -1082,7 +1082,7 @@ func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler continue } - dims := getTextDimensions(mtexts, ruler, edge.Text(), fontFamily) + dims := GetTextDimensions(mtexts, ruler, edge.Text(), fontFamily) if dims == nil { return fmt.Errorf("dimensions for edge label %#v not found", edge.Text()) } diff --git a/d2renderers/d2svg/appendix/appendix.go b/d2renderers/d2svg/appendix/appendix.go new file mode 100644 index 000000000..8dbc399cb --- /dev/null +++ b/d2renderers/d2svg/appendix/appendix.go @@ -0,0 +1,180 @@ +// appendix.go writes appendices/footnotes to SVG +// Intended to be run only for static exports, like PNG or PDF. +// SVG exports are already interactive. + +package appendix + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "oss.terrastruct.com/d2/d2graph" + "oss.terrastruct.com/d2/d2renderers/d2fonts" + "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/d2target" + "oss.terrastruct.com/d2/lib/textmeasure" + "oss.terrastruct.com/util-go/go2" +) + +// โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +// โ”‚ โ”‚ +// โ”‚ DIAGRAM โ”‚ +// โ”‚ โ”‚ +// PAD_ โ”‚ โ”‚ +// SIDES โ”‚ โ”‚ +// โ”‚ โ”‚ โ”‚ +// โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +// โ–ผ โ—„โ”€โ”€โ”€โ”€โ”€โ”€ PAD_TOP +// +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// +// +// 1. asdfasdf +// +// โ—„โ”€โ”€โ”€โ”€ SPACER +// 2. qwerqwer +// +// + +const ( + PAD_TOP = 50 + PAD_SIDES = 40 + SPACER = 20 + + FONT_SIZE = 16 + ICON_RADIUS = 16 +) + +var viewboxRegex = regexp.MustCompile(`viewBox=\"([0-9\- ]+)\"`) +var widthRegex = regexp.MustCompile(`width=\"([0-9]+)\"`) +var heightRegex = regexp.MustCompile(`height=\"([0-9]+)\"`) + +func AppendTooltips(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []byte { + svg := string(in) + + appendix, w, h := generateTooltipAppendix(diagram, ruler, svg) + + if h == 0 { + return in + } + + viewboxMatch := viewboxRegex.FindStringSubmatch(svg) + viewboxRaw := viewboxMatch[1] + viewboxSlice := strings.Split(viewboxRaw, " ") + viewboxPadLeft, _ := strconv.Atoi(viewboxSlice[0]) + viewboxWidth, _ := strconv.Atoi(viewboxSlice[2]) + viewboxHeight, _ := strconv.Atoi(viewboxSlice[3]) + + tl, br := diagram.BoundingBox() + seperator := fmt.Sprintf(``, + tl.X-PAD_SIDES, br.Y+PAD_TOP, go2.IntMax(w, br.X)+PAD_SIDES, br.Y+PAD_TOP) + appendix = seperator + appendix + + w -= viewboxPadLeft + w += PAD_SIDES * 2 + if viewboxWidth < w { + viewboxWidth = w + } + + viewboxHeight += h + PAD_TOP + + newViewbox := fmt.Sprintf(`viewBox="%s %s %s %s"`, viewboxSlice[0], viewboxSlice[1], strconv.Itoa(viewboxWidth), strconv.Itoa(viewboxHeight)) + + widthMatch := widthRegex.FindStringSubmatch(svg) + heightMatch := heightRegex.FindStringSubmatch(svg) + newWidth := fmt.Sprintf(`width="%s"`, strconv.Itoa(viewboxWidth)) + newHeight := fmt.Sprintf(`height="%s"`, strconv.Itoa(viewboxHeight)) + + svg = strings.Replace(svg, viewboxMatch[0], newViewbox, 1) + svg = strings.Replace(svg, widthMatch[0], newWidth, 1) + svg = strings.Replace(svg, heightMatch[0], newHeight, 1) + + if !strings.Contains(svg, `font-family: "font-regular"`) { + appendix += fmt.Sprintf(``, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)]) + } + if !strings.Contains(svg, `font-family: "font-bold"`) { + appendix += fmt.Sprintf(``, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)]) + } + + closingIndex := strings.LastIndex(svg, "") + svg = svg[:closingIndex] + appendix + svg[closingIndex:] + + i := 1 + for _, s := range diagram.Shapes { + if s.Tooltip != "" { + // The clip-path has a unique ID, so this won't replace any user icons + // In the existing SVG, the transform places it top-left, so we adjust + svg = strings.Replace(svg, d2svg.TooltipIcon, generateNumberedIcon(i, 0, ICON_RADIUS), 1) + i++ + } + } + + return []byte(svg) +} + +func generateTooltipAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg string) (string, int, int) { + tl, br := diagram.BoundingBox() + + maxWidth, totalHeight := 0, 0 + + var tooltipLines []string + i := 1 + for _, s := range diagram.Shapes { + if s.Tooltip != "" { + line, w, h := generateTooltipLine(i, br.Y+(PAD_TOP*2)+totalHeight, s.Tooltip, ruler) + i++ + tooltipLines = append(tooltipLines, line) + maxWidth = go2.IntMax(maxWidth, w) + totalHeight += h + SPACER + } + } + + return fmt.Sprintf(`%s +`, tl.X, br.Y, (br.X - tl.X), strings.Join(tooltipLines, "\n")), maxWidth, totalHeight +} + +func generateNumberedIcon(i, x, y int) string { + line := fmt.Sprintf(``, + x+ICON_RADIUS, y, ICON_RADIUS) + + line += fmt.Sprintf(`%d`, + x+ICON_RADIUS, y+5, FONT_SIZE, i) + + return line +} + +func generateTooltipLine(i, y int, text string, ruler *textmeasure.Ruler) (string, int, int) { + mtext := &d2target.MText{ + Text: text, + FontSize: FONT_SIZE, + } + + dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil) + + // TODO box-shadow: 0px 0px 32px rgba(31, 36, 58, 0.1); + line := fmt.Sprintf(`%s`, + 0, y, generateNumberedIcon(i, 0, 0)) + + line += fmt.Sprintf(`%s`, + ICON_RADIUS*3, y, FONT_SIZE, d2svg.RenderText(text, ICON_RADIUS*3, float64(dims.Height))) + + return line, dims.Width + ICON_RADIUS*3, dims.Height +} diff --git a/d2renderers/d2svg/appendix/appendix_test.go b/d2renderers/d2svg/appendix/appendix_test.go new file mode 100644 index 000000000..a589498cf --- /dev/null +++ b/d2renderers/d2svg/appendix/appendix_test.go @@ -0,0 +1,144 @@ +package appendix_test + +import ( + "context" + "encoding/xml" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "cdr.dev/slog" + + tassert "github.com/stretchr/testify/assert" + + "oss.terrastruct.com/util-go/assert" + "oss.terrastruct.com/util-go/diff" + + "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" + "oss.terrastruct.com/d2/d2lib" + "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/d2renderers/d2svg/appendix" + "oss.terrastruct.com/d2/lib/log" + "oss.terrastruct.com/d2/lib/textmeasure" +) + +func TestAppendix(t *testing.T) { + t.Parallel() + + tcs := []testCase{ + { + name: "tooltip_wider_than_diagram", + script: `x: { tooltip: Total abstinence is easier than perfect moderation } +y: { tooltip: Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS! } +x -> y +`, + }, + { + name: "diagram_wider_than_tooltip", + script: `shape: sequence_diagram + +customer +issuer +store: { tooltip: Like starbucks or something } +acquirer: { tooltip: I'm not sure what this is } +network +customer bank +store bank + +customer: {shape: person} +customer bank: { + shape: image + icon: https://cdn-icons-png.flaticon.com/512/858/858170.png +} +store bank: { + shape: image + icon: https://cdn-icons-png.flaticon.com/512/858/858170.png +} + +initial transaction: { + customer -> store: 1 banana please + store -> customer: '$10 dollars' +} +customer.internal -> customer.internal: "thinking: wow, inflation" +customer.internal -> customer bank: checks bank account +customer bank -> customer.internal: 'Savings: $11' +customer."An error in judgement is about to occur" +customer -> store: I can do that, here's my card +payment processor behind the scenes: { + store -> acquirer: Run this card + acquirer -> network: Process to card issuer + simplified: { + network -> issuer: Process this payment + issuer -> customer bank: '$10 debit' + acquirer -> store bank: '$10 credit' + } +} +`, + }, + } + runa(t, tcs) +} + +type testCase struct { + name string + script string + skip bool +} + +func runa(t *testing.T, tcs []testCase) { + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + if tc.skip { + t.Skip() + } + t.Parallel() + + run(t, tc) + }) + } +} + +func run(t *testing.T, tc testCase) { + ctx := context.Background() + ctx = log.WithTB(ctx, t, nil) + ctx = log.Leveled(ctx, slog.LevelDebug) + + ruler, err := textmeasure.NewRuler() + if !tassert.Nil(t, err) { + return + } + + diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{ + Ruler: ruler, + ThemeID: 0, + Layout: d2dagrelayout.Layout, + }) + if !tassert.Nil(t, err) { + return + } + + dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestAppendix/")) + pathGotSVG := filepath.Join(dataPath, "sketch.got.svg") + + svgBytes, err := d2svg.Render(diagram, &d2svg.RenderOpts{ + Pad: d2svg.DEFAULT_PADDING, + }) + assert.Success(t, err) + svgBytes = appendix.AppendTooltips(diagram, ruler, svgBytes) + + err = os.MkdirAll(dataPath, 0755) + assert.Success(t, err) + err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600) + assert.Success(t, err) + defer os.Remove(pathGotSVG) + + var xmlParsed interface{} + err = xml.Unmarshal(svgBytes, &xmlParsed) + assert.Success(t, err) + + err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes) + assert.Success(t, err) +} diff --git a/d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg b/d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg new file mode 100644 index 000000000..08a8b08b2 --- /dev/null +++ b/d2renderers/d2svg/appendix/testdata/diagram_wider_than_tooltip/sketch.exp.svg @@ -0,0 +1,61 @@ + +customerissuerstore1Like starbucks or somethingacquirer2I'm not sure what this isnetworkcustomer bankstore bankinitial transactionpayment processor behind the scenessimplified 1 banana please$10 dollarsthinking: wow, inflationchecks bank accountSavings: $11I can do that, here's my cardRun this cardProcess to card issuerProcess this payment$10 debit$10 creditAn error in judgement is about to occur + + + + + + + + + + + + + + + +1Like starbucks or something +2I'm not sure what this is + \ No newline at end of file diff --git a/d2renderers/d2svg/appendix/testdata/tooltip_wider_than_diagram/sketch.exp.svg b/d2renderers/d2svg/appendix/testdata/tooltip_wider_than_diagram/sketch.exp.svg new file mode 100644 index 000000000..0a9140175 --- /dev/null +++ b/d2renderers/d2svg/appendix/testdata/tooltip_wider_than_diagram/sketch.exp.svg @@ -0,0 +1,42 @@ + +x1Total abstinence is easier than perfect moderationy2Gee, I feel kind of LIGHT in the head now, +knowing I can't make my satellite dish PAYMENTS! + + +1Total abstinence is easier than perfect moderation +2Gee, I feel kind of LIGHT in the head now,knowing I can't make my satellite dish PAYMENTS! + \ No newline at end of file diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 90e7bb191..d6a799274 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -39,10 +39,15 @@ const ( DEFAULT_PADDING = 100 MIN_ARROWHEAD_STROKE_WIDTH = 2 threeDeeOffset = 15 + + tooltipIconRadius = 16 ) var multipleOffset = geo.NewVector(10, -10) +//go:embed tooltip.svg +var TooltipIcon string + //go:embed style.css var styleCSS string @@ -437,7 +442,7 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co fontClass, x, y, textStyle, - renderText(connection.Label, x, float64(connection.LabelHeight)), + RenderText(connection.Label, x, float64(connection.LabelHeight)), ) } @@ -473,7 +478,7 @@ func renderArrowheadLabel(connection d2target.Connection, text string, position, return fmt.Sprintf(`%s`, x, y, textStyle, - renderText(text, x, height), + RenderText(text, x, height), ) } @@ -828,18 +833,28 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske fontClass, x, y, textStyle, - renderText(targetShape.Label, x, float64(targetShape.LabelHeight)), + RenderText(targetShape.Label, x, float64(targetShape.LabelHeight)), ) if targetShape.Blend { labelMask = makeLabelMask(labelTL, targetShape.LabelWidth, targetShape.LabelHeight-d2graph.INNER_LABEL_PADDING) } } } + + if targetShape.Tooltip != "" { + fmt.Fprintf(writer, `%s`, + targetShape.Pos.X+targetShape.Width-tooltipIconRadius, + targetShape.Pos.Y-tooltipIconRadius, + TooltipIcon, + ) + fmt.Fprintf(writer, `%s`, targetShape.Tooltip) + } + fmt.Fprintf(writer, ``) return labelMask, nil } -func renderText(text string, x, height float64) string { +func RenderText(text string, x, height float64) string { if !strings.Contains(text, "\n") { return svg.EscapeText(text) } @@ -936,6 +951,20 @@ func embedFonts(buf *bytes.Buffer, fontFamily *d2fonts.FontFamily) { } } + triggers = []string{ + `tooltip-icon`, + } + + for _, t := range triggers { + if strings.Contains(content, t) { + buf.WriteString(` +.tooltip-icon { + box-shadow: 0px 0px 32px rgba(31, 36, 58, 0.1); +}`) + break + } + } + triggers = []string{ `class="text-bold"`, ``, diff --git a/d2renderers/d2svg/tooltip.svg b/d2renderers/d2svg/tooltip.svg new file mode 100644 index 000000000..eb29cc2eb --- /dev/null +++ b/d2renderers/d2svg/tooltip.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index 2df367f50..ed8a52027 100644 --- a/e2etests/stable_test.go +++ b/e2etests/stable_test.go @@ -1624,6 +1624,13 @@ mama bear: { shape: text; style.font-size: 28; style.italic: true } papa bear: { shape: text; style.font-size: 32; style.underline: true } mama bear -> bear papa bear -> bear +`, + }, + { + name: "tooltips", + script: `x: { tooltip: Total abstinence is easier than perfect moderation } +y: { tooltip: Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS! } +x -> y `, }, } diff --git a/e2etests/testdata/stable/tooltips/dagre/board.exp.json b/e2etests/testdata/stable/tooltips/dagre/board.exp.json new file mode 100644 index 000000000..79b8db45b --- /dev/null +++ b/e2etests/testdata/stable/tooltips/dagre/board.exp.json @@ -0,0 +1,136 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "x", + "type": "", + "pos": { + "x": 1, + "y": 0 + }, + "width": 113, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "Total abstinence is easier than perfect moderation", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "x", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "y", + "type": "", + "pos": { + "x": 0, + "y": 226 + }, + "width": 114, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS!", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "y", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 14, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(x -> y)[0]", + "src": "x", + "srcArrow": "none", + "srcLabel": "", + "dst": "y", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 57, + "y": 126 + }, + { + "x": 57, + "y": 166 + }, + { + "x": 57, + "y": 186 + }, + { + "x": 57, + "y": 226 + } + ], + "isCurve": true, + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/tooltips/dagre/sketch.exp.svg b/e2etests/testdata/stable/tooltips/dagre/sketch.exp.svg new file mode 100644 index 000000000..047bdc2bd --- /dev/null +++ b/e2etests/testdata/stable/tooltips/dagre/sketch.exp.svg @@ -0,0 +1,58 @@ + +x + + + + + + + + + + + + +Total abstinence is easier than perfect moderationy + + + + + + + + + + + + +Gee, I feel kind of LIGHT in the head now, +knowing I can't make my satellite dish PAYMENTS! + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/tooltips/elk/board.exp.json b/e2etests/testdata/stable/tooltips/elk/board.exp.json new file mode 100644 index 000000000..24850e39e --- /dev/null +++ b/e2etests/testdata/stable/tooltips/elk/board.exp.json @@ -0,0 +1,127 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "x", + "type": "", + "pos": { + "x": 12, + "y": 12 + }, + "width": 113, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "Total abstinence is easier than perfect moderation", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "x", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "y", + "type": "", + "pos": { + "x": 12, + "y": 238 + }, + "width": 114, + "height": 126, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#F7F8FE", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "Gee, I feel kind of LIGHT in the head now,\nknowing I can't make my satellite dish PAYMENTS!", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "y", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 14, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + } + ], + "connections": [ + { + "id": "(x -> y)[0]", + "src": "x", + "srcArrow": "none", + "srcLabel": "", + "dst": "y", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 69, + "y": 138 + }, + { + "x": 69, + "y": 238 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/tooltips/elk/sketch.exp.svg b/e2etests/testdata/stable/tooltips/elk/sketch.exp.svg new file mode 100644 index 000000000..8c584ce14 --- /dev/null +++ b/e2etests/testdata/stable/tooltips/elk/sketch.exp.svg @@ -0,0 +1,58 @@ + +x + + + + + + + + + + + + +Total abstinence is easier than perfect moderationy + + + + + + + + + + + + +Gee, I feel kind of LIGHT in the head now, +knowing I can't make my satellite dish PAYMENTS! + + + \ No newline at end of file diff --git a/main.go b/main.go index 45d154f1f..e87b5149e 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "oss.terrastruct.com/d2/d2plugin" "oss.terrastruct.com/d2/d2renderers/d2fonts" "oss.terrastruct.com/d2/d2renderers/d2svg" + "oss.terrastruct.com/d2/d2renderers/d2svg/appendix" "oss.terrastruct.com/d2/d2themes" "oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/d2/lib/imgbundler" @@ -247,7 +248,8 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc out := svg if filepath.Ext(outputPath) == ".png" { - svg := svg + svg := appendix.AppendTooltips(diagram, ruler, svg) + if !bundle { var bundleErr2 error svg, bundleErr2 = imgbundler.BundleRemote(ctx, ms, svg)