Chapter 8. Charts

OpenOffice.org includes a powerful charting function. You may place a chart in a word processing document, a drawing, a presentation, or a spreadsheet.

Rather than being inserted directly into the content.xml, the chart is inserted as a <draw:object> element. This element will have an xlink:href attribute of the form #./Object 1. If you know your URLs, you see that this points to a subdirectory named Object 1. Inside that directory you will find another content.xml file that contains the chart data and style information.

In addition to creating the link and the subdirectory, you must also add entries in the META-INF/manifest.xml file in order for OpenOffice.org to locate the chart. The entries must define the paths for the subdirectory and the content.xml and style.xml file in the subdirectory (if present).

Charts in spreadsheets are special; they display data that is within the rows and columns of the spreadsheet. Here are the <draw:object> attributes in question:

Example 8.2, “XML for Chart in Spreadsheet” shows the XML that embeds a chart shown in Figure 8.1, “Chart Derived from Spreadsheet” into a spreadsheet as.

You find the actual chart data and specifications in the content.xml file that is in the object subdirectory. This file follows the same general pattern that we have seen for content files of all the other document types. Its root <office:document-content> element will have an office:class attribute of chart. The first child of the <office:document> is an <office:automatic-styles> element that contains all the styles to control the chart’s presentation.

The styles are followed by the <office:body>, which contains a <chart:chart> element. This has child elements that specify:

Now let’s take a closer look at the chart:chart element and its attribute and children. The chart:class attribute tells what kind of chart to draw: line, area (stacked areas), circle (pie chart), ring, scatter, radar (called “net”) in OpenOffice.org, bar (vertical bars), stock, and add-in.

The <chart:chart> element has these children, in this order:

The <chart:title> and <chart:subtitle> elements have svg:x and svg:y attributes for positioning, and a chart:style-name for presentation, They contain a <text:p> element that gives the title (or subtitle) text, as shown in Example 8.3, “Example of Chart Title and Subtitle”

The <chart:legend> element has a chart:legend-position attribute that gives the relative location of the legend; top, left, bottom, or right, and an absolute svg:x and svg:y position. It also has a chart:style-name attribute to determine the presentation of the text in the legend.

The next element in line is a <chart:plot-area> element is where the action is. It establishes the location of the chart with the typical svg:x, svg:y, svg:width, and svg:height attributes.

If you are creating a chart from a spreadsheet, you will specify the source of the data in the table:cell-range-address attribute. Depending on whether this range of cells contains labels for the rows or columns, you must set chart:data-source-has-labels to none, row, column, or both. The <chart:table-number-list> is not used in the XML format, and should be set to 0.

You may be tempted to overlook the standard chart:style-name attribute, but that would be a mistake, because that style is just packed with information.

chart:lines
true for a line chart, false for any other type of chart.
chart:symbol
Used only with line charts, this is set to a negative number, the default being -1. I have no idea what the value of this attribute means.
chart:splines, chart:spline-order, chart:spline-resolution
If you are using splines instead of lines, then chart:splines will be 1 instead of 0, and you must specify the chart:spline-order (2 for cubic splines). The chart:spline-resolution tells how smooth the curve is; the larger the number, the smoother the curve; the default value is 20.
chart:vertical, chart:stacked, chart:percentage, chart:connect-bars
These booleans are used for bar charts. If chart:vertical is true then bars are drawn along the vertical axis from left to right (the default is false for bars drawn up and down along the horizontal axis). chart:stacked tells whether bars are stacked or side-by-side. This is mutually exclusive with chart:percentage, which draws stacked bars by default. The chart:connect-bars attribute is only used for stacked bars or percentage charts; it draws lines connecting the various levels of bars.
chart:lines-used
The default value is zero; it is set to one if a bar chart has lines on it as well.
chart:stock-updown-bars, chart:stock-with-volume
These boolean attributes apply only when chart:class is stock.
chart:series-source
If your source data has its data series in rows instead of columns, set this attribute to rows instead of the default columns.
chart:data-label-number
Is the data labeled with the value, a percentage, or none (the default)
chart:data-label-text, chart:data-label-symbol
Should all data points have a text label (the name of the corresponding series) and/or the legend symbol next to them? Set these to true or the default false.

Example 8.4, “Plot Area and Style” shows the opening <chart:plot-area> element (and its associated style) for the bar chart in Figure 8.1, “Chart Derived from Spreadsheet”

