Eight Squared Software

Open Data Visualization Markup Language (ODVML)

This document describes the goals and usage of ODVML, a proposed markup language for describing scalable data visualizations via a text-based, human-editable and machine-readable format.

Introduction

We live in an increasingly data-driven world. This is partly because we now have more ways than ever to take and collect measurements on an enormous scale, and partly because the modern era has been marked by the application of the scientific method to an increasingly wider spectrum of our lives. With such tremendous quantities of raw data available, visualization is the first and often the most practical way to locate patterns, make predictions about the future, and generally make sense of it all.

Fortunately, the same technological lever we pull to augment our data collection abilities has also extended our ability to visualize that data far beyond the age of William Playfair and his hand-drawn charts. Thanks to the work of Edward Tufte, Stephen Few, and other giants of modern data visualization, we have a library of battle-tested techniques for extracting useful information from piles of numbers.

The modern programmer is also fortunate that the explosion of software in general and open source in particular means that code to produce such visualizations is readily available in most languages. Nearly all such libraries can readily produce the basic column, line, and area charts that make up the lion's share of the average person's chart diet.Some libraries, such as the venerable D3.js, grant one near-magical powers for exploring and interacting with large data sets. It would therefore seem that data visualization is a solved problem. This, however, is not so.

The problem

Having a wide array of pre-written, well-tested code available is both a blessing and a curse. On the one hand, libraries undeniably save programmer time, and thereby save money. On the other hand, using a library often ties the calling code not only to a particular language, but also to that library's API. Swapping out the underlying visualization library requires careful abstraction on the part of the programmer to provide consistent integration points that hide differences in the libraries' API surfaces. Porting even a simple visualization to another platform is nigh impossible without a rewrite. One cannot simply take charting code written in JavaScript on top of D3.js and target OpenGL on iOS or Direct2D on Windows without tremendous effort. Targeting a cross-platform language can alleviate this problem, but ideally we prefer to retain freedom of language choice.

Setting aside the problem of language portability, we find a second problem with modern data visualization: the output formats are not portable. At some point the shapes that make up the visualization must be rasterized for display, and it is at this point that the underlying data is irrevocably cut off from its visual representation. Certainly, one can capture the visualization as a raster image and do a number of things with it. But one cannot reliably and accurately recover the underlying data from a string of pixels, making it effectively impossible to do such simple things as change a column chart to a line chart or combine the data with another series to produce a comparison chart. The separation of source and output prevents the free exchange and remixing of data for which the Web was invented.

Why not SVG?

On its surface, Scalable Vector Graphics appears to be an ideal format for describing data visualizations in a format that is not only human-editable but also device-independent. SVG supports every shape one might need for a two-dimensional chart while also providing for potentially rich interaction through embedded scripting. However, certain foundational characteristics of SVG make it unsuitable for solving the problem of truly portable visualizations.

First, SVG has weak support for arbitrary coordinate spaces. Raw data is hardly ever expressed in pixels, yet SVG expects shapes to be defined in a coordinate space that is some set of transformation functions away from the native display space. This means that one cannot plot even something as simple as a line that expresses changes in temperature over time without resorting to an intermediary layer to convert the raw data to display coordinates. Such a transformation makes it impossible to recover the original data, as it throws away any semblance of semantics.

Second, SVG does not allow for non-uniform scaling. A typical scatter plot will have two axes that take up a certain amount of space based on the axis labeling, with the remainder of the image given to the plot itself. SVG provides no way for scaling to be restricted to the plot area. Instead, all elements are scaled proportionally. This results in axis labels defined to match the body text of a surrounding HTML document becoming either too small to read when the plot is scaled down to fit on a smartphone screen or excessively large when scaled up to fit on a widescreen desktop display.

Finally, SVG does not support the combining of units. On one level, this means that a shape's stroke width cannot be defined in terms of absolute pixels while the the shape's bounds are defined in terms of some other unit, regardless of the active coordinate space transformation. On a deeper level, it means that defining a shape's coordinate or dimension as the result of a mathematical equation involving multiple units is impossible.CSS provides something like this via its calc() function. At first this may not seem like much, but first-class support for arbitrary arithmetic expressions opens up a range of features previously unthinkable.

The above problems do not mean that SVG is a poor format. On the contrary, SVG is very good at what it was designed to do: describe a variety of shapes and effects previously achievable only through raster graphics in a manner that is infinitely scalable in either direction. But for the specialized needs of data visualization, SVG often works better as a final output format rather than a distribution method.

Proposed solution

If standardization on a programming language is neither possible nor desirable, the next best option is to standardize on the protocol.Databases have known this for decades, which is why one's choice of language rarely precludes the use of a popular database technology such as PostgreSQL or Redis. A stable and well-documented protocol provides fertile ground for client libraries to flourish in a number of languages, which allows programmers to focus on solving the problem at hand without worrying about the intricacies of parsing or of shuffling command and response packets from client to server and back again.

In line with this strategy, this document proposes and describes a markup language, ODVML, for describing vector graphics. When paired with a lightweight rasterizer (which need be written only once per language), ODVML provides a human-editable and machine-readable format capable of producing consistent images across languages and platforms. Under this paradigm, the calling code is responsible only for producing text output that can be interpreted by the rasterizer in a black-box manner. This simplifies testing and opens up use cases such as generating ODVML on the server and rendering it in multiple clients with scaling appropriate to the device's available resolution.

Like SVG, ODVML is an XML-based vector graphics format. Unlike SVG, however, ODVML places special emphasis on features needed to describe the most common types of visualizations and is optimized for describing graphics as a transformation of raw data into two-dimensional shapes using arbitrary coordinate spaces. ODVML also differs from SVG in that it targets only the most basic shapes and eschews complex effects such as arbitrary transformations, filters, scripting, and animation. This decision is based on the fact that ODVML is not intended to replace SVG, and the fact that restricting the API surface simplifies the development of the client libraries in multiple languages. Ideally, writing an ODVML-to-screen rasterizer should not be a difficult task in any language that already has a general-purpose raster graphics library, as most modern languages do.

The data visualization space today covers a wide variety of chart types. If used purely as a vector graphics language, ODVML could potentially be used to describe complex visualizations such as flow diagrams and chloropleth maps. In its current form, however, ODVML is tailored toward representing data that is naturally expressed as a series of points that plot correlations between two axes. This encompasses column charts, line charts, and scatter plots, and potentially pie charts or other proportional representations. These chart types cover the most common and well-understood data visualizations in use today.

Roadmap

ODVML is currently in a beta stage of development. Prior to the 1.0 release, feedback is welcomed at the official GitLab page for this documentation.

During this stage of development, efforts will be made to ensure that change to the language are made in a backward-compatible manner. However, no such guarantee is made.

Along the way to establishing the feature set that will comprise version 1.0 of the specification, ODVML's cross-platform capabilities should be demonstrated by producing rasterizer implementations in at least three modern languages.The examples on this page use a JavaScript rasterizer, which tentatively serves as the reference implementation.

Following the 1.0 release, all changes in the 1.x line must retain backward compatibility with the 1.0 specification. The definition of backward compatibility in this context is yet to be determined. For example, the addition of a new property to an existing element that is ignored by an older rasterizer may alter the final image, but not in a such a way that the image is rendered unusual. Given the impact of the change, this may be considered backward-compatible.

Currently, rendering an ODVML image in the browser requires the use of JavaScript. It is anticipated that ODVML will eventually reach a maturity stage that permits a distinct MIME type and the inclusion of native rasterizers in browsers and operating systems. This would allow all users to view, download, and share ODVML files as easily as SVG and PNG images.

