Radar/rosace graphism with perl and GD

For audits processing, i've been asked to generate Rosace/Radar type of graphism on the fly. It's a really efficient way to compare annual results on a monthly basis and many other things. We will use the GD module from perl to do it.

Two series of numbers must be compared. We will draw three types of areas, thoses exclusive to one of the series and the one that is shared by both series. The output will be a png image as it is the most optimised way. Here's an example :

example

First we extract the two series of data by using the CGI module :

$query = new CGI;
$params = $query->param("val1");

We chomp both strings and then split the results in an array as individual values are separated by spaces. As the trigonometrical order is opposed to our way of reading the data clockwise, we will now invert both series stored in arrays :

chomp(params);
@suite1 = reverse split / /,$params;

Now we initialize the GD module by specificating the size of the image and then declaring the colours we will use, white for the background, black for the grid, green and blue for series and a mix of both for shared areas :

$im = new GD::Image(200,200);
$white = $im->colorAllocate(255,255,255);
$black = $im->colorAllocate(0,0,0);
$mid = $im->colorAllocate(0,128,128);
$blue = $im->colorAllocate(0,0,255);
$green = $im->colorAllocate(0,255,0);
$im->transparent($white);
$im->interlaced('true');

We need to calculate π and then the angle represented by each slice :

$nbparam = scalar(@suite1);
$pi = atan2(1,1) * 4;
$tranche = 2 * $pi / $nbparam;

To determine the scale of the graph, we will browse both series to find the highest value :

$max = 0;
foreach (@suite1) {
  $max = $_ if ($_ > $max);
}
foreach (@suite2) {
  $max = $_ if ($_ > $max);
}

We are now able to calculate the coordinates of each point using the following formula :

$prevx1 = (sin ($pi) / $max * $suite1[$nbparam-1] * $size / 2) + $size / 2;
$prevy1 = (cos ($pi) / $max * $suite1[$nbparam-1] * $size / 2) + $size / 2;

The first coordinates are precalculated and then we cycle through data with a loop matching each slice. Once we have calculated the four points, we need to distinguish between the four possible cases, the cases when one pair of data is superior to the other and when there is a switch in the highest value between the two series.

The non crossing cases are easy to process :

  if (($suite1[$i] >= $suite2[$i]) and ($prevmax1 >= $prevmax2)) {
    $poly = new GD::Polygon;
    $poly->addPt($size / 2, $size / 2);
    $poly->addPt($prevx2,$prevy2);
    $poly->addPt($xfull2,$yfull2);
    $im->filledPolygon($poly,$mid);
    $poly = new GD::Polygon;
    $poly->addPt($prevx2,$prevy2);
    $poly->addPt($xfull2,$yfull2);
    $poly->addPt($xfull1,$yfull1);
    $poly->addPt($prevx1,$prevy1);
    $im->filledPolygon($poly,$blue);
  } elsif (($suite2[$i] >= $suite1[$i]) and ($prevmax2 >= $prevmax1)) {
    $poly = new GD::Polygon;
    $poly->addPt($size / 2, $size / 2);
    $poly->addPt($prevx1,$prevy1);
    $poly->addPt($xfull1,$yfull1);
    $im->filledPolygon($poly,$mid);
    $poly = new GD::Polygon;
    $poly->addPt($prevx1,$prevy1);
    $poly->addPt($xfull1,$yfull1);
    $poly->addPt($xfull2,$yfull2);
    $poly->addPt($prevx2,$prevy2);
    $im->filledPolygon($poly,$green);

The crossing series are more difficult as we need to calculate the intersection point that is the fourth point needed to design the shared area polygon. To do so we use vectorial calculations. You can check out the demonstration here. Here is the first case:

Calculation of alpha and beta :

    $alpha = $suite1[$i] / $suite2[$i];
    $beta = $prevmax2 / $prevmax1;

Calculation of lambda and gamma :

    $gamma = ($alpha - ($alpha * $beta)) / (1 - ($beta * $alpha));
    $lambda = 1 - ($gamma / $alpha);

Calculation of the CB (CO+OB) vector :

    $xvco = ($size / 2) - $xfull1;
    $yvco = ($size / 2) - $yfull1;
    $xvob = $prevx1 - ($size / 2);
    $yvob = $prevy1 - ($size / 2);
    $xvcb = $xvco + $xvob;
    $yvcb = $yvco + $yvob;

Calculation of the OM = OC + lambda CB vector :

    $xvom = $xfull1 - ($size / 2) + ($lambda * $xvcb);
    $yvom = $yfull1 - ($size / 2) + ($lambda * $yvcb);

Deduction of the interscetion points coordinates :

    $xxx = $xvom + ($size / 2);
    $yyy = $yvom + ($size / 2);

Drawing the exclusives areas :

    $poly = new GD::Polygon;
    $poly->addPt($xxx,$yyy);
    $poly->addPt($prevx1,$prevy1);
    $poly->addPt($prevx2,$prevy2);
    $im->filledPolygon($poly,$blue);
    $poly = new GD::Polygon;
    $poly->addPt($xxx,$yyy);
    $poly->addPt($xfull1,$yfull1);
    $poly->addPt($xfull2,$yfull2);
    $im->filledPolygon($poly,$green);

Drawing the shared area :

    $poly = new GD::Polygon;
    $poly->addPt($size / 2, $size / 2);
    $poly->addPt($prevx2,$prevy2);
    $poly->addPt($xxx,$yyy);
    $poly->addPt($xfull1,$yfull1);
    $im->filledPolygon($poly,$mid);

Now is time to draw the grid over the graph :

$curangle = $pi;
$prevangle = $pi;
for $i (0..$nbparam-1) {
  $curangle += $tranche;
  $xmag = (sin ($curangle) * $size / 2) + $size / 2;
  $ymag = (cos ($curangle) * $size / 2) + $size / 2;
  $im->line($size/2, $size/2, $xmag, $ymag, $black);
  foreach $qu (0.2 , 0.4, 0.6, 0.8, 1) {
    $thex = (sin ($curangle) * $size / 2 * $qu) + $size / 2;
    $they = (cos ($curangle) * $size / 2 * $qu) + $size / 2;
    $thexo = (sin ($prevangle) * $size / 2 * $qu) + $size / 2;
    $theyo = (cos ($prevangle) * $size / 2 * $qu) + $size / 2;
    $im->line($thex,$they,$thexo,$theyo,$black);
  }
  $prevangle = $curangle;
}

The graph is now completed but when must make it an image visible to the user, that is converting binary data encoded in png format. Headers are included first to let the browser know what type of data to expect:

binmode STDOUT;
print "Content-type: image/png\n";
print "Pragma: no-cache\n";
print "Expires: now\n\n";

print $im->png;

Demonstration: Enter two series of of the same number of figures, separated by spaces :

radar.zip

main menu