1 If you are creating a bar chart from scratch, these three attributes are required.
2 If you were creating a line chart, you’d need these attributes, but you can leave them out for a bar chart.
3 These are all set to none or false so that no extra labelling appears next to the data points.
4 Because this is an “essentials” book, we didn’t talk about these attributes at all. They are used if you use the Insert/Statistics menu in OpenOffice.org.
5 Finally, these attributes are all false because this is neither a stock chart nor a three-d chart.

Within the <chart:plot-area> element are two <chart:axis> elements; the first for the x-axis and the second for the y-axis. For pie charts, there is only one axis; the y-axis.

Each <chart:axis> has a chart:name attribute, which is either primary-x or primary-y. The chart:class attribute tells whether the axis represents a category, value, or domain. (This last is for the x-axis of a scatter chart.) Of course, there’s a chart:style-name, and the style it refers to also contains oodles of information about how to display the axis:

chart:display-label
A boolean that determines whether to display a label with this axis or not.
chart:tick-marks-major-inner, chart:tick-marks-major-outer, chart:tick-marks-minor-inner, chart:tick-marks-minor-outer
These four booleans tell whether you want tick marks at major and minor intervals, and whether you want them to appear outside the chart area or inside the chart area.
chart:logarithmic
Set this to true if you want a logarithmic scale for the numbers on the given axis.
text:line-break
In order to fit labels into small charts, OpenOffice.org will break words. For example, a category label of “Northwest” may appear with “North” on one line and “west” beneath it. You can turn off this action by setting the attribute to false.
chart:text-overlap
If you turn off line break and your chart is small, but its labels are long, then the labels may overlap. If you don’t want this to happen, set this attribute to its default value of false. OpenOffice.org will then avoid displaying some of the labels rather than have labels display on top of one another. If you don’t mind the overwriting, set this attribute to true.
chart:label-arrangement
Ordinarily the labels on a chart appear side-by-side (the default value). You may avoid overlap by setting this value to stagger-even or stagger-odd. Figure 8.4, “Chart With Even-Staggered Labels” shows the labels for a chart with this attribute set to stagger-even.
chart:visible
Set this to false if you don’t want to see any labels or tick marks at all.

Warning

Don’t set this to false unless you have a compelling reason to do so. Graphs without labels are confusing at best and misleading or useless at worst.

If your axis has a title, then the <chart:axis> element will have a <chart:title> child element, formatted exactly like the chart’s main title.

The last child of the <chart:axis> element is the optional <chart:grid> element. Its <chart:class> attribute tells whether you want grid lines at major intervals only (major), or at both major and minor intervals (minor). For no grid lines, omit the element.

This has been an immense amount of explanation, and we need to see how this all fits together. Example 8.5, “Styles and Content for a Bar Chart” shows the XML (so far) for the chart shown in Figure 8.1, “Chart Derived from Spreadsheet”,

Example 8.5. Styles and Content for a Bar Chart

<chart:chart chart:class="bar" chart:style-name="ch1"
  svg:width="8cm" svg:height="7cm" >

    <chart:title svg:x="2.564cm" svg:y="0.14cm" chart:style-name="ch2">
        <text:p>Sales Report</text:p>
    </chart:title>

    <chart:subtitle svg:x="2.741cm" svg:y="0.953cm" chart:style-name="ch3">
        <text:p>First Quarter</text:p>
    </chart:subtitle>

    <chart:legend chart:legend-position="right"
      svg:x="6.476cm" svg:y="2.833cm" chart:style-name="ch4"/>

    <chart:plot-area chart:style-name="ch5"
      table:cell-range-address="Sheet1.$A$1:.$E$4"
      chart:data-source-has-labels="both" chart:table-number-list="0"
      svg:x="0.16cm" svg:y="1.69cm"
      svg:width="5.997cm" svg:height="5.17cm">

        <chart:axis chart:class="category"
          chart:name="primary-x" chart:style-name="ch6">
            <chart:title svg:x="3.345cm" svg:y="6.484cm"
                chart:style-name="ch7">
                <text:p>Month</text:p>
            </chart:title>
        </chart:axis>

        <chart:axis chart:class="value"
          chart:name="primary-y" chart:style-name="ch8">
            <chart:title svg:x="0.16cm" svg:y="4.52cm"
              chart:style-name="ch7">
                <text:p>Units Sold</text:p>
            </chart:title>
            <chart:grid chart:class="major"/>
        </chart:axis>

        <chart:series chart:style-name="ch9">
            <chart:data-point chart:repeated="3"/>
        </chart:series>
        
        <chart:series chart:style-name="ch10">
            <chart:data-point chart:repeated="3"/>
        </chart:series>

        <chart:series chart:style-name="ch11">
            <chart:data-point chart:repeated="3"/>
        </chart:series>

        <chart:series chart:style-name="ch12">
            <chart:data-point chart:repeated="3"/>
        </chart:series>

        <chart:wall chart:style-name="ch13"/>
        <chart:floor chart:style-name="ch14"/>
    </chart:plot-area>

    <!-- data table follows -->