Contributing

Contributions to ODVML are welcome and vital for the long-term success of the format. You can help in the following ways:

  • Proposing and discussing new features. Proposals for new features are currently tracked as individual issues at the official GitLab page for this documentation. Ideally, a feature proposal should contain a detailed description of one or more real-world use cases in which the new feature improves on existing functionality or enables something that was previously impossible.
  • Contributing a new rasterizer implementation in your favorite language. In order to gain a wide user base, ODVML needs to have high-quality rasterizers in as many major languages as possible. Java, C#, and Python are excellent candidates for this task.
  • Experimenting with ODVML in your workplace or hobby projects, and contributing feedback or bug reports in the form of GitLab issues or writing blog posts about your experiences.

Document structure

As mentioned above, all ODVML documents are XML documents.The rationale behind the decision to use XML is covered in the Appendix under the heading Why XML?. The root <odv> element of an ODVML document may contain shapes, data series, and units in any order. Some elements, such as loops and conditionals, may enclose other visible elements, forming a tree that describes the object scene.

As a general rule, objects in ODVML are drawn in depth-first order. That is, objects are drawn in the order they are encountered, with an object's children being completely drawn before moving to that object's following sibling.This rule does not apply to non-visible elements such as series and units, which are handled specially.

This section covers elements and other values that are found in multiple parts of an ODVML document and are thus better examined separately.

Expressions

Just about any attribute value that expects a numeric value also accepts an expression: a mathematical formula that is evaluated at render time to produce a value.

Expressions in ODVML are written in Reverse Polish Notation, also known as postfix notation.Although less common than infix notation, RPN avoids swarms of parentheses to define operator precedence and is easy for computers to efficiently parse. A valid expression consists of a list of numbers and/or operators delimited by spaces.

Under the hood, each expression starts with an empty stack of numbers. The renderer reads the expression from left to right. When it encounters a number, it “pushes” it onto the top of the stack. When it encounters a non-number, it attempts to look up an operator with the same name and modifies the stack according to the operator's definition. After reaching the end of the expression, the number at the top of the stack is returned.The top value of an empty stack is considered to be zero.

Expression Result Note
4 2 + 6 + adds the top two numbers
4 2 - 2 - subtracts the top number from the next number
4 2 * 8 * multiplies the top two numbers
4 2 / 2 / divides the next-topmost number by the top number
6 4 2 + * 36 4 and 2 are first added to produce 6, which is then multiplied by the 6 at the beginning of the expression.
Various examples of basic expressions.

Operations such as dividing by zero, which yields in an unrepresentable number, result in the special value NaN (Not a Number) being pushed onto the stack. As a general rule, if an expression results in the value NaN, the shape or other object that depends on it will be skipped by the renderer.

Built-in units

ODVML provides the following units for use in expressions.In this documentation, a unit is distinguished from an operator in that a unit works with only the topmost value on the stack, while an operator may work with multiple values up to the entire stack. Internally, there may be no such distinction inside the renderer. Unlike units in SVG or CSS, units in ODVML must be separated from the preceding number by a space. 42 px is a valid expression, but 42px is not.

Unit Effect
deg Converts a number expressed in degrees to a number of turns, where 360 degrees equals one turn.
grad Converts a number expressed in gradians to a number of turns, where 400 gradians equals one turn.
pctmax Converts a number representing a percentage of the longest output dimension to pixels.
pctmin Converts a number representing a percentage of the shortest output dimension to pixels.
pctx Converts a number representing a percentage of the output width to pixels.
pcty Converts a number representing a percentage of the output height to pixels.
px Converts a number to pixels. Usually this is a no-op, but may be specified for clarity.
rad Converts a number expressed in radians to a number of turns, where 2π radians equals one turn.
List of built-in units.

In addition to the above units, ODVML permits the definition of custom units based on linear scales in either the X or Y direction. This is covered below in the section on units.

Built-in operators

ODVML provides the following operators for use in expressions.

Operator Effect
avg Replaces all numbers in the stack with their average.
ceil Replaces the top number in the stack with the smallest integer greater than or equal to the number.
cos Replaces the top number in the stack with its cosine. (The number should be expressed in turns. Use the one of the angle units such as deg or rad to convert an angle to turns.)
floor Replaces the top number in the stack with the largest integer less than or equal to the number.
log Replaces the top two numbers in the stack with the logarithm of the next-topmost number, where the top number is the base. If either number is less than or equal to zero, this operator pushes NaN onto the stack.
max Replaces all numbers in the stack with the greatest number. If the stack has no finite numbers, this operator pushes NaN onto the stack.
min Replaces all numbers in the stack with the smallest number. If the stack has no finite numbers, this operator pushes NaN onto the stack.
sin Replaces the top number in the stack with its sine. (The number should be expressed in turns. Use the one of the angle units such as deg or rad to convert an angle to turns.)
tan Replaces the top number in the stack with its tangent. (The number should be expressed in turns. Use the one of the angle units such as deg or rad to convert an angle to turns.)
sum Replaces all numbers in the stack with their sum.
pow Replaces the top two numbers in the stack with the next-topmost raised to the power of the top number.
List of built-in operators.

Paint

Elements that describe shapes (which includes lines, rectangles, arcs, paths, and labels) may contain a <paint> element that describes how the object is to be rendered.A shape lacking a <paint> element is considered to be invisible.

A <paint> element may contain any number of <fill> and <stroke> elements.A <paint> element may also contain an <if> element with clauses that contain <fill> or <stroke> elements. In the event a <paint> element contains more than one child element, the styles are applied in depth-first order according to the usual rule. That is, if a <paint> element contains a <stroke> element followed by a <fill> element, the shape is first stroked according the content of the <stroke> element and then filled according the content of the <fill> element.

It is permissible for a <paint> element to contain multiple <fill> elements or multiple <stroke> elements.

The root element may contain a <paint> element that describes the style of a rectangle that covers the entire image.This is a shortcut that could be replicated by drawing an actual rectangle that completely covers the image. By default, the output image's background is transparent. Including a <paint> element directly under the root element enables changing the image's background color or even adding a border.

  • Source
    <odv>
      <paint>
        <fill>
          <color r="1" g="1" b="1"/>
        </fill>
        <stroke width="4">
          <color b="1"/>
        </stroke>
      </paint>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
Using a <paint> element under the root element to define both a background color and an image border.

Note that the although the stroke width is defined as four pixels, the visible stroke width is two pixels, as the outer half of the stroke lies beyond the image boundaries.

Colors

Both the <fill> and <stroke> stroke elements may contain one or more <color> elements.If a <fill> or <stroke> element contains no <color> elements, it is treated as though it contained exactly one <color> element set to fully-opaque black. If it contains multiple <color> elements, it describes a gradient, which is covered in a later section.

The <color> element identifies a color in either the RGB or HSL color space. The rule for determining which color space is used are as follows:

  • If a value is provided for one of the r (red), g (green), or b (blue) attributes, the RGB color space is assumed.
  • Otherwise, if a value is provided for one of the h (hue), s (saturation), or l (lightness) attributes, the HSL color space is assumed.
Attribute Default Value
r 0 The value of the red component of an RGB color, in the range 0 to 1.
g 0 The value of the green component of an RGB color, in the range 0 to 1.
b 0 The value of the blue component of an RGB color, in the range 0 to 1.
h 0 The value of the hue component of an HSL color, in the range 0 to 1.
s 0 The value of the saturation component of an HSL color, in the range 0 to 1.
l 0 The value of the lightness component of an HSL color, in the range 0 to 1.
a 1 The value of the alpha component of either an RGB or an HSL color, in the range 0 to 1.
Attributes supported by the <color> element.

