svg.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. // Package exporter provides high-quality SVG export functionality based on mermaid.js rendering logic
  2. package exporter
  3. import (
  4. "fmt"
  5. "math"
  6. "strings"
  7. "mermaid-go/pkg/ast"
  8. )
  9. // SVGExporter exports diagrams to high-quality SVG format
  10. type SVGExporter struct {
  11. width int
  12. height int
  13. theme string
  14. }
  15. // NewSVGExporter creates a new SVG exporter
  16. func NewSVGExporter() *SVGExporter {
  17. return &SVGExporter{
  18. width: 800,
  19. height: 600,
  20. theme: "default",
  21. }
  22. }
  23. // SetSize sets the SVG canvas size
  24. func (e *SVGExporter) SetSize(width, height int) *SVGExporter {
  25. e.width = width
  26. e.height = height
  27. return e
  28. }
  29. // SetTheme sets the SVG theme
  30. func (e *SVGExporter) SetTheme(theme string) *SVGExporter {
  31. e.theme = theme
  32. return e
  33. }
  34. // ExportToSVG exports a diagram to SVG format
  35. func (e *SVGExporter) ExportToSVG(diagram ast.Diagram) (string, error) {
  36. switch d := diagram.(type) {
  37. case *ast.PieChart:
  38. return e.exportPieChartToSVG(d)
  39. case *ast.OrganizationDiagram:
  40. return e.exportOrganizationToSVG(d)
  41. case *ast.Flowchart:
  42. return e.exportFlowchartToSVG(d)
  43. case *ast.SequenceDiagram:
  44. return e.exportSequenceToSVG(d)
  45. case *ast.GanttDiagram:
  46. return e.exportGanttToSVG(d)
  47. case *ast.TimelineDiagram:
  48. return e.exportTimelineToSVG(d)
  49. case *ast.UserJourneyDiagram:
  50. return e.exportJourneyToSVG(d)
  51. case *ast.ArchitectureDiagram:
  52. return e.exportArchitectureToSVG(d)
  53. case *ast.BPMNDiagram:
  54. return e.exportBPMNToSVG(d)
  55. case *ast.ClassDiagram:
  56. return e.exportClassToSVG(d)
  57. case *ast.StateDiagram:
  58. return e.exportStateToSVG(d)
  59. case *ast.ERDiagram:
  60. return e.exportERToSVG(d)
  61. case *ast.MindmapDiagram:
  62. return e.exportMindmapToSVG(d)
  63. case *ast.KanbanDiagram:
  64. return e.exportKanbanToSVG(d)
  65. case *ast.GitDiagram:
  66. return e.exportGitToSVG(d)
  67. case *ast.SankeyDiagram:
  68. return e.exportSankeyToSVG(d)
  69. case *ast.XYChartDiagram:
  70. return e.exportXYChartToSVG(d)
  71. case *ast.RadarDiagram:
  72. return e.exportRadarToSVG(d)
  73. case *ast.TreemapDiagram:
  74. return e.exportTreemapToSVG(d)
  75. case *ast.PacketDiagram:
  76. return e.exportPacketToSVG(d)
  77. case *ast.InfoDiagram:
  78. return e.exportInfoToSVG(d)
  79. case *ast.C4ContextDiagram:
  80. return e.exportC4ToSVG(d)
  81. case *ast.C4ContainerDiagram:
  82. return e.exportC4ToSVG(d)
  83. case *ast.C4ComponentDiagram:
  84. return e.exportC4ToSVG(d)
  85. case *ast.C4DynamicDiagram:
  86. return e.exportC4ToSVG(d)
  87. case *ast.C4DeploymentDiagram:
  88. return e.exportC4ToSVG(d)
  89. case *ast.QuadrantChart:
  90. return e.exportQuadrantToSVG(d)
  91. default:
  92. return "", fmt.Errorf("unsupported diagram type for SVG export: %T", diagram)
  93. }
  94. }
  95. // exportPieChartToSVG exports pie chart to SVG (based on mermaid.js pieRenderer.ts)
  96. func (e *SVGExporter) exportPieChartToSVG(diagram *ast.PieChart) (string, error) {
  97. // Calculate total value
  98. total := 0.0
  99. for _, slice := range diagram.Data {
  100. total += slice.Value
  101. }
  102. if total == 0 {
  103. return e.createEmptySVG("Empty Pie Chart"), nil
  104. }
  105. // Filter out slices < 1%
  106. var validSlices []*ast.PieSlice
  107. for _, slice := range diagram.Data {
  108. if (slice.Value/total)*100 >= 1 {
  109. validSlices = append(validSlices, slice)
  110. }
  111. }
  112. // Mermaid.js pie chart dimensions
  113. margin := 40
  114. legendRectSize := 18
  115. legendSpacing := 4
  116. height := 450
  117. pieWidth := height
  118. radius := float64(min(pieWidth, height)/2 - margin)
  119. centerX, centerY := float64(pieWidth/2), float64(height/2)
  120. // Colors from mermaid.js theme
  121. colors := []string{
  122. "#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57", "#ff9ff3",
  123. "#54a0ff", "#5f27cd", "#00d2d3", "#ff9ff3", "#54a0ff", "#5f27cd",
  124. }
  125. svg := e.createSVGHeader()
  126. svg += e.getPieChartStyles()
  127. // Main group with transform
  128. svg += fmt.Sprintf(`<g transform="translate(%g,%g)">`, centerX, centerY)
  129. // Outer circle
  130. svg += fmt.Sprintf(`<circle cx="0" cy="0" r="%g" class="pieOuterCircle"/>`, radius+1)
  131. // Generate pie slices
  132. startAngle := 0.0
  133. for i, slice := range validSlices {
  134. angle := (slice.Value / total) * 2 * math.Pi
  135. color := colors[i%len(colors)]
  136. // Create arc path
  137. arcPath := e.createArcPath(0, 0, radius, startAngle, startAngle+angle)
  138. svg += fmt.Sprintf(`<path d="%s" fill="%s" class="pieCircle"/>`, arcPath, color)
  139. // Add percentage text
  140. midAngle := startAngle + angle/2
  141. textRadius := radius * 0.7 // textPosition from mermaid.js
  142. textX := textRadius * math.Cos(midAngle)
  143. textY := textRadius * math.Sin(midAngle)
  144. percentage := fmt.Sprintf("%.0f%%", (slice.Value/total)*100)
  145. svg += fmt.Sprintf(`<text x="%g" y="%g" text-anchor="middle" class="slice">%s</text>`,
  146. textX, textY, percentage)
  147. startAngle += angle
  148. }
  149. svg += "</g>" // Close main group
  150. // Add title
  151. if diagram.Title != nil {
  152. svg += fmt.Sprintf(`<text x="%d" y="25" text-anchor="middle" class="pieTitleText">%s</text>`,
  153. e.width/2, *diagram.Title)
  154. }
  155. // Add legend
  156. legendX := float64(pieWidth + margin)
  157. legendY := centerY - float64(len(validSlices)*22)/2
  158. for i, slice := range validSlices {
  159. color := colors[i%len(colors)]
  160. y := legendY + float64(i*22)
  161. // Legend rectangle
  162. svg += fmt.Sprintf(`<rect x="%g" y="%g" width="%d" height="%d" fill="%s" stroke="%s"/>`,
  163. legendX, y-9, legendRectSize, legendRectSize, color, color)
  164. // Legend text
  165. labelText := slice.Label
  166. if diagram.Config != nil {
  167. if showData, ok := diagram.Config["showData"].(bool); ok && showData {
  168. labelText = fmt.Sprintf("%s [%.0f]", slice.Label, slice.Value)
  169. }
  170. }
  171. svg += fmt.Sprintf(`<text x="%g" y="%g" class="legend">%s</text>`,
  172. legendX+float64(legendRectSize+legendSpacing), y+5, labelText)
  173. }
  174. // Calculate total width for viewBox
  175. totalWidth := pieWidth + margin + legendRectSize + legendSpacing + 200 // Approximate legend width
  176. svg += e.createSVGFooter()
  177. // Set proper viewBox
  178. return e.wrapWithViewBox(svg, totalWidth, height), nil
  179. }
  180. // exportOrganizationToSVG exports organization chart to SVG
  181. func (e *SVGExporter) exportOrganizationToSVG(diagram *ast.OrganizationDiagram) (string, error) {
  182. svg := e.createSVGHeader()
  183. svg += e.getOrganizationStyles()
  184. if diagram.Root != nil {
  185. svg += e.renderOrgNodeSVG(diagram.Root, e.width/2, 80, 0)
  186. }
  187. // Add title
  188. if diagram.Title != nil {
  189. svg += fmt.Sprintf(`<text x="%d" y="30" text-anchor="middle" class="title">%s</text>`,
  190. e.width/2, *diagram.Title)
  191. }
  192. svg += e.createSVGFooter()
  193. return e.wrapWithViewBox(svg, e.width, e.height), nil
  194. }
  195. // exportFlowchartToSVG exports flowchart to SVG
  196. func (e *SVGExporter) exportFlowchartToSVG(diagram *ast.Flowchart) (string, error) {
  197. svg := e.createSVGHeader()
  198. svg += e.getFlowchartStyles()
  199. // Simple grid layout
  200. nodePositions := e.calculateFlowchartLayout(diagram)
  201. // Render edges first (so they appear behind nodes)
  202. for _, edge := range diagram.Edges {
  203. fromPos, fromExists := nodePositions[edge.Start]
  204. toPos, toExists := nodePositions[edge.End]
  205. if fromExists && toExists {
  206. svg += e.renderFlowchartEdgeSVG(edge, fromPos, toPos)
  207. }
  208. }
  209. // Render nodes
  210. for id, vertex := range diagram.Vertices {
  211. if pos, exists := nodePositions[id]; exists {
  212. svg += e.renderFlowchartNodeSVG(vertex, pos)
  213. }
  214. }
  215. svg += e.createSVGFooter()
  216. return e.wrapWithViewBox(svg, e.width, e.height), nil
  217. }
  218. // exportSequenceToSVG exports sequence diagram to SVG
  219. func (e *SVGExporter) exportSequenceToSVG(diagram *ast.SequenceDiagram) (string, error) {
  220. svg := e.createSVGHeader()
  221. svg += e.getSequenceStyles()
  222. participantWidth := e.width / (len(diagram.Participants) + 1)
  223. participantY := 60
  224. // Draw participants
  225. for i, participant := range diagram.Participants {
  226. x := participantWidth * (i + 1)
  227. svg += fmt.Sprintf(`<rect x="%d" y="%d" width="120" height="40" class="participant"/>`,
  228. x-60, participantY-20)
  229. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="participantText">%s</text>`,
  230. x, participantY+5, participant.Name)
  231. // Lifeline
  232. svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="lifeline"/>`,
  233. x, participantY+20, x, e.height-50)
  234. }
  235. // Draw messages
  236. messageY := participantY + 60
  237. for _, message := range diagram.Messages {
  238. fromX, toX := 0, 0
  239. for i, p := range diagram.Participants {
  240. x := participantWidth * (i + 1)
  241. if p.ID == message.From {
  242. fromX = x
  243. }
  244. if p.ID == message.To {
  245. toX = x
  246. }
  247. }
  248. // Message arrow
  249. svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="messageArrow" marker-end="url(#arrowhead)"/>`,
  250. fromX, messageY, toX, messageY)
  251. // Message text
  252. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="messageText">%s</text>`,
  253. (fromX+toX)/2, messageY-5, message.Message)
  254. messageY += 50
  255. }
  256. svg += e.createSVGFooter()
  257. return e.wrapWithViewBox(svg, e.width, e.height), nil
  258. }
  259. // exportGanttToSVG exports Gantt chart to SVG
  260. func (e *SVGExporter) exportGanttToSVG(diagram *ast.GanttDiagram) (string, error) {
  261. svg := e.createSVGHeader()
  262. svg += e.getGanttStyles()
  263. y := 80
  264. if diagram.Title != nil {
  265. svg += fmt.Sprintf(`<text x="%d" y="30" text-anchor="middle" class="title">%s</text>`,
  266. e.width/2, *diagram.Title)
  267. }
  268. // Draw sections and tasks
  269. for _, section := range diagram.Sections {
  270. // Section header
  271. svg += fmt.Sprintf(`<text x="20" y="%d" class="sectionText">%s</text>`, y, section.Name)
  272. y += 30
  273. // Tasks
  274. for _, task := range section.Tasks {
  275. // Task bar
  276. barWidth := 200
  277. svg += fmt.Sprintf(`<rect x="50" y="%d" width="%d" height="20" class="taskBar"/>`,
  278. y-10, barWidth)
  279. // Task name
  280. svg += fmt.Sprintf(`<text x="%d" y="%d" class="taskText">%s</text>`,
  281. 60+barWidth, y+5, task.Name)
  282. y += 35
  283. }
  284. y += 15
  285. }
  286. svg += e.createSVGFooter()
  287. return e.wrapWithViewBox(svg, e.width, e.height), nil
  288. }
  289. // Placeholder implementations for other diagram types
  290. func (e *SVGExporter) exportTimelineToSVG(diagram *ast.TimelineDiagram) (string, error) {
  291. return e.createPlaceholderSVG("Timeline", "Timeline SVG export coming soon"), nil
  292. }
  293. func (e *SVGExporter) exportJourneyToSVG(diagram *ast.UserJourneyDiagram) (string, error) {
  294. return e.createPlaceholderSVG("User Journey", "User Journey SVG export coming soon"), nil
  295. }
  296. func (e *SVGExporter) exportArchitectureToSVG(diagram *ast.ArchitectureDiagram) (string, error) {
  297. return e.createPlaceholderSVG("Architecture", "Architecture SVG export coming soon"), nil
  298. }
  299. func (e *SVGExporter) exportBPMNToSVG(diagram *ast.BPMNDiagram) (string, error) {
  300. return e.createPlaceholderSVG("BPMN", "BPMN SVG export coming soon"), nil
  301. }
  302. func (e *SVGExporter) exportClassToSVG(diagram *ast.ClassDiagram) (string, error) {
  303. return e.createPlaceholderSVG("Class Diagram", "Class Diagram SVG export coming soon"), nil
  304. }
  305. func (e *SVGExporter) exportStateToSVG(diagram *ast.StateDiagram) (string, error) {
  306. return e.createPlaceholderSVG("State Diagram", "State Diagram SVG export coming soon"), nil
  307. }
  308. func (e *SVGExporter) exportERToSVG(diagram *ast.ERDiagram) (string, error) {
  309. return e.createPlaceholderSVG("ER Diagram", "ER Diagram SVG export coming soon"), nil
  310. }
  311. // Helper methods
  312. // createSVGHeader creates SVG header with proper namespace and definitions
  313. func (e *SVGExporter) createSVGHeader() string {
  314. return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
  315. <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  316. <defs>
  317. %s
  318. </defs>
  319. `, e.getCommonDefs())
  320. }
  321. // createSVGFooter creates SVG footer
  322. func (e *SVGExporter) createSVGFooter() string {
  323. return "</svg>"
  324. }
  325. // wrapWithViewBox wraps SVG content with proper viewBox
  326. func (e *SVGExporter) wrapWithViewBox(content string, width, height int) string {
  327. // Insert viewBox after the opening svg tag
  328. viewBox := fmt.Sprintf(` viewBox="0 0 %d %d" width="%d" height="%d"`, width, height, width, height)
  329. return strings.Replace(content, "<svg xmlns=", "<svg"+viewBox+" xmlns=", 1)
  330. }
  331. // createEmptySVG creates an empty SVG with message
  332. func (e *SVGExporter) createEmptySVG(message string) string {
  333. svg := e.createSVGHeader()
  334. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="emptyMessage">%s</text>`,
  335. e.width/2, e.height/2, message)
  336. svg += e.createSVGFooter()
  337. return e.wrapWithViewBox(svg, e.width, e.height)
  338. }
  339. // createPlaceholderSVG creates a placeholder SVG
  340. func (e *SVGExporter) createPlaceholderSVG(title, message string) string {
  341. svg := e.createSVGHeader()
  342. svg += fmt.Sprintf(`<rect width="100%%" height="100%%" fill="#f8f9fa"/>`)
  343. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="title">%s</text>`,
  344. e.width/2, e.height/2-20, title)
  345. svg += fmt.Sprintf(`<text x="%d" y="%d" text-anchor="middle" class="message">%s</text>`,
  346. e.width/2, e.height/2+20, message)
  347. svg += e.createSVGFooter()
  348. return e.wrapWithViewBox(svg, e.width, e.height)
  349. }
  350. // getCommonDefs returns common SVG definitions
  351. func (e *SVGExporter) getCommonDefs() string {
  352. return `
  353. <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
  354. <polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
  355. </marker>
  356. <marker id="arrowheadWhite" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
  357. <polygon points="0 0, 10 3.5, 0 7" fill="#fff"/>
  358. </marker>`
  359. }
  360. // Style methods based on mermaid.js themes
  361. func (e *SVGExporter) getPieChartStyles() string {
  362. return `<style>
  363. .pieOuterCircle { fill: none; stroke: #333; stroke-width: 2; }
  364. .pieCircle { stroke: #fff; stroke-width: 2; }
  365. .slice { font-family: Arial, sans-serif; font-size: 14px; fill: #fff; font-weight: bold; }
  366. .pieTitleText { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; fill: #333; }
  367. .legend { font-family: Arial, sans-serif; font-size: 14px; fill: #333; }
  368. </style>`
  369. }
  370. func (e *SVGExporter) getOrganizationStyles() string {
  371. return `<style>
  372. .orgNode { fill: #e3f2fd; stroke: #1976d2; stroke-width: 2; }
  373. .orgText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; text-anchor: middle; }
  374. .orgEdge { stroke: #1976d2; stroke-width: 2; }
  375. .title { font-family: Arial, sans-serif; font-size: 18px; font-weight: bold; fill: #333; }
  376. </style>`
  377. }
  378. func (e *SVGExporter) getFlowchartStyles() string {
  379. return `<style>
  380. .flowNode { fill: #fff; stroke: #333; stroke-width: 2; }
  381. .flowText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; text-anchor: middle; }
  382. .flowEdge { stroke: #333; stroke-width: 2; fill: none; }
  383. </style>`
  384. }
  385. func (e *SVGExporter) getSequenceStyles() string {
  386. return `<style>
  387. .participant { fill: #e3f2fd; stroke: #1976d2; stroke-width: 2; }
  388. .participantText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }
  389. .lifeline { stroke: #ccc; stroke-width: 1; stroke-dasharray: 5,5; }
  390. .messageArrow { stroke: #333; stroke-width: 2; }
  391. .messageText { font-family: Arial, sans-serif; font-size: 11px; fill: #333; }
  392. </style>`
  393. }
  394. func (e *SVGExporter) getGanttStyles() string {
  395. return `<style>
  396. .taskBar { fill: #4ecdc4; stroke: #26a69a; stroke-width: 1; }
  397. .taskText { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }
  398. .sectionText { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
  399. .title { font-family: Arial, sans-serif; font-size: 18px; font-weight: bold; fill: #333; }
  400. </style>`
  401. }
  402. // Layout and rendering helpers
  403. type Position struct {
  404. X, Y int
  405. }
  406. func (e *SVGExporter) calculateFlowchartLayout(diagram *ast.Flowchart) map[string]Position {
  407. positions := make(map[string]Position)
  408. x, y := 100, 100
  409. col := 0
  410. maxCols := 3
  411. for id := range diagram.Vertices {
  412. positions[id] = Position{X: x, Y: y}
  413. col++
  414. if col >= maxCols {
  415. col = 0
  416. x = 100
  417. y += 120
  418. } else {
  419. x += 200
  420. }
  421. }
  422. return positions
  423. }
  424. func (e *SVGExporter) renderFlowchartNodeSVG(vertex *ast.FlowVertex, pos Position) string {
  425. text := vertex.ID
  426. if vertex.Text != nil {
  427. text = *vertex.Text
  428. }
  429. return fmt.Sprintf(`
  430. <g transform="translate(%d,%d)">
  431. <rect x="-50" y="-20" width="100" height="40" class="flowNode"/>
  432. <text x="0" y="5" class="flowText">%s</text>
  433. </g>`, pos.X, pos.Y, text)
  434. }
  435. func (e *SVGExporter) renderFlowchartEdgeSVG(edge *ast.FlowEdge, from, to Position) string {
  436. return fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="flowEdge" marker-end="url(#arrowhead)"/>`,
  437. from.X, from.Y, to.X, to.Y)
  438. }
  439. func (e *SVGExporter) renderOrgNodeSVG(node *ast.OrganizationNode, x, y, level int) string {
  440. svg := fmt.Sprintf(`
  441. <g transform="translate(%d,%d)">
  442. <rect x="-80" y="-25" width="160" height="50" class="orgNode"/>
  443. <text x="0" y="5" class="orgText">%s</text>
  444. </g>`, x, y, node.Name)
  445. // Render children
  446. if len(node.Children) > 0 {
  447. childY := y + 100
  448. totalWidth := len(node.Children) * 200
  449. startX := x - totalWidth/2 + 100
  450. for i, child := range node.Children {
  451. childX := startX + i*200
  452. // Connection line
  453. svg += fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" class="orgEdge"/>`,
  454. x, y+25, childX, childY-25)
  455. // Recursively render child
  456. svg += e.renderOrgNodeSVG(child, childX, childY, level+1)
  457. }
  458. }
  459. return svg
  460. }
  461. // createArcPath creates SVG arc path for pie slices
  462. func (e *SVGExporter) createArcPath(centerX, centerY, radius, startAngle, endAngle float64) string {
  463. x1 := centerX + radius*math.Cos(startAngle)
  464. y1 := centerY + radius*math.Sin(startAngle)
  465. x2 := centerX + radius*math.Cos(endAngle)
  466. y2 := centerY + radius*math.Sin(endAngle)
  467. largeArc := 0
  468. if endAngle-startAngle > math.Pi {
  469. largeArc = 1
  470. }
  471. return fmt.Sprintf("M %g %g L %g %g A %g %g 0 %d 1 %g %g Z",
  472. centerX, centerY, x1, y1, radius, radius, largeArc, x2, y2)
  473. }
  474. // min returns the minimum of two integers
  475. func min(a, b int) int {
  476. if a < b {
  477. return a
  478. }
  479. return b
  480. }
  481. // New diagram type SVG export methods
  482. // exportMindmapToSVG exports mindmap to SVG
  483. func (e *SVGExporter) exportMindmapToSVG(diagram *ast.MindmapDiagram) (string, error) {
  484. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  485. <rect width="100%%" height="100%%" fill="white"/>
  486. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Mindmap Diagram</text>
  487. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d nodes</text>
  488. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Nodes))
  489. return svg, nil
  490. }
  491. // exportKanbanToSVG exports kanban to SVG
  492. func (e *SVGExporter) exportKanbanToSVG(diagram *ast.KanbanDiagram) (string, error) {
  493. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  494. <rect width="100%%" height="100%%" fill="white"/>
  495. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Kanban Board</text>
  496. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d columns</text>
  497. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Columns))
  498. return svg, nil
  499. }
  500. // exportGitToSVG exports git diagram to SVG
  501. func (e *SVGExporter) exportGitToSVG(diagram *ast.GitDiagram) (string, error) {
  502. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  503. <rect width="100%%" height="100%%" fill="white"/>
  504. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Git Graph</text>
  505. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d commits</text>
  506. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Commits))
  507. return svg, nil
  508. }
  509. // exportSankeyToSVG exports sankey diagram to SVG
  510. func (e *SVGExporter) exportSankeyToSVG(diagram *ast.SankeyDiagram) (string, error) {
  511. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  512. <rect width="100%%" height="100%%" fill="white"/>
  513. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Sankey Diagram</text>
  514. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d links</text>
  515. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Links))
  516. return svg, nil
  517. }
  518. // exportXYChartToSVG exports XY chart to SVG
  519. func (e *SVGExporter) exportXYChartToSVG(diagram *ast.XYChartDiagram) (string, error) {
  520. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  521. <rect width="100%%" height="100%%" fill="white"/>
  522. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">XY Chart</text>
  523. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d series</text>
  524. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Series))
  525. return svg, nil
  526. }
  527. // exportRadarToSVG exports radar chart to SVG
  528. func (e *SVGExporter) exportRadarToSVG(diagram *ast.RadarDiagram) (string, error) {
  529. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  530. <rect width="100%%" height="100%%" fill="white"/>
  531. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Radar Chart</text>
  532. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d series</text>
  533. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Series))
  534. return svg, nil
  535. }
  536. // exportTreemapToSVG exports treemap to SVG
  537. func (e *SVGExporter) exportTreemapToSVG(diagram *ast.TreemapDiagram) (string, error) {
  538. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  539. <rect width="100%%" height="100%%" fill="white"/>
  540. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Treemap</text>
  541. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d nodes</text>
  542. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Nodes))
  543. return svg, nil
  544. }
  545. // exportPacketToSVG exports packet diagram to SVG
  546. func (e *SVGExporter) exportPacketToSVG(diagram *ast.PacketDiagram) (string, error) {
  547. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  548. <rect width="100%%" height="100%%" fill="white"/>
  549. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Packet Diagram</text>
  550. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d flows</text>
  551. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Flows))
  552. return svg, nil
  553. }
  554. // exportInfoToSVG exports info diagram to SVG
  555. func (e *SVGExporter) exportInfoToSVG(diagram *ast.InfoDiagram) (string, error) {
  556. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  557. <rect width="100%%" height="100%%" fill="white"/>
  558. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Info Diagram</text>
  559. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d items</text>
  560. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Items))
  561. return svg, nil
  562. }
  563. // exportC4ToSVG exports C4 diagram to SVG
  564. func (e *SVGExporter) exportC4ToSVG(diagram ast.Diagram) (string, error) {
  565. // Get element count based on diagram type
  566. elementCount := 0
  567. switch d := diagram.(type) {
  568. case *ast.C4ContextDiagram:
  569. elementCount = len(d.Elements)
  570. case *ast.C4ContainerDiagram:
  571. elementCount = len(d.Elements)
  572. case *ast.C4ComponentDiagram:
  573. elementCount = len(d.Elements)
  574. case *ast.C4DynamicDiagram:
  575. elementCount = len(d.Elements)
  576. case *ast.C4DeploymentDiagram:
  577. elementCount = len(d.Elements)
  578. }
  579. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  580. <rect width="100%%" height="100%%" fill="white"/>
  581. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">C4 Diagram</text>
  582. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d elements</text>
  583. </svg>`, e.width, e.height, e.width/2, e.width/2, elementCount)
  584. return svg, nil
  585. }
  586. // exportQuadrantToSVG exports quadrant chart to SVG
  587. func (e *SVGExporter) exportQuadrantToSVG(diagram *ast.QuadrantChart) (string, error) {
  588. svg := fmt.Sprintf(`<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">
  589. <rect width="100%%" height="100%%" fill="white"/>
  590. <text x="%d" y="30" font-family="Arial" font-size="16" font-weight="bold" text-anchor="middle">Quadrant Chart</text>
  591. <text x="%d" y="50" font-family="Arial" font-size="12" text-anchor="middle">%d points</text>
  592. </svg>`, e.width, e.height, e.width/2, e.width/2, len(diagram.Points))
  593. return svg, nil
  594. }