Spider Charts with Playfair

Spider charts, also known as radar charts, are among the most versatile tools for data visualization. These circular charts make it possible to display multiple variables simultaneously and present complex data sets in an intuitive form. They are particularly valuable for analyzing performance indicators, evaluations, or comparisons between different objects or time periods.

The characteristic spider-like structure is created by radially arranged axes that originate from the center and each represent one dimension of the data. Connecting the data points creates polygonal shapes that reveal strengths, weaknesses, and patterns in the data at a glance. This visual representation makes spider charts an indispensable tool for analysts, researchers, and decision-makers who want to quickly grasp and communicate complex relationships.

One use case for spider charts is in software architecture, where the LASR (Lightweight Approach for Software Reviews) approach is used to systematically evaluate architectural decisions. LASR evaluates multiple quality attributes such as performance, security, maintainability, and scalability—exactly the kind of multidimensional data for which spider charts were designed. While manual creation is perfectly feasible for occasional reviews, automation offers significant advantages for regular reviews or the evaluation of multiple architectural alternatives. Automated solutions extract LASR evaluation data directly from review logs or evaluation matrices and seamlessly convert it into spider charts, providing architecture teams with consistent, instantly available visual insights into the strengths and weaknesses of their architectures.

In this post, I will implement simple spider charts for LASR architecture charts in Playfair. As a first step, I will define an example description for a spider chart in Playfair notation.

[spiderchart,target=spiderchart-lasr]
----
title: LASR Result Diagram
title.font: Roboto bold 36
label.font: Roboto plain 16
background: #F5F5F5
grid.color: #C0C0C0
groups: Target line, Assessed status
data.1: 60, 75, 80, 70, 90
data.2: 70, 65, 60, 50, 50
index.labels: Security, Usability, Compatibility, Scalability, Sustainability
style: outline
colors: MediumSeaGreen, DodgerBlue
----

In the Asciidoctor environment, the chart type is selected using the spiderchart keyword. The individual graphs are represented as groups. In this example, these are the target line and the assessed status. The names for the five axes can be found under index.labels. In this LASR analysis, we are dealing with the categories of security, usability, compatibility, scalability, and sustainability. The result of our Asciicoctorj extension is the following chart on the left.

On the right-hand side, you can see the data as a radar chart. The difference is obvious, as is the origin of the name for this chart. Due to the large overlap in implementations, most of it is drawn in the common base class AbstractSpiderChart. The exception is the spider web, which is drawn in the drawNet method.

protected void drawNet(ValueAxisTicks valueAxisTicks, int diameter, double step, Graphics2D g2d, int axisCount, double angleExtend, Point2D center, int radius) {
  for (int i = 0; i < valueAxisTicks.tickCount() - 1; i++) {
    double circleRadius = radius - i * step;
    g2d.draw(getShape(axisCount, angleExtend, center, circleRadius));
  }
}

private Path2D getShape(int axisCount, double angleExtend, Point2D center, double circleRadius) {
  Path2D path = new Path2D.Double();
  Point2D first = CircleCalculations.position(center, circleRadius, CircleCalculations.HALF_PI);
  path.moveTo(first.getX(), first.getY());
  for (int j = 1; j < axisCount; j++) {
    double angle = CircleCalculations.HALF_PI - j * angleExtend;
    Point2D p = CircleCalculations.position(center, circleRadius, angle);
    path.lineTo(p.getX(), p.getY());
  }
  path.closePath();
  return path;
}

The implementation is somewhat more complex for the spider chart because the intersections with the axes must be determined and a Path2D generated from this.

protected void drawNet(ValueAxisTicks valueAxisTicks, int diameter, double step, Graphics2D circle2g, int axisCount, double angleExtend, Point2D center, int radius) {
  for (int i = 0; i < valueAxisTicks.tickCount() - 1; i++) {
    double circleWidth = diameter - 2 * i * step;
    circle2g.draw(getShape(step, i, circleWidth));
  }
}