As noted in the table above, in ODVML, each color component (whether RGB or HSL) is defined on a scale from 0.0 to 1.0.This is in contrast to traditional markup languages, in which the range depends on the color space; e.g. red, green, and blue range from 0 to 255. Expressions permit conversion between alternate ranges. For example, division by 255 can be used to convert from a traditional red, green, or blue value to the range expected by ODVML. Similarly, the deg operator can be used for the hue component to convert from degrees to turns.

  • Source
    <odv>
      <paint>
        <fill>
          <!-- rgb(76, 175, 80) -->
          <color r="76 255 /" g="175 255 /" b="80 255 /"/>
        </fill>
        <stroke width="20">
          <!-- hsl(14deg, 87%, 56.7%) -->
          <color h="14 deg" s="0.87" l="0.567"/>
        </stroke>
      </paint>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
Illustration of the RGB and HSL color spaces.

Fills

The <fill> element describes the color (or gradient) applied to the interior area of a shape.

The <fill> element has no attributes aside from those common to it and the <stroke> element for constructing gradients.A future version may include support for defining the fill rule.

Strokes

The <stroke> element describes the color (or gradient) applied to the outline of a shape.

The <stroke> element accepts several attributes that determine the stroke style:

Attribute Default Value
width 1 The stroke width in pixels. The stroke extends for half the value of width on either side of the shape's infinitely-thin outline.
cap butt How the ends of a non-closed path are capped. Allowed values: round, square, or butt.
join miter How consecutive segments of a path are joined. Allowed values: round, bevel, or miter.
dash (empty) A comma-separated list of pixel lengths that determines the lengths of dashes and interleaved spaces. If not provided, the stroke is drawn without dashes.
Attributes supported by the <stroke> element.

The cap, join, and dash attributes follow the definition of SVG's stroke-linecap, stroke-linejoin, and stroke-dasharray attributes as seen in the following example.

  • Source
    <odv>
      <path>
        <paint><stroke cap="round" join="round" width="30"/></paint>
        <move x="20" y="75"/>
        <line x="100" y="25"/>
        <line x="200" y="75"/>
        <line x="280" y="25"/>
      </path>
      <path>
        <paint><stroke cap="square" join="bevel" width="30"/></paint>
        <move x="20" y="125"/>
        <line x="100" y="75"/>
        <line x="200" y="125"/>
        <line x="280" y="75"/>
      </path>
      <path>
        <paint><stroke cap="butt" join="miter" width="30"/></paint>
        <move x="20" y="175"/>
        <line x="100" y="125"/>
        <line x="200" y="175"/>
        <line x="280" y="125"/>
      </path>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
Illustration of stroke cap and join styles.

Gradients

In other markup languages, defining a gradient tends to involve specialized or cumbersome syntax. ODVML tries to limit the use of specialized syntax for one-off use cases, and so supports the definition of linear gradients in the following way:Support for radial gradients may be added in a future version provided there is sufficient real-world need for such a feature.

  • If a <fill> or <stroke> element has one or no <color> elements, it describes a solid color.
  • Otherwise, it describes a gradient where each <color> element marks a stop along the gradient line.

Since a line is needed to define the direction and extent of the gradient, both the <fill> and <stroke> elements support x1, y1, x2, and y2 attributes to define a line segment along which the gradient travels.The coordinates are always interpreted in terms of the image's boundary, and never the shape's boundary.

Additionally, the <color> element supports an offset attribute for determining that color's relative position along the gradient line. As with the other attributes on the <color> element, the offset attribute should be between 0 and 1.0 and optionally accepts an expression.

Beyond the bounds of the line segment, the color at the end of the gradient is continued infinitely.In SVG, this is equivalent to spreadMethod="pad".

  • Source
    <odv>
      <paint>
        <fill x1="25 pctx" y1="25 pcty" x2="75 pctx" y2="75 pcty">
          <color h="120 deg" s="1" l="0.75" offset="0" />
          <color h="180 deg" s="1" l="0.75" offset="0.5" />
          <color h="240 deg" s="1" l="0.75" offset="1" />
        </fill>
      </paint>
      <line x1="25 pctx" y1="25 pcty" x2="75 pctx" y2="75 pcty">
        <paint>
          <stroke>
            <color r="1" a="0.5"/>
          </stroke>
        </paint>
      </line>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
Demonstration of a linear gradient. The line that defines the gradient's extent is superimposed on the gradient as a reference.

Shapes

ODVML offers a fairly standard set of basic shapes for composing images. As mentioned above, each of the below shapes (except for clipping rectangles) can be styled by including a <paint> element inside the shape element.

Lines

The <line> element plots a straight line between two points and accepts the following attributes.Although called a line, a more accurate term would be segment, as the line does not extend beyond the terminating points. A future version of ODVML may include support for drawing rays and true lines that extend into infinity.

Attribute Default Value
x1 0 The X coordinate of the first point that defines the line.
y1 0 The Y coordinate of the first point that defines the line.
x2 0 The X coordinate of the second point that defines the line.
y2 0 The Y coordinate of the second point that defines the line.
Attributes supported by the <line> element.
  • Source
    <odv>
      <line x1="50" y1="25" x2="50" y2="175">
        <paint><stroke width="10"/></paint>
      </line>
      <line x1="100" y1="175" x2="200" y2="25">
        <paint><stroke width="10"/></paint>
      </line>
      <line x1="250" y1="25" x2="250" y2="175">
        <paint><stroke width="10"/></paint>
      </line>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
The <line> element.

Rectangles

The <rect> element accepts the same coordinate attributes as the <line> element and draws a rectangle with the given points as opposite corners.

Attribute Default Value
x1 0 The X coordinate of the first corner that defines the rectangle.
y1 0 The Y coordinate of the first corner that defines the rectangle.
x2 0 The X coordinate of the second corner that defines the rectangle.
y2 0 The Y coordinate of the second corner that defines the rectangle.
Attributes supported by the <rect> element.
  • Source
    <odv>
      <rect x1="0" y1="0" x2="50 pctx" y2="50 pcty">
        <paint><fill><color r="1" a="0.75"/></fill></paint>
      </rect>
      <rect x1="50 pctx" y1="0" x2="100 pctx" y2="50 pcty">
        <paint><fill><color g="1" a="0.75"/></fill></paint>
      </rect>
      <rect x1="0" y1="50 pcty" x2="50 pctx" y2="100 pcty">
        <paint><fill><color b="1" a="0.75"/></fill></paint>
      </rect>
      <rect x1="50 pctx" y1="50 pcty" x2="100 pctx" y2="100 pcty">
        <paint><fill><color r="1" g="1" a="0.75"/></fill></paint>
      </rect>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
The <rect> element.

Arcs

The <arc> element provides a way to draw circles or sections of circles. Although it lacks support for elliptical arcs, the <arc> element is perfect for pie charts and other proportional representations by way of the a1 (starting angle) and a2 (ending angle) attributes.As is customary with pie charts, the zero angle points toward the top edge of the image. The optional r2 attribute simplifies the creation of “donut” charts.