</chart:chart>

Example 8.6, “Styles for Bar Chart Excerpt” shows the corresponding styles, cut down to the absolute minimum necessary. (For example, in style ch9, the bars have no labels or statistics, so we have been able to dispense with attributes such as chart:data-label-number and fo:font-family and chart:error-margin. For variety, we have used fo:font-family on some styles to explicitly specify a font, and in others we have used style:font-family-generic to specify the font. Comments have been added to indicate which styles apply to which parts of the chart.

Example 8.6. Styles for Bar Chart Excerpt


<!-- style for <chart:chart> element -->
<style:style style:name="ch1" style:family="chart">
    <style:properties
      draw:stroke="solid" draw:fill="solid" draw:fill-color="#ffffff" />
</style:style>

<!-- style for <chart:title> element -->
<style:style style:name="ch2" style:family="chart">
    <style:properties fo:font-family="&apos;Bitstream Vera Sans&apos;"
       style:font-family-generic="swiss"
       fo:font-size="13pt" />
</style:style>

<!-- style for <chart:subtitle> element -->
<style:style style:name="ch3" style:family="chart">
    <style:properties fo:font-family="&apos;Bitstream Vera Sans&apos;"/>
</style:style>

<!-- style for <chart:legend> element -->
<style:style style:name="ch4" style:family="chart">
    <style:properties style:font-family-generic="swiss"
      fo:font-size="6pt"/>
</style:style>

<!-- style for <chart:plot-area> element -->
<style:style style:name="ch5" style:family="chart">
    <style:properties chart:vertical="false" chart:lines-used="0"
      chart:connect-bars="false" chart:series-source="columns"/>
</style:style>

<!-- style for first <chart:axis> (x-axis) -->
<style:style style:name="ch6" style:family="chart"
  style:data-style-name="N0">
    <style:properties chart:display-label="true"
      chart:tick-marks-major-inner="false"
      chart:tick-marks-major-outer="true" 
      draw:stroke="solid" svg:stroke-width="0cm"  
      style:font-family-generic="swiss" fo:font-size="7pt"/>
</style:style>

<!-- style for <chart:title> in both axes -->
<style:style style:name="ch7" style:family="chart">
    <style:properties fo:font-family="&apos;Bitstream Vera Sans&apos;"
      fo:font-size="9pt"/>
</style:style>

<!-- style for the second <chart:axis> element (y-axis) -->
<style:style style:name="ch8" style:family="chart"
  style:data-style-name="N0">
    <style:properties chart:display-label="true"
      chart:tick-marks-major-inner="false"
      chart:tick-marks-major-outer="true"
      fo:font-family="&apos;Bitstream Vera Sans&apos;"
      fo:font-size="7pt"/>
</style:style>

<!-- style for the first <chart:series> element -->
<style:style style:name="ch9" style:family="chart">
    <style:properties draw:fill-color="#9999ff"/>
</style:style>

<!-- style for the second <chart:series> element -->
<style:style style:name="ch10" style:family="chart">
    <style:properties draw:fill-color="#993366"/>
</style:style>

<!-- style for the third <chart:series> element -->
<style:style style:name="ch11" style:family="chart">
    <style:properties draw:fill-color="#ffffcc"/>
</style:style>

<!-- style for the fourth <chart:series> element -->
<style:style style:name="ch12" style:family="chart">
    <style:properties draw:fill-color="#ccffff"/>
</style:style>

<!-- style for the <chart:wall> element -->
<style:style style:name="ch13" style:family="chart">
    <style:properties draw:fill="solid" draw:fill-color="#ffffff"/>
</style:style>

<!-- style for the <chart:floor> element -->
<style:style style:name="ch14" style:family="chart">
    <style:properties draw:fill-color="#999999"/>
</style:style>

Following the plot area is a table containing the data to be displayed. Even if you are creating a chart from a spreadsheet, OpenOffice.org does not look at the spreadsheet cells for the data—it looks at the internal table in the chart object’s content.xml file.

Compared to the chart and plot area definitions, the data table is positively anticlimactic. The <table:table> element has a table:name attribute which is set to local-table.

The first child of the <table:table> is a <table:table-header-columns> element that contains an empty <table:table-column> element. This is followed by a <table:table-header-rows> element that contains the first row of the table. Finally, a <table:table-rows> element contains the remaining data, one <table:table-row> at a time.

Example 8.7, “Table for Bar Chart” gives an excerpt of the table that was used in Figure 8.1, “Chart Derived from Spreadsheet”.

Example 8.7. Table for Bar Chart

<table:table table:name="local-table">
    <table:table-header-columns>
        <table:table-column/>
    </table:table-header-columns>
    <table:table-columns>
        <table:table-column table:number-columns-repeated="4"/>
    </table:table-columns>
    
    <table:table-header-rows>
        <table:table-row>
            <table:table-cell>
                <text:p/>
            </table:table-cell>
            <table:table-cell table:value-type="string">
                <text:p>Widgets</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="string">
                <text:p>Thingies</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="string">
                <text:p>Doodads</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="string">
                <text:p>Whatzits</text:p>
            </table:table-cell>
        </table:table-row>
    </table:table-header-rows>
    
    <table:table-rows>
        <table:table-row>
            <table:table-cell table:value-type="string">
                <text:p>Jan</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="10">
                <text:p>10</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="20">
                <text:p>20</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="29">
                <text:p>29</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="15">
                <text:p>15</text:p>
            </table:table-cell>
        </table:table-row>
        
        <!-- February row, similar to January above -->
        
        <table:table-row>
            <table:table-cell table:value-type="string">
                <text:p>Mar</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="22">
                <text:p>22</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="27">
                <text:p>27</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="31">
                <text:p>31</text:p>
            </table:table-cell>
            <table:table-cell table:value-type="float" table:value="29">
                <text:p>29</text:p>
            </table:table-cell>
        </table:table-row>
    </table:table-rows>
</table:table>

We are now prepared to do a rather complex case study. We will begin with an OpenOffice.org spreadsheet that contains the results of a survey[15], as shown in Figure 8.5, “Spreadsheet with Survey Responses”. Our goal is to create a word processing document. Each question will be displayed in a two-column section. The left column will contain the question and the results in text form; the right column will contain a pie chart of the responses to the question. The result will look like Figure 8.6, “Text Document with Survey Responses”

While we did write the OOoTransform.java program to create .zip format files, writing a program to create zip files with subdirectories seems a bit too much like work. Instead, we will write a shell script that creates a temporary directory, runs an Perl program to create the individual files in that directory, and then zips those files into our finished word processing document. Example 8.8, “Shell Script for Chart Document Creation” shows the script for the bash shell. It takes two parameters: the input file name (without the .sxc extension) and the name of the temporary directory where the XML files are stored.

If you don’t have a way to make a directory with all its intervening levels, use the utility program in the section called “Creating Multiple Directory Levels”.

The Perl code is fairly lengthy, though most of it is just “boilerplate.” We have broken it into sections. for ease of analysis. We will use the XML::DOM module to parse the input file for use with the Document Object Model. We won’t use it to create the output file; we’ll just create raw XML text and put it into files. Let’s begin with the variable declarations and some utility routines.

#!/usr/bin/perl

use Archive::Zip;
use XML::DOM;
use warnings;
use strict;

#
#   Command line arguments:
#       input file
#       output directory

my $doc;        # the DOM document
my $rows;       # all the <table:table-row> elements
my $n_rows;     # number of rows
my $row;        # current row number
my $col;        # current column number
my @data;       # contents of current row
my $sum;        # sum of the row items
my @legends;    # legends for the graph

my $percent;    # string holding nicely formatted percent value

#
#   Extract the content.xml file from the given
#   filename, parse it, and return a DOM object.
#
sub makeDOM
{
    my ($filename) = shift;
    my $zip = Archive::Zip->new( $filename );
    my $parser = new XML::DOM::Parser;
    my $doc;
    
    $zip->extractMember( "content.xml", "$ARGV[1]/workfile" );

    $doc = $parser->parsefile( "$ARGV[1]/workfile" );
    unlink( "$ARGV[1]/workfile" );
    return $doc;
}

#
#   $node - starting node
#   $name - name of desired child element
#   returns the node's first child with the given name
#
sub getFirstChildElement 1
{
    my ($node, $name) = @_;
    for my $child ($node->getChildNodes)
    {
        if ($child->getNodeName eq $name)
        {
            return $child;
        }
    }
    return undef;
}

#
#   $node - starting node
#   $name - name of desired sibling element
#   returns the node's next sibling with the given name
#
sub getNextSiblingElement 2
{
    my ($node, $name) = @_;
    
    while (($node = $node->getNextSibling) &&
        $node->getNodeName ne $name)
    {
        # do nothing
        ;
    }
    
    return $node;
}

#
#   $itemref - Reference to an array to hold the row contents
#   $rowNode - a table row
#
sub getRowContents 3
{
    my ($itemRef, $rowNode) = @_;
    my $cell;           # a cell node
    my $value;
    my $n_repeat;
    my $i;
    my $para;   # <text:p> node

    @{$itemRef} = ();
    $cell = getFirstChildElement( $rowNode, "table:table-cell" );
    while ($cell)
    {
        $n_repeat = $cell->getAttribute("table:number-columns-repeated");
        $n_repeat = 1 if (!$n_repeat);
        
        $value = "";
        $para = getFirstChildElement( $cell, "text:p" );
        while ($para)  4
        {
            $value .= $para->getFirstChild->getNodeValue . " ";
            $para = getNextSiblingElement( $para, "text:p" );
        }
        chop $value;
        
        for ($i=0; $i < $n_repeat; $i++)
        {
            push @{$itemRef}, $value;
        }
        $cell = getNextSiblingElement( $cell, "table:table-cell" );
    }
}
1 Because an XML file may have newlines and tabs between elements, the first child of an element may not necessarily be another element. That means that the DOM’s getFirstChild method might return a text node. Hence this utility routine that bypasses text nodes and gets the specific element node that we are interested in.
2 Similarly, the presence of newlines means we can’t use the getNextSibling method, but must use this utility to bypass text nodes and get to the element we are interested in.
3 Ths routine takes a <table:table-row> element and creates an array with all the row’s values. It expands repeated cells (where the table:number-columns-repeated attribute is present).
4 A table cell can contain multiple paragraphs; we concatenate them into one long string with blanks between each paragraph.

We start the main program by parsing the input file and emitting boilerplate for the styles.xml file, which is devoted to setting up the page dimensions.

print "Processing $ARGV[0]\n";

$doc = makeDOM( $ARGV[0] );

open STYLEFILE, ">$ARGV[1]/styles.xml";
print STYLEFILE <<"STYLEINFO";
<!DOCTYPE office:document-styles
    PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "office.dtd">
<office:document-styles xmlns:office="http://openoffice.org/2000/office"
    xmlns:style="http://openoffice.org/2000/style"
    xmlns:text="http://openoffice.org/2000/text"
    xmlns:table="http://openoffice.org/2000/table"
    xmlns:draw="http://openoffice.org/2000/drawing"
    xmlns:fo="http://www.w3.org/1999/XSL/Format"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:number="http://openoffice.org/2000/datastyle"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns:chart="http://openoffice.org/2000/chart"
    xmlns:dr3d="http://openoffice.org/2000/dr3d"
    xmlns:math="http://www.w3.org/1998/Math/MathML"
    xmlns:form="http://openoffice.org/2000/form"
    xmlns:script="http://openoffice.org/2000/script"
    office:version="1.0">
    <office:automatic-styles>
        <style:page-master style:name="pm1">
            <style:properties
              fo:page-width="21.59cm" fo:page-height="27.94cm" 
              style:num-format="1" style:print-orientation="portrait" 
              fo:margin-top="1.27cm" fo:margin-bottom="1.27cm"  
              fo:margin-left="1.27cm" fo:margin-right="1.27cm" 
              style:writing-mode="lr-tb" style:footnote-max-height="0cm">
                <style:columns fo:column-count="0" fo:column-gap="0cm"/>
            </style:properties>
        </style:page-master>
    </office:automatic-styles>
    <office:master-styles>
        <style:master-page style:name="Standard"
            style:page-master-name="pm1"/>
    </office:master-styles>
</office:document-styles>
STYLEINFO

close STYLEFILE;

Up until now, we haven’t had any objects inserted into our documents, so we haven’t had to create a manifest file. Now we have to do so, as that is how OpenOffice.org locates the chart. This is the boilerplate for the main directory files; as we create the charts, we will append elements to the manifest file.

#
#   Create the directory for the manifest file
#   and the header of the manifest file
#
mkdir ( "$ARGV[1]/META-INF", 0755 );
open MANIFESTFILE, ">$ARGV[1]/META-INF/manifest.xml";
print MANIFESTFILE <<"MANIFEST_HEADER";
<!DOCTYPE manifest:manifest
    PUBLIC "-//OpenOffice.org//DTD Manifest 1.0//EN" "Manifest.dtd">
<manifest:manifest xmlns:manifest="http://openoffice.org/2001/manifest">
    <manifest:file-entry
        manifest:media-type="application/vnd.sun.xml.writer"
        manifest:full-path="/"/>
    <manifest:file-entry
        manifest:media-type="text/xml" manifest:full-path="content.xml"/>
    <manifest:file-entry
        manifest:media-type="text/xml" manifest:full-path="styles.xml"/>
MANIFEST_HEADER

And now, the main event: the content.xml file. First, the boilerplate for the styles that we will need for the text and the chart itself:

#
#   Create the main content.xml file and its
#   header information
#
open CONTENTFILE, ">$ARGV[1]/content.xml";
print CONTENTFILE  <<"CONTENT_HEADER";
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE office:document-content
    PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN"
    "office.dtd">
<office:document-content
    xmlns:office="http://openoffice.org/2000/office" 
    xmlns:style="http://openoffice.org/2000/style" 
    xmlns:text="http://openoffice.org/2000/text" 
    xmlns:table="http://openoffice.org/2000/table" 
    xmlns:draw="http://openoffice.org/2000/drawing" 
    xmlns:fo="http://www.w3.org/1999/XSL/Format" 
    xmlns:xlink="http://www.w3.org/1999/xlink" 
    xmlns:number="http://openoffice.org/2000/datastyle" 
    xmlns:svg="http://www.w3.org/2000/svg" 
    xmlns:chart="http://openoffice.org/2000/chart" 
    xmlns:dr3d="http://openoffice.org/2000/dr3d" 
    xmlns:math="http://www.w3.org/1998/Math/MathML" 
    xmlns:form="http://openoffice.org/2000/form" 
    xmlns:script="http://openoffice.org/2000/script"
    office:class="text" 
    office:version="1.0">
    <office:script/>

    <office:automatic-styles>
        <!-- style for question title -->
        <style:style style:name="hdr1" style:family="paragraph">
            <style:properties
            fo:font-family="Bitstream Charter"
            style:font-family-generic="roman"
            style:font-pitch="variable"
            fo:font-size="14pt"
            fo:font-style="italic"/>
        </style:style>

        <!-- style for text summary of results -->
        <style:style style:name="info" style:family="paragraph">
            <style:properties
            fo:font-family="Bitstream Charter"
            style:font-family-generic="roman"
            style:font-pitch="variable"
            fo:font-size="10pt">
                <style:tab-stops> 1
                    <style:tab-stop style:position="3.5cm"
                        style:type="right"/>
                    <style:tab-stop style:position="5cm"
                        style:type="char"
                        style:char="."/>
                </style:tab-stops>
            </style:properties>
        </style:style>

        <!-- style to force a move to column two -->
        <style:style style:name="colBreak" style:family="paragraph">
            <style:properties fo:break-before="column"/>
        </style:style>

        <!-- set column widths -->
        <style:style style:name="Sect1" style:family="section">
            <style:properties 2
                text:dont-balance-text-columns="true">
                <style:columns fo:column-count="2">
                    <style:column style:rel-width="3968*"
                        fo:margin-left="0cm" fo:margin-right="0cm"/>
                    <style:column style:rel-width="7370*"
                        fo:margin-left="0cm" fo:margin-right="0cm"/>
                </style:columns>
            </style:properties>
        </style:style>

        <!-- style for chart frame -->
        <style:style style:name="fr1" style:family="graphics">
            <style:properties style:wrap="run-through"
                style:vertical-pos="middle"
                style:horizontal-pos="from-left"/>
        </style:style>
    </office:automatic-styles>

    <office:body>
CONTENT_HEADER
1 Rather than create a table for the summary of the results, we took the easy way out and set up tab stops to align the data properly.
2 We have two columns with text that is not automatically distributed to both columns. Because the columns have different relative widths, we do not have an fo:column-gap attribute in the <style:columns> element.

That finishes the static portion of the content file. We now grab all the rows, and, for each row in the table:

After processing all the rows, we close the remaining tags in the content.xml and manifest.xml files, and close them. This finishes the main program.

$rows = $doc->getElementsByTagName( "table:table-row" );
getRowContents( \@legends, $rows->item(0));

$n_rows = $rows->getLength;

for ($row=1; $row<$n_rows; $row++)
{
    getRowContents( \@data, $rows->item($row));
    
    next if (!$data[0]);  # skip rows without a question
    
    # calculate total number of responses
    
    $sum = 0;
    for ($col=1; $col < scalar(@data); $col++)
    {
        $sum += $data[$col];
    }
    
    print CONTENTFILE qq!<text:section text:style-name="Sect1"!;
    print CONTENTFILE qq! text:name="Section$row">!;
    print CONTENTFILE qq!<text:h text:style-name="hdr1" text:level="1">!;
    print CONTENTFILE qq!$row. $data[0]</text:h>\n!;
    
    for ($col=1; $col < scalar(@data); $col++)
    {
        $percent = sprintf(" (%.2f%%)", 100*$data[$col]/$sum);
        print CONTENTFILE qq!<text:p text:style-name="info">!;
        print CONTENTFILE qq!$legends[$col]<text:tab-stop/>$data[$col]!;
        print CONTENTFILE qq!<text:tab-stop/>$percent</text:p>\n!;
    }

    # now insert the reference to the graph
    
    print CONTENTFILE qq!<text:p text:style-name="colBreak">!;
    print CONTENTFILE qq!<draw:object draw:style-name="fr1"
        draw:name="Object$row" 
        svg:x="7cm" svg:width="8cm" svg:height="7cm"
        xlink:href="#./Object$row" xlink:type="simple"
        xlink:show="embed" xlink:actuate="onLoad"/></text:p>\n!;
    print CONTENTFILE qq!</text:section>\n!;
    
    # Create a directory for the chart
    
    mkdir ( "$ARGV[1]/Object$row", 0755 );
    
    construct_chart( \@legends, \@data, $row );
    
    append_manifest( $row );
}

print CONTENTFILE <<"CONTENT_FOOTER";
</office:body>
</office:document-content>
CONTENT_FOOTER

close CONTENTFILE;

print MANIFESTFILE "</manifest:manifest>\n";
close MANIFESTFILE;

Let’s handle the easy subroutine first—adding the path information to the manifest file. The append_manifest subroutine takes one parameter: the chart number.

#
#   Append data to the manifest file;
#   the parameter is the chart number
#
sub append_manifest
{
    my $number = shift;
    
    print MANIFESTFILE <<"ADD_MANIFEST";
<manifest:file-entry
    manifest:media-type="application/vnd.sun.xml.chart"
    manifest:full-path="Object$number/"/>
<manifest:file-entry
    manifest:media-type="text/xml"
    manifest:full-path="Object$number/content.xml"/>
<manifest:file-entry
    manifest:media-type="text/xml"
    manifest:full-path="Object$number/styles.xml"/>

ADD_MANIFEST
}

Finally, the subroutine to construct the chart. Again, we start with an immense amount of boilerplate, with styles for the chart title, legend, plot area, the data series, and the individual pie slices.

#
#   Construct the chart file, given:
#       reference to the @legends array
#       reference to the @data array
#       chart number
#
sub construct_chart
{
    my $legendref = shift;
    my $dataref = shift;
    my $chart_num = shift;
    
    my $cell;   # current cell number being processed
    
    open CHARTFILE, ">$ARGV[1]/Object$chart_num/content.xml";
    print CHARTFILE <<"CHART_HEADER";
<!DOCTYPE office:document-content
    PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "office.dtd">
<office:document-content xmlns:office="http://openoffice.org/2000/office"
    xmlns:style="http://openoffice.org/2000/style"
    xmlns:text="http://openoffice.org/2000/text"
    xmlns:table="http://openoffice.org/2000/table"
    xmlns:draw="http://openoffice.org/2000/drawing" 
    xmlns:fo="http://www.w3.org/1999/XSL/Format"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:number="http://openoffice.org/2000/datastyle"
    xmlns:svg="http://www.w3.org/2000/svg"
    xmlns:chart="http://openoffice.org/2000/chart"
    xmlns:dr3d="http://openoffice.org/2000/dr3d"
    xmlns:math="http://www.w3.org/1998/Math/MathML"
    xmlns:form="http://openoffice.org/2000/form"
    office:class="chart" office:version="1.0">
    <office:automatic-styles>
        <style:style style:name="title" style:family="chart">
            <style:properties
                style:font-family-generic="swiss"
                fo:font-size="12pt"/>
        </style:style>
        <style:style style:name="legend" style:family="chart">
            <style:properties
                style:font-family-generic="swiss"
                fo:font-size="8pt" />
        </style:style>
        <style:style style:name="plot" style:family="chart">
            <style:properties
                chart:lines="false"
                chart:series-source="columns"/>
        </style:style>
        <style:style style:name="series" style:family="chart">
            <style:properties draw:fill-color="#ffffff"/>
        </style:style>
        
        <style:style style:name="slice1" style:family="chart">
            <style:properties draw:fill-color="#ff6060"/>
        </style:style>
        <style:style style:name="slice2" style:family="chart">
            <style:properties draw:fill-color="#ffa560"/>
        </style:style>
        <style:style style:name="slice3" style:family="chart">
            <style:properties draw:fill-color="#ffff60"/>
        </style:style>
        <style:style style:name="slice4" style:family="chart">
            <style:properties draw:fill-color="#60ff60"/>
        </style:style>
        <style:style style:name="slice5" style:family="chart">
            <style:properties draw:fill-color="#6060ff"/>
        </style:style>
        <style:style style:name="slice6" style:family="chart">
            <style:properties draw:fill-color="#606080"/>
        </style:style>
    </office:automatic-styles>

The “here” document continues with the static part of the <office:body>, setting up the chart, title, legend, plot area, and table headings. There is only one series of data per chart, and each series has six data points. The first row of the table is a dummy header row, with the letter N (number of responses) as its content.

    <office:body>
        <chart:chart chart:class="circle" svg:width="9cm" svg:height="9cm">
        <chart:title chart:style-name="title" svg:x="1cm"
            svg:y="0.25cm">
            <text:p>${$dataref}[0]</text:p>
        </chart:title>
        <chart:legend chart:legend-position="right" svg:x="8cm" svg:y="3cm"
            chart:style-name="legend"/>
        
        <chart:plot-area svg:x="0.5cm" svg:y="1.5cm"
            svg:width="6cm" svg:height="6cm" chart:style-name="plot">
            <chart:axis
                chart:display-label="false"
                chart:class="value"
                chart:name="primary-y"/>
            <chart:series chart:style="series">
                <chart:data-point chart:style-name="slice1"/>
                <chart:data-point chart:style-name="slice2"/>
                <chart:data-point chart:style-name="slice3"/>
                <chart:data-point chart:style-name="slice4"/>
                <chart:data-point chart:style-name="slice5"/>
                <chart:data-point chart:style-name="slice6"/>
            </chart:series>
        </chart:plot-area>
        <table:table table:name="local-table">
            <table:table-header-columns>
                <table:table-column/>
            </table:table-header-columns>
            <table:table-columns>
                <table:table-column table:number-columns-repeated="2"/>
            </table:table-columns>
            
            <table:table-header-rows>
                <table:table-row>
                    <table:table-cell><text:p/></table:table-cell>
                    <table:table-cell table:value-type="string">
                        <text:p>N</text:p>
                    </table:table-cell>
                </table:table-row>
            </table:table-header-rows>
            <table:table-rows>
CHART_HEADER

Now we create the dynamic portion of the table contents; each category (Strongly Agree/Agree/etc.) is in the first column, and the number of responses in the second column. The subroutine finishes by closing off all the open tags.

    for ($cell=1; $cell < scalar(@{$dataref}); $cell++)
    {
        print CHARTFILE qq!<table:table-row>\n!;
        print CHARTFILE qq!<table:table-cell table:value-type="string">!;
        print CHARTFILE qq!<text:p>!, ${$legendref}[$cell], qq!</text:p>!;
        print CHARTFILE qq!</table:table-cell>!;
        print CHARTFILE qq!<table:table-cell table:value-type="float" !;
        print CHARTFILE qq!table:value="!, ${$dataref}[$cell], qq!">!;
        print CHARTFILE qq!<text:p>!, ${$dataref}[$cell], qq!</text:p>!;
        print CHARTFILE qq!</table:table-cell></table:table-row>\n!;
    }
    print CHARTFILE <<"CHART_FOOTER";
</table:table-rows>
</table:table>
</chart:chart>
</office:body>
</office:document-content>
CHART_FOOTER
}


[15] This survey uses what is called a six-point Likert scale. If you are setting up a survey, always make sure you have an even number of choices. If you have an odd number of choices with “Neutral” in the middle, people will head for the center like moths to a flame. Using an even number of choices forces respondents to make a decision.

[16] As modern art, this is actually quite nice. The results for a pie chart look almost obscene.


Creative Commons License Content licensed under a Creative Commons License.
All content is copyright O’Reilly & Associates, Inc.
During development, I give permission for non-commercial copying for educational and review purposes. After publication, all text will be released under the Free Software Foundation’s GNU Free Documentation License.