private Shape getShape(double step, int i, double circleWidth) {
  return new Ellipse2D.Double(i * step, i * step, circleWidth, circleWidth);
}

The radar chart is somewhat simpler because no path is required; only a circle is drawn.

Since the number of chart types has grown and there are relatively many configuration options, manually checking the charts becomes problematic. This can be remedied by using custom Junit 6 assertions based on the following code.

private static void assertEquals(String name,  Path expected, Path actual, Path temp) throws IOException {
  Path expectedPng = createPath(temp.resolve( "expected"), expected.resolve(name));
  Path actualPng = createPath(temp.resolve("actual"), actual.resolve(name));
  ImageSaver.safe(expected.resolve(name), expectedPng);
  ImageSaver.safe(actual.resolve(name), actualPng);
  BufferedImage expectedImage = ImageIO.read(expectedPng.toFile());
  BufferedImage actualImage = ImageIO.read(actualPng.toFile());

  Assertions.assertEquals(expectedImage.getWidth(), actualImage.getWidth(), "width");
  Assertions.assertEquals(expectedImage.getHeight(), actualImage.getHeight(), "height");

  for (int x = 0; x < expectedImage.getWidth(); x++) {
    for (int y = 0; y < actualImage.getHeight(); y++) {
      if (expectedImage.getRGB(x, y) != actualImage.getRGB(x, y)) {
        Assertions.fail();
      }
    }
  }
}

For a chart with the filename name, an SVG version from the expected folder and one from the target folder are loaded and saved as PNG files in the temp folder. The expected folder contains files that generate the expected appearance of the charts, and actual is the folder in which the tests save the newly generated charts.

It may seem surprising why a PNG is generated and why it is saved instead of simply comparing the SVG files with each other. Comparing the SVG files can be misleading if only the structure of the SVG file has changed but the generated graphic has remained the same. The files are saved so that in the event of an error, it is easy to check what is wrong with the PNG graphics.

Due to its philosophy, Playfair has some limitations regarding the charts it generates. For spider charts and radar charts, these limitations can be found in the axis scaling. Unlike general spider charts, Playfair cannot scale the axes independently of each other.

Of course, Playfair can do a little more than just draw LASR diagrams. As the example above shows, spider charts with a different number of axes can also be generated, and the area inside the individual graphs can be colored if desired. In the example above, semi-transparency was chosen for the areas. Spider charts are only drawn if at least three axes have been defined, and the drawing method for three axes is slightly modified.

This comparison shows why a different drawing method had to be chosen for three axes. When positioning the corners of the regular polygons on a circle, a very large open space at the bottom results for the triangle. This space makes the positioning on the page appear to be moved far up and the chart as a whole looks very small. The other regular polygons fit much better, and since Archimedes, we know that as the number of axes increases, the polygons approach a circle more and more closely.

To create a larger triangular spider chart, we select the circle diameter as the side length of the new triangle. We create the polygon using the method used previously and need the radius of the circumcircle to do so. The necessary formula is r_{u} = \frac{a}{\sqrt{3}}, with the side length a. As you can see, the new triangle is much larger and uses up most of the previously unused lower area.

Spider charts with only three axes are not used very often, so this brief excursion should suffice to explain some of the special features of their implementation. The aesthetics of the output are often not apparent in the source code.

Shortly before the new release of Playfair, I discovered a strange error in the legend. The markers for the individual charts were too close to the labels. It should be noted that the markers are rendered as Unicode characters. To maintain the necessary distance from the label, a whitespace was added for the width calculation.

As you can see on the left, the circle extends beyond the font, even though space for the circle and a space for the label position were included.

At first, I thought it was a problem with my own code, but some tests showed that this problem occurs with the Roboto font and not with some other fonts. Therefore, I suspected a problem in the EchoSVG library, but a test without EchoSVG now points to a problem directly in Java AWT. Since I have never submitted a bug report for a JDK, I will first take a look at what needs to be considered there.

If you don’t need the Roboto font and want to use the new spider charts and radar charts, you can try the latest Playfair version 0.9.0.

Leave a Comment