Attribute Default Value
x 0 The X coordinate of the arc's center.
y 0 The Y coordinate of the arc's center.
r1 0 The arc's primary radius.
r2 0 The arc's secondary radius. Providing a positive value produces a donut or ring shape. r2 may be greater than r1.
a1 0 The starting angle (expressed in turns), where zero points straight up to the top edge of the image.
a2 0 The ending angle (expressed in turns). The arc is always drawn clockwise from a1 to a2.
Attributes supported by the <arc> element.
  • Source
    <odv>
      <arc x="75" y="100" r1="50" a1="0" a2="140 deg">
        <paint><fill><color r="1" a="0.75"/></fill></paint>
      </arc>
      <arc x="75" y="100" r1="50" a1="140 deg" a2="240 deg">
        <paint><fill><color g="1" a="0.75"/></fill></paint>
      </arc>
      <arc x="75" y="100" r1="50" a1="240 deg" a2="320 deg">
        <paint><fill><color b="1" a="0.75"/></fill></paint>
      </arc>
      <arc x="75" y="100" r1="50" a1="320 deg" a2="360 deg">
        <paint><fill><color r="1" g="1" a="0.75"/></fill></paint>
      </arc>
      <arc x="225" y="100" r1="50" r2="30" a1="0" a2="140 deg">
        <paint>
          <fill><color r="1" a="0.75"/></fill>
          <stroke width="2"><color r="1" g="1" b="1"/></stroke>
        </paint>
      </arc>
      <arc x="225" y="100" r1="50" r2="30" a1="140 deg" a2="240 deg">
        <paint>
          <fill><color g="1" a="0.75"/></fill>
          <stroke width="2"><color r="1" g="1" b="1"/></stroke>
        </paint>
      </arc>
      <arc x="225" y="100" r1="50" r2="30" a1="240 deg" a2="320 deg">
        <paint>
          <fill><color b="1" a="0.75"/></fill>
          <stroke width="2"><color r="1" g="1" b="1"/></stroke>
        </paint>
      </arc>
      <arc x="225" y="100" r1="50" r2="30" a1="320 deg" a2="360 deg">
        <paint>
          <fill><color r="1" g="1" a="0.75"/></fill>
          <stroke width="2"><color r="1" g="1" b="1"/></stroke>
        </paint>
      </arc>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
The <arc> element.

Paths

The <path> element permits the construction of arbitrarily complex paths composed of straight line segments and quadratic or cubic Bezier curves. The <path> element itself accepts no attributes.

Unlike SVG, which fits the entire path definition into the <path> element's d attribute, ODVML uses separate <move>, <line>, <quad>, <cube>, and <close> child elements to define the path. Although more verbose than SVG's terse path definition syntax, using full elements for each path command allows paths to make use of loops and conditionals, covered in a later section.

The <move> element starts a new subpath. The current subpath (if any) is capped using the stroke's cap style (if defined) and the “pen” is moved to the specified point.

Attribute Default Value
x 0 The X coordinate at which to start the new subpath.
y 0 The Y coordinate at which to start the new subpath.
Attributes supported by the <move> element.

The <line> element extends the current subpath by drawing a straight line segment from the current point to the specified point.

Attribute Default Value
x 0 The X coordinate to which to draw the line segment.
y 0 The Y coordinate to which to draw the line subpath.
Attributes supported by the <line> element.

The <quad> element extends the current subpath by drawing a quadratic Bezier curve from the current point to the specified point.

Attribute Default Value
x 0 The X coordinate to which to draw the curve.
y 0 The Y coordinate to which to draw the curve.
cx 0 The X coordinate of the curve's control point.
cy 0 The Y coordinate of the curve's control point.
Attributes supported by the <quad> element.

The <cube> element extends the current subpath by drawing a cubic Bezier curve from the current point to the specified point.

Attribute Default Value
x 0 The X coordinate to which to draw the curve.
y 0 The Y coordinate to which to draw the curve.
c1x 0 The X coordinate of the curve's first control point.
c1y 0 The Y coordinate of the curve's first control point.
c2x 0 The X coordinate of the curve's second control point.
c2y 0 The Y coordinate of the curve's second control point.
Attributes supported by the <cube> element.

The <close> element closes the current subpath by drawing a straight line segment from the current point to the subpath's starting point. Closing a subpath differs subtly from simply using the <line> element to draw a line back to the starting point when a path is painted with a stroke. In the former case, closing the subpath results in the path's stroke join style being applied to the first and last segments in the subpath. In the latter case, the subpath is left open, and the subpath ends are capped, not joined, according to the path's stroke cap style.

The <close> element has no attributes.

  • Source
    <odv>
      <path>
        <paint>
          <stroke width="2"><color b="1"/></stroke>
          <fill><color b="1" a="0.1"/></fill>
        </paint>
        <move x="25" y="25"/>
        <line x="275" y="25"/>
        <quad x="275" y="125" cx="200" cy="75"/>
        <cube x="50" y="125" c1x="150" c1y="175" c2x="100" c2y="75"/>
        <close/>
      </path>
      <!-- Control points -->
      <arc x="200" y="75" r1="3">
        <paint><fill><color r="1"/></fill></paint>
      </arc>
      <arc x="150" y="175" r1="3">
        <paint><fill><color r="1"/></fill></paint>
      </arc>
      <arc x="100" y="75" r1="3">
        <paint><fill><color r="1"/></fill></paint>
      </arc>
      <!-- Lines to control points -->
      <line x1="275" y1="25" x2="200" y2="75">
        <paint><stroke><color r="1"/></stroke></paint>
      </line>
      <line x1="275" y1="125" x2="200" y2="75">
        <paint><stroke><color r="1"/></stroke></paint>
      </line>
      <line x1="275" y1="125" x2="150" y2="175">
        <paint><stroke><color r="1"/></stroke></paint>
      </line>
      <line x1="150" y1="175" x2="100" y2="75">
        <paint><stroke><color r="1"/></stroke></paint>
      </line>
      <line x1="100" y1="75" x2="50" y2="125">
        <paint><stroke><color r="1"/></stroke></paint>
      </line>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
The <path> element and its supported path commands. Lines and circles illustrating curve control points superimposed for clarity.

Labels

The <label> elements provides a way to render short strings of text on the image. Since ODVML is not intended for rich text compositions, no support is provided for wrapping or selective styling of substrings. The intended use case for the <label> element is axis and chart labels and possibly point labels, which are typically drawn in a consistent style on a single line.

Since rotation of labels is sometimes necessary (such as for a vertical axis label), the <label> element is the only element that supports arbitrary rotation about its anchor point.

Attribute Default Value
expr (empty) The expression to evaluate. If given, the value of this expression is used as the label text. If not given, the value of the text attribute is used.
text (empty) The text to draw. This value may contain references to variables, as covered in the section on loops.
x 0 The X coordinate at which to anchor the text.
y 0 The Y coordinate at which to anchor the text.
align center How the text is positioned relative to the anchor point. If left, the text extends to the right of the anchor point. If right, the text extends to the left of the anchor point. Otherwise, the text is centered on the anchor point.
font sans-serif A list of font faces to use. Follows the definition of SVG's font-family attribute.
rotate 0 The rotation to apply to the text. Use the deg or rad units to express a rotation in degrees or radians.
size 0 The text size, in pixels.
style normal The text style, such as italic or oblique.
weight 400 An integer between 100 and 900 that represents the text's “boldness”.
Attributes supported by the <label> element.

