“Most of us need to listen to the music to understand how beautiful it is. But often that’s how we present statistics: we just show the notes, we don’t play the music.”
Hans Rosling
I recently sat in a lecture about PlantUML and the advantages of Diagram As Code. Diagram As Code is a special variant of Documentation As Code. It is the possibility for software developers to create their documentation in the form of text files without the use of word processing systems. The advantage is obvious, the developer can use his documentation like his source code. Versioning, automatically creating comparisons, transforming, validating and much more. This gave me the idea of developing something similar.
Many systems can be used for documentation without word processing. There are markup languages such as \TeX and markdown languages such as Asciidoctor.
For diagrams, there are a number of different tools for creating a graphic from a text description. Well-known tools include PlantUML, Mermaid and the classic GnuPlot. PlantUML can be used, for example, to create a mind map for your own documentation.
@startmindmap - FreshMarker -- Telephone -- Holiday ++ FreshMarker File ++ FreshMarker Money ++ Freshmarker Random ++ FreshMarker Astro @endmindmap
This mind map shows the connections between the FreshMarker project and some other projects. A mind map like this could improve the FreshMarker documentation on the web a little.
The Freshmarker documentation is created with Asciidoctor. Asciidoctor offers the possibility to integrate PlantUML and other Diagram As Code applications directly. The following code can be used for this purpose.
.Projekt Mindmap [plantuml,format=svg] ---- @startmindmap - FreshMarker -- Telephone -- Holiday ++ FreshMarker File ++ FreshMarker Money ++ Freshmarker Random ++ FreshMarker Astro @endmindmap ----
Here the PlantUML mind map is inserted into an Asciidoctor code block. In order for Ascidoctor to handle this code block correctly, the Maven plugin must be supplemented with the necessary libraries.
<plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>${asciidoctor-maven-plugin.version}</version> <configuration> <sourceDocumentName>manual.adoc</sourceDocumentName> <backend>html5</backend> </configuration> <executions> <execution> <id>output-html-2.3.0</id> <phase>generate-resources</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <sourceDirectory>src/main/asciidoc/2.3.0</sourceDirectory> <outputDirectory>${project.build.directory}/generated-docs/2.3.0</outputDirectory> <requires><require>asciidoctor-diagram</require></requires> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>io.spring.asciidoctor</groupId> <artifactId>spring-asciidoctor-extensions-block-switch</artifactId> <version>${asciidoctor-extensions-spring-boot}</version> </dependency> <dependency> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctorj-diagram</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctorj-diagram-plantuml</artifactId> <version>1.2025.3</version> </dependency> </dependencies> </plugin>
The requires
line is important so that the Ascidoctor Diagram extension is activated.
But now we want to implement our own Diagram As Code solution. As fate would have it, there was a small error in the benchmark diagram on the project page. So far, the diagram has been generated separately with GnuPlot, which means that every six months I have to find out how I generated the diagram last time.
So what if the diagram is generated automatically with the documentation and only requires a few lines of documentation? The easiest way in this case is the technical design.
[barchart,width=800,height=600] ---- data: 31451, 51253, 43149, 32204, 36140, 3078, 54861 x-labels: Freemarker, FreshMarker, Mustache, Handlebars, Velocity, Thymeleaf, Pebble ----
A code block in Asciidoctor is also used here. The first line contains the ops/s of the benchmarks and the second line contains the names of the template engines for the measured data. These two lines are to be displayed as a bar chart, which is why the square brackets at the beginning contain barchart
and then the dimensions of the chart as attributes.
Of course, it doesn’t work that way yet. Asciidoctor does not know how to display a bar chart. But there is a possibility to extend Asciidoctor that I wrote about five years ago.
An extension for Asciidoctor in Java is quickly created and can then take over the task of generating a nice bar chart from our two lines of text.
@Name("barchart") @Contexts(Contexts.LISTING) public class BarChartProcessor extends BlockProcessor { @Override public Object process(StructuralNode parent, Reader reader, Map<String, Object> attributes) { return createBlock(parent, "pass", "<div class=\"bar-chart\"></div>"); } }
This BarChartProcessor
does not yet generate a bar chart, but an empty div
as soon as it detects a code block with the name barchart
. We can already use this BlockProcessor
by making it known in the Maven plugin.
<plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>${asciidoctor-maven-plugin.version}</version> <configuration> <sourceDocumentName>manual.adoc</sourceDocumentName> <backend>html5</backend> </configuration> <executions> <execution> <id>output-html-2.3.0</id> <phase>generate-resources</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <sourceDirectory>src/main/asciidoc/2.3.0</sourceDirectory> <outputDirectory>${project.build.directory}/generated-docs/2.3.0</outputDirectory> <extensions> <extension> <className>de.schegge.diagram.BarChartProcessor</className> <blockName>barchart</blockName> </extension> </extensions> <requires><require>asciidoctor-diagram</require></requires> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>io.spring.asciidoctor</groupId> <artifactId>spring-asciidoctor-extensions-block-switch</artifactId> <version>${asciidoctor-extensions-spring-boot}</version> </dependency> <dependency> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctorj-diagram</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctorj-diagram-plantuml</artifactId> <version>1.2025.3</version> </dependency> <dependency> <groupId>de.schegge</groupId> <artifactId>JDiagram</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </plugin>
The new extension
entry is important here, as Asciidoctor now knows our custom BlockProcessor
.
To obtain a graphic in our document, we generate an SVG image from the data. An SVG can be scaled well and requires very little memory for diagrams. An SVG can be generated manually, as it is only an XML document. But Java has its own graphics library for drawing. An Java SVG library based on this is JFreeSVG.
@Name("barchart") @Contexts(Contexts.LISTING) public class BarChartProcessor extends BlockProcessor { @Override public Object process(StructuralNode parent, Reader reader, Map<String, Object> attributes) { double width = Double.parseDouble((String)attributes.getOrDefault("width", "800")); double height = Double.parseDouble((String)attributes.getOrDefault("height", "600")); SVGGraphics2D graphics2D = new SVGGraphics2D(width, height); BarChart barChart = readBarChart(reader.readLines()); List<Double> data = barChart.data(); if (!data.isEmpty()) { YAxisConfig yAxisConfig = YAxisCalculator.calculate(data, true, "%.0f"); FontMetrics fontMetrics = graphics2D.getFontMetrics(graphics2D.getFont()); int maxLabelWidth = yAxisConfig.labels().stream().map(fontMetrics::stringWidth).max(Comparator.naturalOrder()).orElse(0); Border border = new Border(20, 20 + maxLabelWidth, 20 + fontMetrics.getHeight(), 20); drawYGrid(height, border, yAxisConfig, graphics2D, width); drawYTics(height, border, yAxisConfig, graphics2D); drawXTics(height, border, graphics2D, width, barChart); drawBars(graphics2D, border, yAxisConfig, width, height, data, "%.0f"); drawBorder(graphics2D, border, width, height); } return createBlock(parent, "pass", "<div class=\"bar-chart\">\n" + graphics2D.getSVGElement() + "\n</div>"); } // ... }
JFreeSVG allows you to create a special Graphics2D
object and then draw with it using all the possibilities of the Java library. In our BlockProcessor
we create such an SVGGraphics2D
object with the optional height
and width
specifications. We receive the two bar chat description lines from the code block via reader.readLines()
and create a BarChart
instance from them.
The bar chart is then drawn from the specified data. The details are not important at this point, only that at the end the content of the Graphics2D
object can be extracted via getSVGElement()
as an SVG description for our HTML code.
The result of our example as an SVG in the documentation is shown on the left. A simple change in the documentation with a new value in both lines is shown in the diagram on the right.
This is an example of how meaningful diagrams can be inserted into a documentation without much work. I already have a lot of ideas for these Asciidoctor extension. Let’s see when I find the time.