diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index 767ebf9ed..532aa4df1 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -7,6 +7,8 @@ - `d2 fmt` accepts multiple files to be formatted [#718](https://github.com/terrastruct/d2/issues/718) +- `font-size` works for `sql_table` and `class` shapes [#769](https://github.com/terrastruct/d2/issues/769) + - You can now use the reserved keywords `layers`/`scenarios`/`steps` to define diagrams with multiple levels of abstractions. [#714](https://github.com/terrastruct/d2/pull/714) Docs to come soon @@ -26,6 +28,7 @@ - Ensures shapes with icons have enough padding for their labels. [#702](https://github.com/terrastruct/d2/pull/702) - `--force-appendix` flag adds an appendix to SVG outputs with tooltips or links. [#761](https://github.com/terrastruct/d2/pull/761) - `d2 themes` subcommand to list themes. [#760](https://github.com/terrastruct/d2/pull/760) +- `sql_table` header left-aligned with column [#769](https://github.com/terrastruct/d2/pull/769) #### Bugfixes ⛑️ diff --git a/d2exporter/export.go b/d2exporter/export.go index 5a57f0881..418e71370 100644 --- a/d2exporter/export.go +++ b/d2exporter/export.go @@ -136,10 +136,10 @@ func toShape(obj *d2graph.Object, theme *d2themes.Theme) d2target.Shape { case d2target.ShapeClass: shape.Class = *obj.Class // The label is the header for classes and tables, which is set in client to be 4 px larger than the object's set font size - shape.FontSize -= 4 + shape.FontSize -= d2target.HeaderFontAdd case d2target.ShapeSQLTable: shape.SQLTable = *obj.SQLTable - shape.FontSize -= 4 + shape.FontSize -= d2target.HeaderFontAdd } shape.Label = text.Text shape.LabelWidth = text.Dimensions.Width diff --git a/d2graph/d2graph.go b/d2graph/d2graph.go index f74b14496..d907c1999 100644 --- a/d2graph/d2graph.go +++ b/d2graph/d2graph.go @@ -465,6 +465,11 @@ func (obj *Object) Text() *d2target.MText { isItalic = true } fontSize := d2fonts.FONT_SIZE_M + + if obj.Class != nil || obj.SQLTable != nil { + fontSize = d2fonts.FONT_SIZE_L + } + if obj.OuterSequenceDiagram() == nil { if obj.IsContainer() { fontSize = obj.Level().LabelSize() @@ -477,7 +482,7 @@ func (obj *Object) Text() *d2target.MText { } // Class and Table objects have Label set to header if obj.Class != nil || obj.SQLTable != nil { - fontSize = d2fonts.FONT_SIZE_XL + fontSize += d2target.HeaderFontAdd } if obj.Class != nil { isBold = false @@ -795,17 +800,22 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R case d2target.ShapeClass: maxWidth := go2.Max(12, labelDims.Width) + fontSize := d2fonts.FONT_SIZE_L + if obj.Attributes.Style.FontSize != nil { + fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) + } + for _, f := range obj.Class.Fields { - fdims := GetTextDimensions(mtexts, ruler, f.Text(), go2.Pointer(d2fonts.SourceCodePro)) + fdims := GetTextDimensions(mtexts, ruler, f.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro)) if fdims == nil { - return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text()) + return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text(fontSize)) } maxWidth = go2.Max(maxWidth, fdims.Width) } for _, m := range obj.Class.Methods { - mdims := GetTextDimensions(mtexts, ruler, m.Text(), go2.Pointer(d2fonts.SourceCodePro)) + mdims := GetTextDimensions(mtexts, ruler, m.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro)) if mdims == nil { - return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text()) + return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text(fontSize)) } maxWidth = go2.Max(maxWidth, mdims.Width) } @@ -820,9 +830,9 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R // All rows should be the same height var anyRowText *d2target.MText if len(obj.Class.Fields) > 0 { - anyRowText = obj.Class.Fields[0].Text() + anyRowText = obj.Class.Fields[0].Text(fontSize) } else if len(obj.Class.Methods) > 0 { - anyRowText = obj.Class.Methods[0].Text() + anyRowText = obj.Class.Methods[0].Text(fontSize) } if anyRowText != nil { rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + d2target.VerticalPadding @@ -836,10 +846,16 @@ func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.R maxTypeWidth := 0 constraintWidth := 0 + colFontSize := d2fonts.FONT_SIZE_L + if obj.Attributes.Style.FontSize != nil { + colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) + } + for i := range obj.SQLTable.Columns { // Note: we want to set dimensions of actual column not the for loop copy of the struct c := &obj.SQLTable.Columns[i] - ctexts := c.Texts() + + ctexts := c.Texts(colFontSize) nameDims := GetTextDimensions(mtexts, ruler, ctexts[0], fontFamily) if nameDims == nil { @@ -1327,15 +1343,23 @@ func (g *Graph) Texts() []*d2target.MText { texts = appendTextDedup(texts, obj.Text()) } if obj.Class != nil { + fontSize := d2fonts.FONT_SIZE_L + if obj.Attributes.Style.FontSize != nil { + fontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) + } for _, field := range obj.Class.Fields { - texts = appendTextDedup(texts, field.Text()) + texts = appendTextDedup(texts, field.Text(fontSize)) } for _, method := range obj.Class.Methods { - texts = appendTextDedup(texts, method.Text()) + texts = appendTextDedup(texts, method.Text(fontSize)) } } else if obj.SQLTable != nil { + colFontSize := d2fonts.FONT_SIZE_L + if obj.Attributes.Style.FontSize != nil { + colFontSize, _ = strconv.Atoi(obj.Attributes.Style.FontSize.Value) + } for _, column := range obj.SQLTable.Columns { - for _, t := range column.Texts() { + for _, t := range column.Texts(colFontSize) { texts = appendTextDedup(texts, t) } } diff --git a/d2target/class.go b/d2target/class.go index 3cc39921e..c08f11144 100644 --- a/d2target/class.go +++ b/d2target/class.go @@ -2,8 +2,6 @@ package d2target import ( "fmt" - - "oss.terrastruct.com/d2/d2renderers/d2fonts" ) const ( @@ -25,10 +23,10 @@ type ClassField struct { Visibility string `json:"visibility"` } -func (cf ClassField) Text() *MText { +func (cf ClassField) Text(fontSize int) *MText { return &MText{ Text: fmt.Sprintf("%s%s", cf.Name, cf.Type), - FontSize: d2fonts.FONT_SIZE_L, + FontSize: fontSize, IsBold: false, IsItalic: false, Shape: "class", @@ -52,10 +50,10 @@ type ClassMethod struct { Visibility string `json:"visibility"` } -func (cm ClassMethod) Text() *MText { +func (cm ClassMethod) Text(fontSize int) *MText { return &MText{ Text: fmt.Sprintf("%s%s", cm.Name, cm.Return), - FontSize: d2fonts.FONT_SIZE_L, + FontSize: fontSize, IsBold: false, IsItalic: false, Shape: "class", diff --git a/d2target/sqltable.go b/d2target/sqltable.go index 8332a34c8..6ffbddf05 100644 --- a/d2target/sqltable.go +++ b/d2target/sqltable.go @@ -1,11 +1,13 @@ package d2target -import "oss.terrastruct.com/d2/d2renderers/d2fonts" - const ( NamePadding = 10 TypePadding = 20 - HeaderPadding = 20 + HeaderPadding = 10 + + // Setting table font size sets it for columns + // The header needs to be a little larger for visual hierarchy + HeaderFontAdd = 4 ) type SQLTable struct { @@ -19,18 +21,18 @@ type SQLColumn struct { Reference string `json:"reference"` } -func (c SQLColumn) Texts() []*MText { +func (c SQLColumn) Texts(fontSize int) []*MText { return []*MText{ { Text: c.Name.Label, - FontSize: d2fonts.FONT_SIZE_L, + FontSize: fontSize, IsBold: false, IsItalic: false, Shape: "sql_table", }, { Text: c.Type.Label, - FontSize: d2fonts.FONT_SIZE_L, + FontSize: fontSize, IsBold: false, IsItalic: false, Shape: "sql_table", diff --git a/e2etests/regression_test.go b/e2etests/regression_test.go index f40d9589b..ab5785511 100644 --- a/e2etests/regression_test.go +++ b/e2etests/regression_test.go @@ -1,10 +1,7 @@ package e2etests import ( - "math" "testing" - - "oss.terrastruct.com/d2/d2target" ) func testRegression(t *testing.T) { @@ -462,23 +459,12 @@ class2: class without rows { shape: class } `, - assertions: func(t *testing.T, g *d2target.Diagram) { - if len(g.Shapes) != 2 { - t.Fatal("expected 2 shapes") - } - c1Height := float64(g.Shapes[0].Height) - c2Height := float64(g.Shapes[1].Height) - if math.Round(c1Height/2.) != c2Height { - t.Fatal("expected rowless class to be 1/2 height of class with 2 rows") - } - }, }, { name: "just-width", script: `x: "teamwork: having someone to blame" { width: 100 } - `, }, } diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index a489f6398..957451c08 100644 --- a/e2etests/stable_test.go +++ b/e2etests/stable_test.go @@ -1838,6 +1838,55 @@ x.y -> a.b: { style.animated: true target-arrowhead.shape: cf-many } +`, + }, + { + name: "sql_table_column_styles", + script: `Humor in the Court: { + shape: sql_table + Could you see him from where you were standing?: "I could see his head." + And where was his head?: Just above his shoulders. + style.fill: red + style.stroke: lightgray + style.font-color: orange + style.font-size: 20 +} + +Humor in the Court2: { + shape: sql_table + Could you see him from where you were standing?: "I could see his head." + And where was his head?: Just above his shoulders. + style.fill: red + style.stroke: lightgray + style.font-color: orange + style.font-size: 30 +} + +manager: BatchManager { + shape: class + style.font-size: 20 + + -num: int + -timeout: int + -pid + + +getStatus(): Enum + +getJobs(): "Job[]" + +setTimeout(seconds int) +} + +manager2: BatchManager { + shape: class + style.font-size: 30 + + -num: int + -timeout: int + -pid + + +getStatus(): Enum + +getJobs(): "Job[]" + +setTimeout(seconds int) +} `, }, { diff --git a/e2etests/testdata/regression/only_header_class_table/dagre/board.exp.json b/e2etests/testdata/regression/only_header_class_table/dagre/board.exp.json index 87d63c1cb..0b744a933 100644 --- a/e2etests/testdata/regression/only_header_class_table/dagre/board.exp.json +++ b/e2etests/testdata/regression/only_header_class_table/dagre/board.exp.json @@ -49,10 +49,10 @@ "id": "table", "type": "sql_table", "pos": { - "x": 341, + "x": 351, "y": 192 }, - "width": 401, + "width": 381, "height": 36, "opacity": 1, "strokeDash": 0, @@ -92,10 +92,10 @@ "id": "table with short col", "type": "sql_table", "pos": { - "x": 341, + "x": 351, "y": 328 }, - "width": 401, + "width": 381, "height": 72, "opacity": 1, "strokeDash": 0, diff --git a/e2etests/testdata/regression/only_header_class_table/dagre/sketch.exp.svg b/e2etests/testdata/regression/only_header_class_table/dagre/sketch.exp.svg index e10f558fc..547b27f30 100644 --- a/e2etests/testdata/regression/only_header_class_table/dagre/sketch.exp.svg +++ b/e2etests/testdata/regression/only_header_class_table/dagre/sketch.exp.svg @@ -39,9 +39,9 @@ width="1286" height="604" viewBox="-102 -102 1286 604">Humor in the CourtCould you see him from where you were standing? +I could see his head. +And where was his head? +Just above his shoulders. +Humor in the Court2Could you see him from where you were standing? +I could see his head. +And where was his head? +Just above his shoulders. +BatchManager- +num +int- +timeout +int- +pid ++ +getStatus() +Enum+ +getJobs() +Job[]+ +setTimeout(seconds int) +voidBatchManager- +num +int- +timeout +int- +pid ++ +getStatus() +Enum+ +getJobs() +Job[]+ +setTimeout(seconds int) +void + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/sql_table_column_styles/elk/board.exp.json b/e2etests/testdata/stable/sql_table_column_styles/elk/board.exp.json new file mode 100644 index 000000000..a3ac11503 --- /dev/null +++ b/e2etests/testdata/stable/sql_table_column_styles/elk/board.exp.json @@ -0,0 +1,357 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "Humor in the Court", + "type": "sql_table", + "pos": { + "x": 12, + "y": 190 + }, + "width": 678, + "height": 108, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "red", + "stroke": "lightgray", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": [ + { + "name": { + "label": "Could you see him from where you were standing?", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 418, + "labelHeight": 26 + }, + "type": { + "label": "I could see his head.", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 166, + "labelHeight": 26 + }, + "constraint": "", + "reference": "" + }, + { + "name": { + "label": "And where was his head?", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 208, + "labelHeight": 26 + }, + "type": { + "label": "Just above his shoulders.", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 210, + "labelHeight": 26 + }, + "constraint": "", + "reference": "" + } + ], + "label": "Humor in the Court", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "orange", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 202, + "labelHeight": 31, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + }, + { + "id": "Humor in the Court2", + "type": "sql_table", + "pos": { + "x": 710, + "y": 172 + }, + "width": 992, + "height": 144, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "red", + "stroke": "lightgray", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": [ + { + "name": { + "label": "Could you see him from where you were standing?", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 627, + "labelHeight": 38 + }, + "type": { + "label": "I could see his head.", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 248, + "labelHeight": 38 + }, + "constraint": "", + "reference": "" + }, + { + "name": { + "label": "And where was his head?", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 312, + "labelHeight": 38 + }, + "type": { + "label": "Just above his shoulders.", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 315, + "labelHeight": 38 + }, + "constraint": "", + "reference": "" + } + ], + "label": "Humor in the Court2", + "fontSize": 30, + "fontFamily": "DEFAULT", + "language": "", + "color": "orange", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 303, + "labelHeight": 43, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + }, + { + "id": "manager", + "type": "class", + "pos": { + "x": 1722, + "y": 60 + }, + "width": 422, + "height": 368, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#0A0F25", + "stroke": "#FFFFFF", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": [ + { + "name": "num", + "type": "int", + "visibility": "private" + }, + { + "name": "timeout", + "type": "int", + "visibility": "private" + }, + { + "name": "pid", + "type": "", + "visibility": "private" + } + ], + "methods": [ + { + "name": "getStatus()", + "return": "Enum", + "visibility": "public" + }, + { + "name": "getJobs()", + "return": "Job[]", + "visibility": "public" + }, + { + "name": "setTimeout(seconds int)", + "return": "void", + "visibility": "public" + } + ], + "columns": null, + "label": "BatchManager", + "fontSize": 20, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 170, + "labelHeight": 31, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + }, + { + "id": "manager2", + "type": "class", + "pos": { + "x": 2164, + "y": 12 + }, + "width": 582, + "height": 464, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#0A0F25", + "stroke": "#FFFFFF", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": [ + { + "name": "num", + "type": "int", + "visibility": "private" + }, + { + "name": "timeout", + "type": "int", + "visibility": "private" + }, + { + "name": "pid", + "type": "", + "visibility": "private" + } + ], + "methods": [ + { + "name": "getStatus()", + "return": "Enum", + "visibility": "public" + }, + { + "name": "getJobs()", + "return": "Job[]", + "visibility": "public" + }, + { + "name": "setTimeout(seconds int)", + "return": "void", + "visibility": "public" + } + ], + "columns": null, + "label": "BatchManager", + "fontSize": 30, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 241, + "labelHeight": 43, + "zIndex": 0, + "level": 1, + "primaryAccentColor": "#0D32B2", + "secondaryAccentColor": "#4A6FF3", + "neutralAccentColor": "#676C7E" + } + ], + "connections": [] +} diff --git a/e2etests/testdata/stable/sql_table_column_styles/elk/sketch.exp.svg b/e2etests/testdata/stable/sql_table_column_styles/elk/sketch.exp.svg new file mode 100644 index 000000000..0473c7f13 --- /dev/null +++ b/e2etests/testdata/stable/sql_table_column_styles/elk/sketch.exp.svg @@ -0,0 +1,91 @@ + +Humor in the CourtCould you see him from where you were standing? +I could see his head. +And where was his head? +Just above his shoulders. +Humor in the Court2Could you see him from where you were standing? +I could see his head. +And where was his head? +Just above his shoulders. +BatchManager- +num +int- +timeout +int- +pid ++ +getStatus() +Enum+ +getJobs() +Job[]+ +setTimeout(seconds int) +voidBatchManager- +num +int- +timeout +int- +pid ++ +getStatus() +Enum+ +getJobs() +Job[]+ +setTimeout(seconds int) +void + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg b/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg index e047853b3..f74082d41 100644 --- a/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/sql_table_tooltip_animated/dagre/sketch.exp.svg @@ -39,7 +39,7 @@ width="280" height="464" viewBox="-102 -118 280 464">