If the expr attribute is used, there is currently no way to control the formatting of the resulting number. If showing a particular number of decimal places or localizing the number for a different culture is necessary, it is recommended to use the text attribute instead.

  • Source
    <odv>
      <rect x1="25" y1="25" x2="275" y2="175">
        <paint><stroke width="2"><color r="1" a="0.2"/></stroke></paint>
      </rect>
      <label text="Left" align="left" size="14" x="25" y="25">
        <paint><fill/></paint>
      </label>
      <label text="Center" align="center" size="14" x="50 pctx" y="25">
        <paint><fill/></paint>
      </label>
      <label text="Right" align="right" size="14" x="275" y="25">
        <paint><fill/></paint>
      </label>
      <label text="Left" align="left" size="14" x="275" y="25" rotate="90 deg">
        <paint><fill/></paint>
      </label>
      <label text="Center" align="center" size="14" x="275" y="50 pcty" rotate="90 deg">
        <paint><fill/></paint>
      </label>
      <label text="Right" align="right" size="14" x="275" y="175" rotate="90 deg">
        <paint><fill/></paint>
      </label>
      <label text="Left" align="left" size="14" x="25" y="175" rotate="-90 deg">
        <paint><fill/></paint>
      </label>
      <label text="Center" align="center" size="14" x="25" y="50 pcty" rotate="-90 deg">
        <paint><fill/></paint>
      </label>
      <label text="Right" align="right" size="14" x="25" y="25" rotate="-90 deg">
        <paint><fill/></paint>
      </label>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG) Left Center Right Left Center Right Left Center Right
The <label> element.

Clipping rectangles

The <clip> element defines a rectangle outside which nothing is visible. The effect applies only to visible elements contained within the <clip> element. Although not directly visible, clipping rectangles are included here for their effect on visible shapes.

Attribute Default Value
x1 0 The X coordinate of the first corner that defines the rectangle.
y1 0 The Y coordinate of the first corner that defines the rectangle.
x2 0 The X coordinate of the second corner that defines the rectangle.
y2 0 The Y coordinate of the second corner that defines the rectangle.
Attributes supported by the <clip> element.

<clip> elements can be nested to further constrain the clipping region.

  • Source
    <odv>
      <paint>
        <fill><color b="1"/></fill>
      </paint>
      <clip x1="0" y1="25 pcty" x2="100 pctx" y2="75 pcty">
        <rect x1="0" y1="0" x2="100 pctx" y2="100 pcty">
          <paint>
            <fill><color g="1"/></fill>
          </paint>
        </rect>
        <arc x="50 pctx" y="50 pcty" r1="50 pctmin">
          <paint>
            <fill><color r="1"/></fill>
          </paint>
        </arc>
      </clip>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
The <clip> element.

Data

While the shapes listed in the preceding section could be used to create sophisticated data visualizations, the advance calculation and repetition required would mean that ODVML has virtually no advantage over similar vector graphics formats, such as SVG. Such an approach would also go against one of ODVML's primary goals: preserving the source data in a lossless manner so that the same data can be accurately rendered in new ways. To accomplish this, ODVML provides two key features:

Series

The root element of an ODVML document may contain a <data> element, which in turn may contain any number of <series> elements. A <series> element defines a named series of points. Each point can have multiple values, where each value is associated with a unique name.

Attribute Default Value
name (empty) A name to uniquely identify the series. Should not contain white space.
format xml The format of the data within the <series> element. In addition to xml, csv and json are supported.
Attributes supported by the <series> element.

As noted in the above table, the <series> element's optional format attribute identifies the format of the contained data. Regardless of the format, the data must be surrounded by the <![CDATA[ and ]]> tokens.This is an XML-ism that instructs the parser to ignore the character data between these tokens.

For csv data, the <series> element should contain a header line with comma-delimited field names, followed by one or more data lines with the same number of values as the header has fields. For example, a series of daily stock prices could be encoded as follows:

<odv>
  <data>
    <series name="prices" format="csv"><![CDATA[
      date,open,high,low,close,volume
      2012-11-01,598.22,603.0,594.17,596.5399,12903500.0
      2012-11-02,595.89,596.95,574.75,576.8,21406200.0
      2012-11-05,583.515,587.77,577.6,584.6211,18897700.0
      2012-11-06,590.23,590.74,580.09,582.8495,13389900.0
      2012-11-07,573.835,574.54,555.75,558.0019,28344600.0
      2012-11-08,560.63,562.23,535.29,537.75,37719500.0
      2012-11-09,540.42,554.88,533.72,547.06,33211200.0
      2012-11-12,554.15,554.5,538.65,542.83,18421500.0
      2012-11-13,538.91,550.48,536.36,542.898,19033900.0
      2012-11-14,545.5,547.45,536.18,536.88,17041800.0
      2012-11-15,537.53,539.5,522.62,525.62,28211100.0
      2012-11-16,525.2,530.0,505.7501,527.678,45246200.0
      2012-11-19,540.7097,567.5,539.88,565.73,29404200.0
      2012-11-20,571.91,571.95,554.58,560.9134,22955500.0
      2012-11-21,564.25,567.37,556.6,561.7,13321500.0
      2012-11-23,567.17,572.0,562.6,571.5,9743800.0
      2012-11-26,575.9,590.0,573.71,589.53,22520700.0
      2012-11-27,589.55,590.42,580.1,584.78,19047500.0
      2012-11-28,577.27,585.8,572.26,582.94,18602300.0
      2012-11-29,590.215,594.25,585.25,589.36,18382100.0
      2012-11-30,586.79,588.4,582.68,585.28,13975700.0
    ]]></series>
  </data>
<odv>

For json format, the <series> element is expected to contain an array of plain objects representing points. Properties that contain strings and numbers are used as the point fields, while properties that contain other value types are ignored. The same data as in the above example would be formatted as follows:

<odv>
  <data>
    <series name="prices" format="json"><![CDATA[
      [
        { "date": "2012-11-01", "open": 598.22, "high": 603.0, "low": 594.17, "close": 596.5399, "volume": 12903500.0 },
        { "date": "2012-11-02", "open": 595.89, "high": 596.95, "low": 574.75, "close": 576.8, "volume": 21406200.0 },
        { "date": "2012-11-05", "open": 583.515, "high": 587.77, "low": 577.6, "close": 584.6211, "volume": 18897700.0 },
        { "date": "2012-11-06", "open": 590.23, "high": 590.74, "low": 580.09, "close": 582.8495, "volume": 13389900.0 },
        { "date": "2012-11-07", "open": 573.835, "high": 574.54, "low": 555.75, "close": 558.0019, "volume": 28344600.0 },
        { "date": "2012-11-08", "open": 560.63, "high": 562.23, "low": 535.29, "close": 537.75, "volume": 37719500.0 },
        { "date": "2012-11-09", "open": 540.42, "high": 554.88, "low": 533.72, "close": 547.06, "volume": 33211200.0 },
        { "date": "2012-11-12", "open": 554.15, "high": 554.5, "low": 538.65, "close": 542.83, "volume": 18421500.0 },
        { "date": "2012-11-13", "open": 538.91, "high": 550.48, "low": 536.36, "close": 542.898, "volume": 19033900.0 },
        { "date": "2012-11-14", "open": 545.5, "high": 547.45, "low": 536.18, "close": 536.88, "volume": 17041800.0 },
        { "date": "2012-11-15", "open": 537.53, "high": 539.5, "low": 522.62, "close": 525.62, "volume": 28211100.0 },
        { "date": "2012-11-16", "open": 525.2, "high": 530.0, "low": 505.7501, "close": 527.678, "volume": 45246200.0 },
        { "date": "2012-11-19", "open": 540.7097, "high": 567.5, "low": 539.88, "close": 565.73, "volume": 29404200.0 },
        { "date": "2012-11-20", "open": 571.91, "high": 571.95, "low": 554.58, "close": 560.9134, "volume": 22955500.0 },
        { "date": "2012-11-21", "open": 564.25, "high": 567.37, "low": 556.6, "close": 561.7, "volume": 13321500.0 },
        { "date": "2012-11-23", "open": 567.17, "high": 572.0, "low": 562.6, "close": 571.5, "volume": 9743800.0 },
        { "date": "2012-11-26", "open": 575.9, "high": 590.0, "low": 573.71, "close": 589.53, "volume": 22520700.0 },
        { "date": "2012-11-27", "open": 589.55, "high": 590.42, "low": 580.1, "close": 584.78, "volume": 19047500.0 },
        { "date": "2012-11-28", "open": 577.27, "high": 585.8, "low": 572.26, "close": 582.94, "volume": 18602300.0 },
        { "date": "2012-11-29", "open": 590.215, "high": 594.25, "low": 585.25, "close": 589.36, "volume": 18382100.0 },
        { "date": "2012-11-30", "open": 586.79, "high": 588.4, "low": 582.68, "close": 585.28, "volume": 13975700.0 }
      ]
    ]]></series>
  </data>
