Add graph markdown and canvas outputs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package gsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -21,7 +22,7 @@ func (p *Project) graphForUnits(units []*Unit) Graph {
|
||||
include := map[string]bool{}
|
||||
for _, unit := range units {
|
||||
include[unit.ID] = true
|
||||
nodeMap[unit.ID] = GraphNode{ID: unit.ID, Type: unit.Type, Resolution: unit.Resolution, File: unit.File}
|
||||
nodeMap[unit.ID] = GraphNode{ID: unit.ID, Title: unit.Title, Type: unit.Type, Resolution: unit.Resolution, File: unit.File}
|
||||
}
|
||||
for _, unit := range units {
|
||||
if unit.Refines != "" {
|
||||
@@ -74,7 +75,7 @@ func (g Graph) Mermaid() string {
|
||||
return builder.String()
|
||||
}
|
||||
for _, node := range g.Nodes {
|
||||
label := node.ID
|
||||
label := node.DisplayLabel()
|
||||
if node.Missing {
|
||||
label += " (missing)"
|
||||
}
|
||||
@@ -86,6 +87,190 @@ func (g Graph) Mermaid() string {
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (g Graph) Markdown() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("# GSP Graph\n\n")
|
||||
builder.WriteString("```mermaid\n")
|
||||
builder.WriteString(g.Mermaid())
|
||||
builder.WriteString("```\n")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (g Graph) Canvas() ([]byte, error) {
|
||||
canvas := canvasDocument{
|
||||
Nodes: make([]canvasNode, 0, len(g.Nodes)),
|
||||
Edges: make([]canvasEdge, 0, len(g.Edges)),
|
||||
}
|
||||
positions := g.canvasPositions()
|
||||
for _, node := range g.Nodes {
|
||||
pos := positions[node.ID]
|
||||
canvas.Nodes = append(canvas.Nodes, canvasNode{
|
||||
ID: canvasNodeID(node.ID),
|
||||
Type: "text",
|
||||
Text: node.CanvasText(),
|
||||
X: pos.X,
|
||||
Y: pos.Y,
|
||||
Width: 280,
|
||||
Height: 140,
|
||||
Color: node.CanvasColor(),
|
||||
})
|
||||
}
|
||||
for _, edge := range g.Edges {
|
||||
canvas.Edges = append(canvas.Edges, canvasEdge{
|
||||
ID: canvasEdgeID(edge),
|
||||
FromNode: canvasNodeID(edge.From),
|
||||
FromSide: "right",
|
||||
ToNode: canvasNodeID(edge.To),
|
||||
ToSide: "left",
|
||||
Label: edge.Kind,
|
||||
})
|
||||
}
|
||||
data, err := json.MarshalIndent(canvas, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(data, '\n'), nil
|
||||
}
|
||||
|
||||
func (n GraphNode) DisplayLabel() string {
|
||||
title := strings.TrimSpace(n.Title)
|
||||
if title != "" {
|
||||
return title
|
||||
}
|
||||
return n.ID
|
||||
}
|
||||
|
||||
func (n GraphNode) CanvasText() string {
|
||||
var lines []string
|
||||
lines = append(lines, "## "+n.DisplayLabel())
|
||||
if n.Title != "" && n.ID != "" {
|
||||
lines = append(lines, "`"+n.ID+"`")
|
||||
}
|
||||
if n.Type != "" {
|
||||
lines = append(lines, "type: "+n.Type)
|
||||
}
|
||||
if n.Resolution != "" {
|
||||
lines = append(lines, "resolution: "+n.Resolution)
|
||||
}
|
||||
if n.Missing {
|
||||
lines = append(lines, "missing: true")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (n GraphNode) CanvasColor() string {
|
||||
if n.Missing {
|
||||
return "1"
|
||||
}
|
||||
switch n.Resolution {
|
||||
case "L0", "L1":
|
||||
return "2"
|
||||
case "L2":
|
||||
return "3"
|
||||
case "L3":
|
||||
return "4"
|
||||
case "L4", "L5":
|
||||
return "5"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type canvasDocument struct {
|
||||
Nodes []canvasNode `json:"nodes"`
|
||||
Edges []canvasEdge `json:"edges"`
|
||||
}
|
||||
|
||||
type canvasNode struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Color string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
type canvasEdge struct {
|
||||
ID string `json:"id"`
|
||||
FromNode string `json:"fromNode"`
|
||||
FromSide string `json:"fromSide,omitempty"`
|
||||
ToNode string `json:"toNode"`
|
||||
ToSide string `json:"toSide,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
}
|
||||
|
||||
type canvasPosition struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
func (g Graph) canvasPositions() map[string]canvasPosition {
|
||||
incoming := map[string]int{}
|
||||
outgoing := map[string][]string{}
|
||||
for _, node := range g.Nodes {
|
||||
incoming[node.ID] = 0
|
||||
}
|
||||
for _, edge := range g.Edges {
|
||||
if _, ok := incoming[edge.From]; !ok {
|
||||
incoming[edge.From] = 0
|
||||
}
|
||||
if _, ok := incoming[edge.To]; !ok {
|
||||
incoming[edge.To] = 0
|
||||
}
|
||||
incoming[edge.To]++
|
||||
outgoing[edge.From] = append(outgoing[edge.From], edge.To)
|
||||
}
|
||||
for id := range outgoing {
|
||||
sort.Strings(outgoing[id])
|
||||
}
|
||||
var queue []string
|
||||
for _, node := range g.Nodes {
|
||||
if incoming[node.ID] == 0 {
|
||||
queue = append(queue, node.ID)
|
||||
}
|
||||
}
|
||||
if len(queue) == 0 {
|
||||
for _, node := range g.Nodes {
|
||||
queue = append(queue, node.ID)
|
||||
}
|
||||
}
|
||||
sort.Strings(queue)
|
||||
depth := map[string]int{}
|
||||
for len(queue) > 0 {
|
||||
id := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, to := range outgoing[id] {
|
||||
if depth[to] < depth[id]+1 {
|
||||
depth[to] = depth[id] + 1
|
||||
}
|
||||
incoming[to]--
|
||||
if incoming[to] == 0 {
|
||||
queue = append(queue, to)
|
||||
sort.Strings(queue)
|
||||
}
|
||||
}
|
||||
}
|
||||
columns := map[int][]string{}
|
||||
for _, node := range g.Nodes {
|
||||
columns[depth[node.ID]] = append(columns[depth[node.ID]], node.ID)
|
||||
}
|
||||
positions := map[string]canvasPosition{}
|
||||
var keys []int
|
||||
for key := range columns {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
for _, key := range keys {
|
||||
sort.Strings(columns[key])
|
||||
for row, id := range columns[key] {
|
||||
positions[id] = canvasPosition{X: key * 360, Y: row * 190}
|
||||
}
|
||||
}
|
||||
return positions
|
||||
}
|
||||
|
||||
func mermaidID(id string) string {
|
||||
value := mermaidIDPattern.ReplaceAllString(id, "_")
|
||||
if value == "" {
|
||||
@@ -101,3 +286,11 @@ func escapeMermaid(value string) string {
|
||||
value = strings.ReplaceAll(value, `"`, `\"`)
|
||||
return value
|
||||
}
|
||||
|
||||
func canvasNodeID(id string) string {
|
||||
return "gsp_" + mermaidID(id)
|
||||
}
|
||||
|
||||
func canvasEdgeID(edge GraphEdge) string {
|
||||
return "edge_" + mermaidID(edge.From) + "_" + mermaidID(edge.Kind) + "_" + mermaidID(edge.To)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user