[XSL-LIST Mailing List Archive Home] [By Thread] [By Date]

Re: [xsl] Transform XML to HTML table with multiple columns and lines.


Subject: Re: [xsl] Transform XML to HTML table with multiple columns and lines.
From: Jeni Tennison <jeni@xxxxxxxxxxxxxxxx>
Date: Tue, 6 Nov 2001 19:25:09 +0000

Hi Johan,

> I use XP/SAX and Java to produce XML and XT to transform it to HTML.

Damn. I was going to launch into a usual grouping-problem spiel, but
since you use XT you can't use keys! My first recommendation is
switching to a conformant XSLT processor (such as MSXML, Saxon or
Xalan) - you'll probably find this kind of transformation a lot faster
if you do.

Anyway, this is a twist on a grouping problem. The first bit is easy:
you need to create a table element for the Sales element, and a row
for the heading which contains a static header for the Time column:

<xsl:template match="Sales">
  <table border="1">
    <tr>
      <th>Time</th>
      ...
    </tr>
    ...
  </table>
</xsl:template>

Your first problem is how to generate the rest of the headers. You
need to create a th element for each unique value of the Salesperson
elements in the XML. Usually I'd recommend you find it using a key,
but since you can't use keys, you can look at each Occasion and see,
using the preceding-sibling:: axis, if there's already been an
Occasion in the document that has involved the same Salesperson. If
there hasn't been, then this is the first Occasion in the document,
with a particular Salesperson, so you can use that Salesperson as the
value of the header:

<xsl:template match="Sales">
  <xsl:variable name="salespeople"
    select="Occasion[not(preceding-sibling::Occasion/Salesperson =
                         Salesperson)]/Salesperson" />
  <table border="1">
    <tr>
      <th>Time</th>
      <xsl:for-each select="$salespeople">
        <th><xsl:value-of select="." /></th>
      </xsl:for-each>
    </tr>
    ...
  </table>
</xsl:template>

Now for the rest of the table. You want one row for each unique Time
in the document. You can use the same kind of construction to get that
as well:

<xsl:template match="Sales">
  <xsl:variable name="salespeople"
    select="Occasion[not(preceding-sibling::Occasion/Salesperson =
                         Salesperson)]/Salesperson" />
  <table border="1">
    <tr>
      <th>Time</th>
      <xsl:for-each select="$salespeople">
        <th><xsl:value-of select="." /></th>
      </xsl:for-each>
    </tr>
    <xsl:for-each
        select="Occasion[not(preceding-sibling::Occasion/Time =
                             Time)]/Time">
      <xsl:sort select="." />
      <xsl:variable name="time" select="." />
      <tr>
        <th><xsl:value-of select="$time" /></th>
        ...
      </tr>
    </xsl:for-each>
  </table>
</xsl:template>

For the cells, you need to cycle through the sales people again, and
create a cell for each:

<xsl:template match="Sales">
  <xsl:variable name="salespeople"
    select="Occasion[not(preceding-sibling::Occasion/Salesperson =
                         Salesperson)]/Salesperson" />
  <table border="1">
    <tr>
      <th>Time</th>
      <xsl:for-each select="$salespeople">
        <th><xsl:value-of select="." /></th>
      </xsl:for-each>
    </tr>
    <xsl:for-each
        select="Occasion[not(preceding-sibling::Occasion/Time =
                             Time)]/Time">
      <xsl:sort select="." />
      <xsl:variable name="time" select="." />
      <tr>
        <th><xsl:value-of select="$time" /></th>
        <xsl:for-each select="$salespeople">
          <td>
            <xsl:variable name="salesperson" select="." />
            ...
          </td>
        </xsl:for-each>
      </tr>
    </xsl:for-each>
  </table>
</xsl:template>

The value of a cell is the Amount of the Occasion within the document
that has the Time from the outer loop (which is in the $time variable)
and the sales person from the inner loop (which is in the $salesperson
variable). Add a variable definition at the top level of the template
so that you can easily get to all the occasions, and then find the one
with the correct sales person and time, as follows:

<xsl:template match="Sales">
  <xsl:variable name="occasions" select="Occasion" />
  <xsl:variable name="salespeople"
    select="Occasion[not(preceding-sibling::Occasion/Salesperson =
                         Salesperson)]/Salesperson" />
  <table border="1">
    <tr>
      <th>Time</th>
      <xsl:for-each select="$salespeople">
        <th><xsl:value-of select="." /></th>
      </xsl:for-each>
    </tr>
    <xsl:for-each
        select="Occasion[not(preceding-sibling::Occasion/Time =
                             Time)]/Time">
      <xsl:sort select="." />
      <xsl:variable name="time" select="." />
      <tr>
        <th><xsl:value-of select="$time" /></th>
        <xsl:for-each select="$salespeople">
          <td>
            <xsl:variable name="salesperson" select="." />
            <xsl:value-of
              select="$occasions[Salesperson = $salesperson and
                                 Time = $time]/Amount" />
          </td>
        </xsl:for-each>
      </tr>
    </xsl:for-each>
  </table>
</xsl:template>

You will probably find it a lot more efficient using keys; if you
swap processors, have a look at
http://www.jenitennison.com/xslt/grouping/muenchian.html for the
equivalent of the path for selecting unique nodes. You should also use
a key to retrieve the occasion from a given sales person and time,
with a key declaration like:

<xsl:key name="occasionsBySalespersonAndTime"
         match="Occasion"
         use="concat(Salesperson, '::', Time)" />

and retrieving the Occasion with:

  key('occasionsBySalespersonAndTime',
      concat($salesperson, '::', $time))

I hope that helps,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list



Current Thread
Keywords