<odv>

Finally, for xml format, the <series> element should contain a series of <point> elements with attributes for each field.Note that even though directly embedding <point> elements in the <series> element would yield valid XML, you must still use the <![CDATA[ and ]]> delimiters. The data from above in XML format would look like this:

<odv>
  <data>
    <series name="prices" format="xml"><![CDATA[
      <point date="2012-11-01" open="598.22" high="603.0" low="594.17" close="596.5399" volume="12903500.0"/>
      <point date="2012-11-02" open="595.89" high="596.95" low="574.75" close="576.8" volume="21406200.0"/>
      <point date="2012-11-05" open="583.515" high="587.77" low="577.6" close="584.6211" volume="18897700.0"/>
      <point date="2012-11-06" open="590.23" high="590.74" low="580.09" close="582.8495" volume="13389900.0"/>
      <point date="2012-11-07" open="573.835" high="574.54" low="555.75" close="558.0019" volume="28344600.0"/>
      <point date="2012-11-08" open="560.63" high="562.23" low="535.29" close="537.75" volume="37719500.0"/>
      <point date="2012-11-09" open="540.42" high="554.88" low="533.72" close="547.06" volume="33211200.0"/>
      <point date="2012-11-12" open="554.15" high="554.5" low="538.65" close="542.83" volume="18421500.0"/>
      <point date="2012-11-13" open="538.91" high="550.48" low="536.36" close="542.898" volume="19033900.0"/>
      <point date="2012-11-14" open="545.5" high="547.45" low="536.18" close="536.88" volume="17041800.0"/>
      <point date="2012-11-15" open="537.53" high="539.5" low="522.62" close="525.62" volume="28211100.0"/>
      <point date="2012-11-16" open="525.2" high="530.0" low="505.7501" close="527.678" volume="45246200.0"/>
      <point date="2012-11-19" open="540.7097" high="567.5" low="539.88" close="565.73" volume="29404200.0"/>
      <point date="2012-11-20" open="571.91" high="571.95" low="554.58" close="560.9134" volume="22955500.0"/>
      <point date="2012-11-21" open="564.25" high="567.37" low="556.6" close="561.7" volume="13321500.0"/>
      <point date="2012-11-23" open="567.17" high="572.0" low="562.6" close="571.5" volume="9743800.0"/>
      <point date="2012-11-26" open="575.9" high="590.0" low="573.71" close="589.53" volume="22520700.0"/>
      <point date="2012-11-27" open="589.55" high="590.42" low="580.1" close="584.78" volume="19047500.0"/>
      <point date="2012-11-28" open="577.27" high="585.8" low="572.26" close="582.94" volume="18602300.0"/>
      <point date="2012-11-29" open="590.215" high="594.25" low="585.25" close="589.36" volume="18382100.0"/>
      <point date="2012-11-30" open="586.79" high="588.4" low="582.68" close="585.28" volume="13975700.0"/>
    ]]></series>
  </data>
<odv>

As the csv format tends to save space, all examples in this section will use this format.

Defining a series implicitly defines several operators:

Operator Effect
@{series}#i Pushes all indexes from the series named {series} onto the stack. Indexes start at zero and go up by one for each point in the series. The number of indexes equals the number of points.
@{series}#{field} Pushes all values for the field named {field} from the series named {series} onto the stack.
@{series}#{field}:first Pushes the first finite value for the field named {field} from the series named {series} onto the stack, or NaN if there is no such value.
@{series}#{field}:last Pushes the last finite value for the field named {field} from the series named {series} onto the stack, or NaN if there is no such value.
Operators defined by the <series> element.

Loops

The <loop> element iterates through one series in order and draws the shapes it contains for each point.

Attribute Default Value
series (empty) The name of the series to loop through. This must exactly match the name of a defined series, or the entire element will be skipped.
Attributes supported by the <loop> element.

For each iteration, the <loop> element defines several variables for use in expressions. When such a variable is encountered in an expression, the variable's current value is pushed onto the stack as if it were a regular number. All loop variables begin with # to distinguish them from units and operators.

Variable Value
#i The iteration index. Starts at zero and is incremented after each loop iteration.
#{field} The numeric value of the field named {field} in the current point, or NaN if the field has no value in the current point. One such variable is created for each field in the series.
#{field}:prev The numeric value of the field named {field} in the immediately preceding point, or NaN if the immediately preceding point has no value for the field or if there is no such point. One such variable is created for each field in the series.
#{field}:last The last finite numeric value seen for the field named {field} in all points up to and including immediately preceding point, or NaN if no finite numeric value has been seen for the field. One such variable is created for each field in the series.
Variables created by the <loop> element.

A <loop> element can be used anywhere a shape can be used. In this case, the <loop> element may contain shapes. Alternatively, a <loop> element can be used within a <path> element. In this case, the <loop> element may only contain the path command elements listed in the section on paths.

Using the data in the code examples above, it is possible to create a scatter plot of the close values by drawing an arc in each loop iteration:

  • Source
    <odv>
      <paint><stroke width="2"/></paint>
      <loop series="prices">
        <arc x="#i 0.5 + 21 / 100 * pctx" y="#close 525.62 - 70.92 / 100 * 100 - -1 * pcty" r1="4">
          <paint><fill/></paint>
        </arc>
      </loop>
      <data>
        <series name="prices" format="csv"><![CDATA[
          date,open,high,low,close,volume
          2012-11-01,598.22,603.0,594.17,596.5399,12903500.0
          2012-11-02,595.89,596.95,574.75,576.8,21406200.0
          2012-11-05,583.515,587.77,577.6,584.6211,18897700.0
          2012-11-06,590.23,590.74,580.09,582.8495,13389900.0
          2012-11-07,573.835,574.54,555.75,558.0019,28344600.0
          2012-11-08,560.63,562.23,535.29,537.75,37719500.0
          2012-11-09,540.42,554.88,533.72,547.06,33211200.0
          2012-11-12,554.15,554.5,538.65,542.83,18421500.0
          2012-11-13,538.91,550.48,536.36,542.898,19033900.0
          2012-11-14,545.5,547.45,536.18,536.88,17041800.0
          2012-11-15,537.53,539.5,522.62,525.62,28211100.0
          2012-11-16,525.2,530.0,505.7501,527.678,45246200.0
          2012-11-19,540.7097,567.5,539.88,565.73,29404200.0
          2012-11-20,571.91,571.95,554.58,560.9134,22955500.0
          2012-11-21,564.25,567.37,556.6,561.7,13321500.0
          2012-11-23,567.17,572.0,562.6,571.5,9743800.0
          2012-11-26,575.9,590.0,573.71,589.53,22520700.0
          2012-11-27,589.55,590.42,580.1,584.78,19047500.0
          2012-11-28,577.27,585.8,572.26,582.94,18602300.0
          2012-11-29,590.215,594.25,585.25,589.36,18382100.0
          2012-11-30,586.79,588.4,582.68,585.28,13975700.0
        ]]></series>
      </data>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
