|
|
@@ -0,0 +1,543 @@
|
|
|
+// Package exporter provides high-quality SVG export functionality based on mermaid.js rendering logic
|
|
|
+package exporter
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "math"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ "mermaid-go/pkg/ast"
|
|
|
+)
|
|
|
+
|
|
|
+// SVGExporter exports diagrams to high-quality SVG format
|
|
|
+type SVGExporter struct {
|
|
|
+ width int
|
|
|
+ height int
|
|
|
+ theme string
|
|
|
+}
|
|
|
+
|
|
|
+// NewSVGExporter creates a new SVG exporter
|
|
|
+func NewSVGExporter() *SVGExporter {
|
|
|
+ return &SVGExporter{
|
|
|
+ width: 800,
|
|
|
+ height: 600,
|
|
|
+ theme: "default",
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// SetSize sets the SVG canvas size
|
|
|
+func (e *SVGExporter) SetSize(width, height int) *SVGExporter {
|
|
|
+ e.width = width
|
|
|
+ e.height = height
|
|
|
+ return e
|
|
|
+}
|
|
|
+
|
|
|
+// SetTheme sets the SVG theme
|
|
|
+func (e *SVGExporter) SetTheme(theme string) *SVGExporter {
|
|
|
+ e.theme = theme
|
|
|
+ return e
|
|
|
+}
|
|
|
+
|
|
|
+// ExportToSVG exports a diagram to SVG format
|
|
|
+func (e *SVGExporter) ExportToSVG(diagram ast.Diagram) (string, error) {
|
|
|
+ switch d := diagram.(type) {
|
|
|
+ case *ast.PieChart:
|
|
|
+ return e.exportPieChartToSVG(d)
|
|
|
+ case *ast.OrganizationDiagram:
|
|
|
+ return e.exportOrganizationToSVG(d)
|
|
|
+ case *ast.Flowchart:
|
|
|
+ return e.exportFlowchartToSVG(d)
|
|
|
+ case *ast.SequenceDiagram:
|
|
|
+ return e.exportSequenceToSVG(d)
|
|
|
+ case *ast.GanttDiagram:
|
|
|
+ return e.exportGanttToSVG(d)
|
|
|
+ case *ast.TimelineDiagram:
|
|
|
+ return e.exportTimelineToSVG(d)
|
|
|
+ case *ast.UserJourneyDiagram:
|
|
|
+ return e.exportJourneyToSVG(d)
|
|
|
+ case *ast.ArchitectureDiagram:
|
|
|
+ return e.exportArchitectureToSVG(d)
|
|
|
+ case *ast.BPMNDiagram:
|
|
|
+ return e.exportBPMNToSVG(d)
|
|
|
+ case *ast.ClassDiagram:
|
|
|
+ return e.exportClassToSVG(d)
|
|
|
+ case *ast.StateDiagram:
|
|
|
+ return e.exportStateToSVG(d)
|
|
|
+ case *ast.ERDiagram:
|
|
|
+ return e.exportERToSVG(d)
|
|
|
+ default:
|
|
|
+ return "", fmt.Errorf("unsupported diagram type for SVG export: %T", diagram)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// exportPieChartToSVG exports pie chart to SVG (based on mermaid.js pieRenderer.ts)
|
|
|
+func (e *SVGExporter) exportPieChartToSVG(diagram *ast.PieChart) (string, error) {
|
|
|
+ // Calculate total value
|
|
|
+ total := 0.0
|
|
|
+ for _, slice := range diagram.Data {
|
|
|
+ total += slice.Value
|
|
|
+ }
|
|
|
+
|
|
|
+ if total == 0 {
|
|
|
+ return e.createEmptySVG("Empty Pie Chart"), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // Filter out slices < 1%
|
|
|
+ var validSlices []*ast.PieSlice
|
|
|
+ for _, slice := range diagram.Data {
|
|
|
+ if (slice.Value/total)*100 >= 1 {
|
|
|
+ validSlices = append(validSlices, slice)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Mermaid.js pie chart dimensions
|
|
|
+ margin := 40
|
|
|
+ legendRectSize := 18
|
|
|
+ legendSpacing := 4
|
|
|
+ height := 450
|
|
|
+ pieWidth := height
|
|
|
+ radius := float64(min(pieWidth, height)/2 - margin)
|
|
|
+ centerX, centerY := float64(pieWidth/2), float64(height/2)
|
|
|
+
|
|
|
+ // Colors from mermaid.js theme
|
|
|
+ colors := []string{
|
|
|
+ "#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57", "#ff9ff3",
|
|
|
+ "#54a0ff", "#5f27cd", "#00d2d3", "#ff9ff3", "#54a0ff", "#5f27cd",
|
|
|
+ }
|
|
|
+
|
|
|
+ svg := e.createSVGHeader()
|
|
|
+ svg += e.getPieChartStyles()
|
|
|
+
|
|
|
+ // Main group with transform
|
|
|
+ svg += fmt.Sprintf(`<g transform="translate(%g,%g)">`, centerX, centerY)
|
|
|
+
|
|
|
+ // Outer circle
|
|
|
+ svg += fmt.Sprintf(`<circle cx="0" cy="0" r="%g" class="pieOuterCircle"/>`, radius+1)
|
|
|
+
|
|
|
+ // Generate pie slices
|
|
|
+ startAngle := 0.0
|
|
|
+ for i, slice := range validSlices {
|
|
|
+ angle := (slice.Value / total) * 2 * math.Pi
|
|
|
+ color := colors[i%len(colors)]
|
|
|
+
|
|
|
+ // Create arc path
|
|
|
+ arcPath := e.createArcPath(0, 0, radius, startAngle, startAngle+angle)
|
|
|
+ svg += fmt.Sprintf(`<path d="%s" fill="%s" class="pieCircle"/>`, arcPath, color)
|
|
|
+
|
|
|
+ // Add percentage text
|
|
|
+ midAngle := startAngle + angle/2
|
|
|
+ textRadius := radius * 0.7 // textPosition from mermaid.js
|
|
|
+ textX := textRadius * math.Cos(midAngle)
|
|
|
+ textY := textRadius * math.Sin(midAngle)
|
|
|
+ percentage := fmt.Sprintf("%.0f%%", (slice.Value/total)*100)
|
|
|
+
|
|
|
+ svg += fmt.Sprintf(`<text x="%g" y="%g" text-anchor="middle" class="slice">%s</text>`,
|
|
|
+ textX, textY, percentage)
|
|
|
+
|
|
|
+ startAngle += angle
|
|
|
+ }
|
|
|
+
|
|
|
+ svg += "</g>" // Close main group
|
|
|
+
|
|
|
+ // Add title
|
|
|
+ if diagram.Title != nil {
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="25" text-anchor="middle" class="pieTitleText">%s</text>`,
|
|
|
+ e.width/2, *diagram.Title)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add legend
|
|
|
+ legendX := float64(pieWidth + margin)
|
|
|
+ legendY := centerY - float64(len(validSlices)*22)/2
|
|
|
+
|
|
|
+ for i, slice := range validSlices {
|
|
|
+ color := colors[i%len(colors)]
|
|
|
+ y := legendY + float64(i*22)
|
|
|
+
|
|
|
+ // Legend rectangle
|
|
|
+ svg += fmt.Sprintf(`<rect x="%g" y="%g" width="%d" height="%d" fill="%s" stroke="%s"/>`,
|
|
|
+ legendX, y-9, legendRectSize, legendRectSize, color, color)
|
|
|
+
|
|
|
+ // Legend text
|
|
|
+ labelText := slice.Label
|
|
|
+ if diagram.Config != nil {
|
|
|
+ if showData, ok := diagram.Config["showData"].(bool); ok && showData {
|
|
|
+ labelText = fmt.Sprintf("%s [%.0f]", slice.Label, slice.Value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ svg += fmt.Sprintf(`<text x="%g" y="%g" class="legend">%s</text>`,
|
|
|
+ legendX+float64(legendRectSize+legendSpacing), y+5, labelText)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Calculate total width for viewBox
|
|
|
+ totalWidth := pieWidth + margin + legendRectSize + legendSpacing + 200 // Approximate legend width
|
|
|
+
|
|
|
+ svg += e.createSVGFooter()
|
|
|
+
|
|
|
+ // Set proper viewBox
|
|
|
+ return e.wrapWithViewBox(svg, totalWidth, height), nil
|
|
|
+}
|
|
|
+
|
|
|
+// exportOrganizationToSVG exports organization chart to SVG
|
|
|
+func (e *SVGExporter) exportOrganizationToSVG(diagram *ast.OrganizationDiagram) (string, error) {
|
|
|
+ svg := e.createSVGHeader()
|
|
|
+ svg += e.getOrganizationStyles()
|
|
|
+
|
|
|
+ if diagram.Root != nil {
|
|
|
+ svg += e.renderOrgNodeSVG(diagram.Root, e.width/2, 80, 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add title
|
|
|
+ if diagram.Title != nil {
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="30" text-anchor="middle" class="title">%s</text>`,
|
|
|
+ e.width/2, *diagram.Title)
|
|
|
+ }
|
|
|
+
|
|
|
+ svg += e.createSVGFooter()
|
|
|
+ return e.wrapWithViewBox(svg, e.width, e.height), nil
|
|
|
+}
|
|
|
+
|
|
|
+// exportFlowchartToSVG exports flowchart to SVG
|
|
|
+func (e *SVGExporter) exportFlowchartToSVG(diagram *ast.Flowchart) (string, error) {
|
|
|
+ svg := e.createSVGHeader()
|
|
|
+ svg += e.getFlowchartStyles()
|
|
|
+
|
|
|
+ // Simple grid layout
|
|
|
+ nodePositions := e.calculateFlowchartLayout(diagram)
|
|
|
+
|
|
|
+ // Render edges first (so they appear behind nodes)
|
|
|
+ for _, edge := range diagram.Edges {
|
|
|
+ fromPos, fromExists := nodePositions[edge.Start]
|
|
|
+ toPos, toExists := nodePositions[edge.End]
|
|
|
+ if fromExists && toExists {
|
|
|
+ svg += e.renderFlowchartEdgeSVG(edge, fromPos, toPos)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Render nodes
|
|
|
+ for id, vertex := range diagram.Vertices {
|
|
|
+ if pos, exists := nodePositions[id]; exists {
|
|
|
+ svg += e.renderFlowchartNodeSVG(vertex, pos)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ svg += e.createSVGFooter()
|
|
|
+ return e.wrapWithViewBox(svg, e.width, e.height), nil
|
|
|
+}
|
|
|
+
|
|
|
+// exportSequenceToSVG exports sequence diagram to SVG
|
|
|
+func (e *SVGExporter) exportSequenceToSVG(diagram *ast.SequenceDiagram) (string, error) {
|
|
|
+ svg := e.createSVGHeader()
|
|
|
+ svg += e.getSequenceStyles()
|
|
|
+
|
|
|
+ participantWidth := e.width / (len(diagram.Participants) + 1)
|
|
|
+ participantY := 60
|
|
|
+
|
|
|
+ // Draw participants
|
|
|
+ for i, participant := range diagram.Participants {
|
|
|
+ x := participantWidth * (i + 1)
|
|
|
+ svg += fmt.Sprintf(`<rect x="%d" y="%d" width="120" height="40" class="participant"/>`,
|
|
|
+ x-60, participantY-20)
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="participantText">%s</text>`,
|
|
|
+ x, participantY+5, participant.Name)
|
|
|
+
|
|
|
+ // Lifeline
|
|
|
+ svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="lifeline"/>`,
|
|
|
+ x, participantY+20, x, e.height-50)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Draw messages
|
|
|
+ messageY := participantY + 60
|
|
|
+ for _, message := range diagram.Messages {
|
|
|
+ fromX, toX := 0, 0
|
|
|
+ for i, p := range diagram.Participants {
|
|
|
+ x := participantWidth * (i + 1)
|
|
|
+ if p.ID == message.From {
|
|
|
+ fromX = x
|
|
|
+ }
|
|
|
+ if p.ID == message.To {
|
|
|
+ toX = x
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Message arrow
|
|
|
+ svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="messageArrow" marker-end="url(#arrowhead)"/>`,
|
|
|
+ fromX, messageY, toX, messageY)
|
|
|
+
|
|
|
+ // Message text
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="messageText">%s</text>`,
|
|
|
+ (fromX+toX)/2, messageY-5, message.Message)
|
|
|
+
|
|
|
+ messageY += 50
|
|
|
+ }
|
|
|
+
|
|
|
+ svg += e.createSVGFooter()
|
|
|
+ return e.wrapWithViewBox(svg, e.width, e.height), nil
|
|
|
+}
|
|
|
+
|
|
|
+// exportGanttToSVG exports Gantt chart to SVG
|
|
|
+func (e *SVGExporter) exportGanttToSVG(diagram *ast.GanttDiagram) (string, error) {
|
|
|
+ svg := e.createSVGHeader()
|
|
|
+ svg += e.getGanttStyles()
|
|
|
+
|
|
|
+ y := 80
|
|
|
+ if diagram.Title != nil {
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="30" text-anchor="middle" class="title">%s</text>`,
|
|
|
+ e.width/2, *diagram.Title)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Draw sections and tasks
|
|
|
+ for _, section := range diagram.Sections {
|
|
|
+ // Section header
|
|
|
+ svg += fmt.Sprintf(`<text x="20" y="%d" class="sectionText">%s</text>`, y, section.Name)
|
|
|
+ y += 30
|
|
|
+
|
|
|
+ // Tasks
|
|
|
+ for _, task := range section.Tasks {
|
|
|
+ // Task bar
|
|
|
+ barWidth := 200
|
|
|
+ svg += fmt.Sprintf(`<rect x="50" y="%d" width="%d" height="20" class="taskBar"/>`,
|
|
|
+ y-10, barWidth)
|
|
|
+
|
|
|
+ // Task name
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="%d" class="taskText">%s</text>`,
|
|
|
+ 60+barWidth, y+5, task.Name)
|
|
|
+
|
|
|
+ y += 35
|
|
|
+ }
|
|
|
+ y += 15
|
|
|
+ }
|
|
|
+
|
|
|
+ svg += e.createSVGFooter()
|
|
|
+ return e.wrapWithViewBox(svg, e.width, e.height), nil
|
|
|
+}
|
|
|
+
|
|
|
+// Placeholder implementations for other diagram types
|
|
|
+func (e *SVGExporter) exportTimelineToSVG(diagram *ast.TimelineDiagram) (string, error) {
|
|
|
+ return e.createPlaceholderSVG("Timeline", "Timeline SVG export coming soon"), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) exportJourneyToSVG(diagram *ast.UserJourneyDiagram) (string, error) {
|
|
|
+ return e.createPlaceholderSVG("User Journey", "User Journey SVG export coming soon"), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) exportArchitectureToSVG(diagram *ast.ArchitectureDiagram) (string, error) {
|
|
|
+ return e.createPlaceholderSVG("Architecture", "Architecture SVG export coming soon"), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) exportBPMNToSVG(diagram *ast.BPMNDiagram) (string, error) {
|
|
|
+ return e.createPlaceholderSVG("BPMN", "BPMN SVG export coming soon"), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) exportClassToSVG(diagram *ast.ClassDiagram) (string, error) {
|
|
|
+ return e.createPlaceholderSVG("Class Diagram", "Class Diagram SVG export coming soon"), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) exportStateToSVG(diagram *ast.StateDiagram) (string, error) {
|
|
|
+ return e.createPlaceholderSVG("State Diagram", "State Diagram SVG export coming soon"), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) exportERToSVG(diagram *ast.ERDiagram) (string, error) {
|
|
|
+ return e.createPlaceholderSVG("ER Diagram", "ER Diagram SVG export coming soon"), nil
|
|
|
+}
|
|
|
+
|
|
|
+// Helper methods
|
|
|
+
|
|
|
+// createSVGHeader creates SVG header with proper namespace and definitions
|
|
|
+func (e *SVGExporter) createSVGHeader() string {
|
|
|
+ return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
|
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
|
+<defs>
|
|
|
+%s
|
|
|
+</defs>
|
|
|
+`, e.getCommonDefs())
|
|
|
+}
|
|
|
+
|
|
|
+// createSVGFooter creates SVG footer
|
|
|
+func (e *SVGExporter) createSVGFooter() string {
|
|
|
+ return "</svg>"
|
|
|
+}
|
|
|
+
|
|
|
+// wrapWithViewBox wraps SVG content with proper viewBox
|
|
|
+func (e *SVGExporter) wrapWithViewBox(content string, width, height int) string {
|
|
|
+ // Insert viewBox after the opening svg tag
|
|
|
+ viewBox := fmt.Sprintf(` viewBox="0 0 %d %d" width="%d" height="%d"`, width, height, width, height)
|
|
|
+ return strings.Replace(content, "<svg xmlns=", "<svg"+viewBox+" xmlns=", 1)
|
|
|
+}
|
|
|
+
|
|
|
+// createEmptySVG creates an empty SVG with message
|
|
|
+func (e *SVGExporter) createEmptySVG(message string) string {
|
|
|
+ svg := e.createSVGHeader()
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="emptyMessage">%s</text>`,
|
|
|
+ e.width/2, e.height/2, message)
|
|
|
+ svg += e.createSVGFooter()
|
|
|
+ return e.wrapWithViewBox(svg, e.width, e.height)
|
|
|
+}
|
|
|
+
|
|
|
+// createPlaceholderSVG creates a placeholder SVG
|
|
|
+func (e *SVGExporter) createPlaceholderSVG(title, message string) string {
|
|
|
+ svg := e.createSVGHeader()
|
|
|
+ svg += fmt.Sprintf(`<rect width="100%%" height="100%%" fill="#f8f9fa"/>`)
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="title">%s</text>`,
|
|
|
+ e.width/2, e.height/2-20, title)
|
|
|
+ svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="message">%s</text>`,
|
|
|
+ e.width/2, e.height/2+20, message)
|
|
|
+ svg += e.createSVGFooter()
|
|
|
+ return e.wrapWithViewBox(svg, e.width, e.height)
|
|
|
+}
|
|
|
+
|
|
|
+// getCommonDefs returns common SVG definitions
|
|
|
+func (e *SVGExporter) getCommonDefs() string {
|
|
|
+ return `
|
|
|
+<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
|
+ <polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
|
|
|
+</marker>
|
|
|
+<marker id="arrowheadWhite" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
|
+ <polygon points="0 0, 10 3.5, 0 7" fill="#fff"/>
|
|
|
+</marker>`
|
|
|
+}
|
|
|
+
|
|
|
+// Style methods based on mermaid.js themes
|
|
|
+
|
|
|
+func (e *SVGExporter) getPieChartStyles() string {
|
|
|
+ return `<style>
|
|
|
+.pieOuterCircle { fill: none; stroke: #333; stroke-width: 2; }
|
|
|
+.pieCircle { stroke: #fff; stroke-width: 2; }
|
|
|
+.slice { font-family: Arial, sans-serif; font-size: 14px; fill: #fff; font-weight: bold; }
|
|
|
+.pieTitleText { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; fill: #333; }
|
|
|
+.legend { font-family: Arial, sans-serif; font-size: 14px; fill: #333; }
|
|
|
+</style>`
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) getOrganizationStyles() string {
|
|
|
+ return `<style>
|
|
|
+.orgNode { fill: #e3f2fd; stroke: #1976d2; stroke-width: 2; }
|
|
|
+.orgText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; text-anchor: middle; }
|
|
|
+.orgEdge { stroke: #1976d2; stroke-width: 2; }
|
|
|
+.title { font-family: Arial, sans-serif; font-size: 18px; font-weight: bold; fill: #333; }
|
|
|
+</style>`
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) getFlowchartStyles() string {
|
|
|
+ return `<style>
|
|
|
+.flowNode { fill: #fff; stroke: #333; stroke-width: 2; }
|
|
|
+.flowText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; text-anchor: middle; }
|
|
|
+.flowEdge { stroke: #333; stroke-width: 2; fill: none; }
|
|
|
+</style>`
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) getSequenceStyles() string {
|
|
|
+ return `<style>
|
|
|
+.participant { fill: #e3f2fd; stroke: #1976d2; stroke-width: 2; }
|
|
|
+.participantText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }
|
|
|
+.lifeline { stroke: #ccc; stroke-width: 1; stroke-dasharray: 5,5; }
|
|
|
+.messageArrow { stroke: #333; stroke-width: 2; }
|
|
|
+.messageText { font-family: Arial, sans-serif; font-size: 11px; fill: #333; }
|
|
|
+</style>`
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) getGanttStyles() string {
|
|
|
+ return `<style>
|
|
|
+.taskBar { fill: #4ecdc4; stroke: #26a69a; stroke-width: 1; }
|
|
|
+.taskText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }
|
|
|
+.sectionText { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
|
|
|
+.title { font-family: Arial, sans-serif; font-size: 18px; font-weight: bold; fill: #333; }
|
|
|
+</style>`
|
|
|
+}
|
|
|
+
|
|
|
+// Layout and rendering helpers
|
|
|
+
|
|
|
+type Position struct {
|
|
|
+ X, Y int
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) calculateFlowchartLayout(diagram *ast.Flowchart) map[string]Position {
|
|
|
+ positions := make(map[string]Position)
|
|
|
+ x, y := 100, 100
|
|
|
+ col := 0
|
|
|
+ maxCols := 3
|
|
|
+
|
|
|
+ for id := range diagram.Vertices {
|
|
|
+ positions[id] = Position{X: x, Y: y}
|
|
|
+ col++
|
|
|
+ if col >= maxCols {
|
|
|
+ col = 0
|
|
|
+ x = 100
|
|
|
+ y += 120
|
|
|
+ } else {
|
|
|
+ x += 200
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return positions
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) renderFlowchartNodeSVG(vertex *ast.FlowVertex, pos Position) string {
|
|
|
+ text := vertex.ID
|
|
|
+ if vertex.Text != nil {
|
|
|
+ text = *vertex.Text
|
|
|
+ }
|
|
|
+
|
|
|
+ return fmt.Sprintf(`
|
|
|
+<g transform="translate(%d,%d)">
|
|
|
+ <rect x="-50" y="-20" width="100" height="40" class="flowNode"/>
|
|
|
+ <text x="0" y="5" class="flowText">%s</text>
|
|
|
+</g>`, pos.X, pos.Y, text)
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) renderFlowchartEdgeSVG(edge *ast.FlowEdge, from, to Position) string {
|
|
|
+ return fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="flowEdge" marker-end="url(#arrowhead)"/>`,
|
|
|
+ from.X, from.Y, to.X, to.Y)
|
|
|
+}
|
|
|
+
|
|
|
+func (e *SVGExporter) renderOrgNodeSVG(node *ast.OrganizationNode, x, y, level int) string {
|
|
|
+ svg := fmt.Sprintf(`
|
|
|
+<g transform="translate(%d,%d)">
|
|
|
+ <rect x="-80" y="-25" width="160" height="50" class="orgNode"/>
|
|
|
+ <text x="0" y="5" class="orgText">%s</text>
|
|
|
+</g>`, x, y, node.Name)
|
|
|
+
|
|
|
+ // Render children
|
|
|
+ if len(node.Children) > 0 {
|
|
|
+ childY := y + 100
|
|
|
+ totalWidth := len(node.Children) * 200
|
|
|
+ startX := x - totalWidth/2 + 100
|
|
|
+
|
|
|
+ for i, child := range node.Children {
|
|
|
+ childX := startX + i*200
|
|
|
+
|
|
|
+ // Connection line
|
|
|
+ svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="orgEdge"/>`,
|
|
|
+ x, y+25, childX, childY-25)
|
|
|
+
|
|
|
+ // Recursively render child
|
|
|
+ svg += e.renderOrgNodeSVG(child, childX, childY, level+1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return svg
|
|
|
+}
|
|
|
+
|
|
|
+// createArcPath creates SVG arc path for pie slices
|
|
|
+func (e *SVGExporter) createArcPath(centerX, centerY, radius, startAngle, endAngle float64) string {
|
|
|
+ x1 := centerX + radius*math.Cos(startAngle)
|
|
|
+ y1 := centerY + radius*math.Sin(startAngle)
|
|
|
+ x2 := centerX + radius*math.Cos(endAngle)
|
|
|
+ y2 := centerY + radius*math.Sin(endAngle)
|
|
|
+
|
|
|
+ largeArc := 0
|
|
|
+ if endAngle-startAngle > math.Pi {
|
|
|
+ largeArc = 1
|
|
|
+ }
|
|
|
+
|
|
|
+ return fmt.Sprintf("M %g %g L %g %g A %g %g 0 %d 1 %g %g Z",
|
|
|
+ centerX, centerY, x1, y1, radius, radius, largeArc, x2, y2)
|
|
|
+}
|
|
|
+
|
|
|
+// min returns the minimum of two integers
|
|
|
+func min(a, b int) int {
|
|
|
+ if a < b {
|
|
|
+ return a
|
|
|
+ }
|
|
|
+ return b
|
|
|
+}
|