Creating a scatter plot from data with the aid of the <loop> element.

This example reveals a major flaw: positioning the dots requires intimate knowledge of the data. Determining the X coordinate of each arc in terms of a percentage of the image width requires knowing in advance how many points are in the series (in this case, 21). Similarly, finding the Y coordinate of each arc requires knowing not only the range of values in the close field (70.92), but also the minimum close value (525.62). If the underlying data is changed, the representation must also be changed to keep in sync.

Fortunately this problem is solved by one of ODVML's key features: custom units, described in the following section.

Units

Units provide a bridge between presentation and data. This bridge allows the presentation (shapes) to remain ignorant of the content of the underlying data. Units can also provide basic support for panning and zooming in such a way that neither the data nor the presentation need be altered.

The root element of an ODVML document may contain a <units> element. Like the <data> element, the <units> element has no attributes; its sole purpose is to contain<unit> elements. A <unit> element requires values for the following attributes:

Attribute Default Value
name (empty) The unit's name. Must not contain white space. Avoid names that conflict with built-in units or operators.
Attributes supported by the <unit> element.

Every <unit> element must contain exactly two <map> elements. A <map> element defines a correlation between a unit value and a position along one axis of the output image. Two <map> elements gives sufficient information to convert any unit value into a output image coordinate.

Attribute Default Value
unit 0 The unit value. May contain an expression.
to 0 The output image coordinate value. May contain an expression.
Attributes supported by the <map> element.

Defining a unit creates not one, but two new units that may be used anywhere a built-in unit may be used.

Unit Value
{unit} The positional value of the unit with the name {unit}.
!{unit} The dimensional value of the unit with the name {unit}.
Units created by a <unit> element.

In this context, positional means that the unit maps from a number to a pixel position according to the points defined by the <map> elements. On the other hand, dimensional refers to the pixel distance between two integer unit values.

As an example, for a hypothetical unit named x that extends from 100 to 200, where 100 is mapped to the left edge of the image and 200 is mapped to the right edge of the image, the expression 150 x returns the pixel coordinate of the image's (horizontal) middle. The expression 10 !x, however, returns a number equal to ten percent of the image's width, since 10 is ten percent of the unit's range of 100, and the unit is defined as covering the full width of the image.

The example data used above is naturally plotted in terms of a correlation between index and price. Therefore, it makes sense to define a unit for each axis. Using a combination of built-in operators and the operators implicitly defined by <series> elements, it is possible to define each unit without knowledge of individual point values.

  • Source
    <odv>
      <paint><stroke width="2"/></paint>
      <loop series="prices">
        <arc x="#i 0.5 + index" y="#close price" r1="4">
          <paint><fill/></paint>
        </arc>
      </loop>
      <units>
        <unit name="index">
          <map unit="0" to="10"/>
          <map unit="@prices#i max 1 +" to="100 pctx 10 -"/>
        </unit>
        <unit name="price">
          <map unit="@prices#close max" to="10"/>
          <map unit="@prices#close min" to="100 pcty 10 -"/>
        </unit>
      </units>
      <data>
        <series name="prices" format="csv"><![CDATA[
          date,open,high,low,close,volume
          2012-11-01,598.22,603.0,594.17,596.5399,12903500.0
          2012-11-02,595.89,596.95,574.75,576.8,21406200.0
          2012-11-05,583.515,587.77,577.6,584.6211,18897700.0
          2012-11-06,590.23,590.74,580.09,582.8495,13389900.0
          2012-11-07,573.835,574.54,555.75,558.0019,28344600.0
          2012-11-08,560.63,562.23,535.29,537.75,37719500.0
          2012-11-09,540.42,554.88,533.72,547.06,33211200.0
          2012-11-12,554.15,554.5,538.65,542.83,18421500.0
          2012-11-13,538.91,550.48,536.36,542.898,19033900.0
          2012-11-14,545.5,547.45,536.18,536.88,17041800.0
          2012-11-15,537.53,539.5,522.62,525.62,28211100.0
          2012-11-16,525.2,530.0,505.7501,527.678,45246200.0
          2012-11-19,540.7097,567.5,539.88,565.73,29404200.0
          2012-11-20,571.91,571.95,554.58,560.9134,22955500.0
          2012-11-21,564.25,567.37,556.6,561.7,13321500.0
          2012-11-23,567.17,572.0,562.6,571.5,9743800.0
          2012-11-26,575.9,590.0,573.71,589.53,22520700.0
          2012-11-27,589.55,590.42,580.1,584.78,19047500.0
          2012-11-28,577.27,585.8,572.26,582.94,18602300.0
          2012-11-29,590.215,594.25,585.25,589.36,18382100.0
          2012-11-30,586.79,588.4,582.68,585.28,13975700.0
        ]]></series>
      </data>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
Using <unit> elements to create an improved scatter plot.

In the above example, the index unit extends from 10 pixels right of the left border to 10 pixels left of the right border. The expression @prices#i max 1 + returns a number one greater than the greatest point index, thereby yielding the total number of points. Thus in the above example, which has 21 points and an output image width of 300 pixels, the expression 0 index maps to 10 px, 21 index maps to 290 px, and 10.5 index maps to 150 px. If the image width or number of points changes, the unit scales accordingly, with no changes to the shapes required.

Similarly, the price unit extends from 10 pixels below the top border to 10 pixels above the bottom border. Note that the maximum close value is used for the end of the range closest to the top border, which means that the unit values increase toward the image top, as is customary with price charts. This avoids the need for a more complex expression to convert from the usual computer graphics coordinate system in which Y increases toward the image bottom.

With SVG, it is possible to replicate this visualization for a known output image size, but the SVG version has several disadvantages:

First, all positions must be calculated ahead of time. SVG lacks the support for render-time calculations found in ODVML, and so ahead-of-time calculation is needed. The results of this calculation, being defined in terms of image coordinates, obscures all connection to the underlying data, making it impossible to recover the source data after the fact. With ODVML, not only is the data retained in raw form, the expressions used to convert point values to coordinates are potentially highly readable.

Second, the gap between the edge of the image and the most extreme points is not held constant. In ODVML, defining a unit range in terms of absolute offsets from the edge of image effectively preserves a constant space for labels or other decorations. In SVG, the space available for axis labels shrinks and grows proportionally with the rest of the image, leading to labels that fit at one size overflowing at another size.

Finally, the dots are not scaled uniformly. Unless the original aspect ratio is maintained, it is not possible for the dots to remain circular in SVG while keeping the source edges in line with the output edges. Scaling the SVG image only along one axis turns the dots into ellipses, which is not desirable for a scatter plot. With ODVML, if the arc radius is defined in terms of absolute pixels, each dot remains the same size regardless of the output image size.

Conditionals

In certain cases, it is necessary to alter the display of a shape based on the data it represents. For example, when rendering a chart of stock prices as a candlestick plot, the shape of each candle gives no clue as to whether the stock closed higher than it opened. Therefore, it is customary to draw “increasing” candles differently than “decreasing” candles, either by using a different color or by filling in the body of the candle.

To handle cases this and other cases, ODVML provides an <if> element usable in several locations:

  • In locations where a shape may be used, an <if> element's clauses may contain shapes.
  • Inside a <path> element, an <if> element's clauses may contain path command elements.
  • Inside a <paint> element, an <if> element's clauses may contain <fill> and <stroke> elements.

<if> elements may be nested within the clause elements to build up complex conditionals if needed.In this case, the child elements retain the same clause restrictions as the parent element.

The <if> element supports a single attribute:

Attribute Default Value
test 0 An expression to test. The top value of the stack after evaluating the expression is used as the test value. It is permissible for this expression to return a non-finite number.
Attributes supported by the <if> element.

The <if> element may contain any number of clause elements. Each clause element is evaluated against the value returned by the test expression and, if the clause evaluates to true, the elements within the clause are processed.Note that unlike if constructs in most programming languages, every clause is tested regardless of whether a preceding clause passed or failed.

The <defined> and <undefined> clauses test whether the value of the test expression is finite or non-finite (infinity or NaN), respectively. These clause elements have no attributes.

The remaining clauses perform numeric comparisons:

Clause Test
<between> Passes if the test expression is between the value of the expressions in the clause's min and max attributes.
<equal> Passes if the test expression is equal (or very, very close) to the value of the expression in the clause's to attribute.
<greater> Passes if the test expression is greater than the value of the expression in the clause's than attribute.
<less> Passes if the test expression is less than the value of the expression in the clause's than attribute.
Comparison clauses supported by the <if> element.

Of the above clauses, the <between>, <greater>, and <less> clauses also support an inclusive attribute that determines whether the comparison value is included in the comparison. For <greater> and <less>, if the inclusive attribute is set to yes, the comparisons become “greater than or equal to” and “less than or equal to”, respectively. The <between> clause's inclusive attribute supports the values min, max, and both to select which end/ends of the range is/are included in the comparison.

A numeric comparison to NaN always evaluates to false.

As a simple demonstration, the above scatter plot example can be converted to a candlestick chart in the following way:

  • Source
    <odv>
      <paint><stroke width="2"/></paint>
      <loop series="prices">
        <path>
          <paint>
            <if test="#close">
              <greater than="#open" inclusive="yes">
                <fill><color r="1" g="1" b="1"/></fill>
              </greater>
              <less than="#open">
                <fill><color r="0" g="0" b="0"/></fill>
              </less>
            </if>
            <stroke/>
          </paint>
          <!-- Draw a box extending from the open to the close: -->
          <move x="#i 0.2 + index" y="#open price"/>
          <line x="#i 0.8 + index" y="#open price"/>
          <line x="#i 0.8 + index" y="#close price"/>
          <line x="#i 0.2 + index" y="#close price"/>
          <close/>
          <if test="#high">
            <greater than="#open #close max">
              <!-- Draw the "wick" from the top to the high: -->
              <move x="#i 0.5 + index" y="#open #close max price"/>
              <line x="#i 0.5 + index" y="#high price"/>
            </greater>
          </if>
          <if test="#low">
            <less than="#open #close min">
              <!-- Draw the "wick" from the bottom to the low: -->
              <move x="#i 0.5 + index" y="#open #close min price"/>
              <line x="#i 0.5 + index" y="#low price"/>
            </less>
          </if>
        </path>
      </loop>
      </loop>
      <units>
        <unit name="index">
          <map unit="0" to="10"/>
          <map unit="@prices#i max 1 +" to="100 pctx 10 -"/>
        </unit>
        <unit name="price">
          <map unit="@prices#close max" to="10"/>
          <map unit="@prices#close min" to="100 pcty 10 -"/>
        </unit>
      </units>
      <data>
        <series name="prices" format="csv"><![CDATA[
          date,open,high,low,close,volume
          2012-11-01,598.22,603.0,594.17,596.5399,12903500.0
          2012-11-02,595.89,596.95,574.75,576.8,21406200.0
          2012-11-05,583.515,587.77,577.6,584.6211,18897700.0
          2012-11-06,590.23,590.74,580.09,582.8495,13389900.0
          2012-11-07,573.835,574.54,555.75,558.0019,28344600.0
          2012-11-08,560.63,562.23,535.29,537.75,37719500.0
          2012-11-09,540.42,554.88,533.72,547.06,33211200.0
          2012-11-12,554.15,554.5,538.65,542.83,18421500.0
          2012-11-13,538.91,550.48,536.36,542.898,19033900.0
          2012-11-14,545.5,547.45,536.18,536.88,17041800.0
          2012-11-15,537.53,539.5,522.62,525.62,28211100.0
          2012-11-16,525.2,530.0,505.7501,527.678,45246200.0
          2012-11-19,540.7097,567.5,539.88,565.73,29404200.0
          2012-11-20,571.91,571.95,554.58,560.9134,22955500.0
          2012-11-21,564.25,567.37,556.6,561.7,13321500.0
          2012-11-23,567.17,572.0,562.6,571.5,9743800.0
          2012-11-26,575.9,590.0,573.71,589.53,22520700.0
          2012-11-27,589.55,590.42,580.1,584.78,19047500.0
          2012-11-28,577.27,585.8,572.26,582.94,18602300.0
          2012-11-29,590.215,594.25,585.25,589.36,18382100.0
          2012-11-30,586.79,588.4,582.68,585.28,13975700.0
        ]]></series>
      </data>
    </odv>
  • Actual output (ODV)
  • Expected output (SVG)
Using <if> elements to correctly paint a candlestick chart.

The above example demonstrates the usage of an <if> element to not only determine the proper fill color for the candle (white if the close value is greater than or equal to the open value, black if it is less), but also whether it is necessary to draw either the top or bottom wicks.

As with previous examples, while it is possible to replicate this chart in SVG with laborious, error-prone precalculation, the ODVML version handles scaling much better. In particular, scaling the SVG horizontally will undesirably scale the width of the candle wicks.This is mitigated by the non-scaling-stroke value for the vector-effect attribute defined in SVG Tiny 1.2, but support for this spec is far from universal. In contrast, the ODVML version maintains the desired stroke width regardless of output size.

Appendix

Why XML?

The evaluation of potential formats for what would eventually become ODVML adhered to the following principles:

  • The format should be human-readable and human-editable without specialized tools. It is important that users are able to quickly whip up basic ODV documents with a plain text editor and experiment without first installing special editing software. This goal eliminates binary formats.
  • The format should not be custom. Although a custom format could be crafted to best express the structures in ODV, using a custom format would require that every rasterizer implementation also include a parser for said format. This increases the expense of creating (and potential for bugs in) every such implementation.
  • The format should have excellent cross-language support through either a well-maintained package or (preferably) the standard library. This goal aims to reduce the effort spent on parsing an ODV document to a function call into an easily-imported library.

XML, JSON, and YAML fall out as the three best options that satisfy the above criteria. YAML arguably has the best “look and feel”, as it makes optional much of the brace-and-bracket trappings required by its cousin JSON. JSON enjoys wider standard library support than YAML, and is the de facto data transport language of the modern Web. XML, being the most verbose of the three, is much-derided for its proliferation of angle brackets and extraneous syntax, and at first glance would appear to be the least favorable choice.

XML does, however, hold two critical advantages over JSON and YAML. First, XML is naturally suited to describing a tree of objects. The “tree of objects” approach has enjoyed success as a natural way to describe graphical user interfaces and other visual compositions (as evidenced by SVG). Second (and more importantly), XML enforces the concept of object types at the syntactical level. Every element must have a name that identifies its type. To replicate this feature in JSON or YAML, one would have to standardize on an object property, such as type or kind, to encode that object's type. This means that a valid JSON or YAML document may not actually be valid if that property is missing from an object or contains an invalid value. (What happens if type contains the number 42?) This necessitates parse-time checking that is not needed for XML, as an element without a name is simply malformed.

When considering the above, XML emerged as well suited to the task of describing ODV documents. Despite its syntactical warts, it has extensive support both from a programming language standpoint and a tooling standpoint. ODVML therefore adds to a long history of XML-derived markup languages that have reached the same conclusion.