Module:Charts SVG

Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

This module creates charts by the generation of code for an SVG file.

(For background to the project, and current issues with the module, see Module talk:Charts SVG.)

The module exposes five functions: barChart, lineChart, mixedChart, scatterChart and pieChart.

Setup & Usage edit

This module is not intended to be used in Wikipedia articles. It is best used in a page in your own userspace, or in a sandbox. On an existing page, add {{#invoke:Charts SVG}} (with further parameters) to the page, and click Preview. When you do, the module will show a box that includes a lot of text, starting with <?xml version="1.0" encoding="UTF-8" ?>.

The process of using the module from there involves a number of steps:

  • on your computer, create a file with the filename extension '.svg' (eg: mychart.svg)
  • open this file with a text editor (eg: Notepad)
  • back in your browser, use your mouse to select the text within the box. It will start with <?xml version="1.0" encoding="UTF-8" ?> and end with </svg>.
  • copy this text, paste it into the file on your computer, and save the file.
  • To test the file is producing the image you want, you can view the file directly in some web browsers, or go to the Labs SVG Checker.
Note that results in web browsers, and in the Wikimedia Labs checker, may be different from the final Wikimedia rendering. Currently the only way to be absolutely sure of the final rendering is to upload the file and view it on a Wikipedia page. See #Known Rendering Issues.
  • Once you are happy with the image, upload the .svg file, then you can use it in a Wikipedia article using a [[File:]] link.
  • If you upload the image to commons, which is preferred to uploading to individual wikis, please add [[Category:Charts created with Charts SVG]] to the bottom of the file description page for the image.
  • It is also good practice to include the module-call that produced the image on the file description page.

Invoking the Module edit

The first parameter in the module call must be one of the following function names:

barChart Creates a bar chart with X and Y axes, showing bars of height (or length) Y at position X.
lineChart Creates a line chart with X and Y axes, showing lines between specified X/Y points.
mixedChart Creates a chart with X and Y axes, with both bars and lines.
scatterChart Creates a scatter chart with X and Y axes, showing markers at specified X/Y points.
pieChart Creates a pie chart with segments proportional to each of a series of values.

Without further parameters the module will produce (for barChart, lineChart, mixedChart and scatterChart) a chart with X and Y axes and gridlines, but no other content. For a pieChart, it will produce a completely blank image.

A note on terminology: throughout this documentation 'image' means the whole area produced by the SVG code produced by this module, while 'chart' refers to the area bounded by the X and Y axes (or, for pie charts, the rectangle that includes the pie). Most texts appear (by default) outside the area of the 'chart'.

  • for some parameters named using the style eg: Series<N>Values, parameters are ignored beyond the first value of <N> that does not have a parameter defined: eg: if only Series1Values, Series2Values and Series4Values are defined, only series 1 and 2 will be used. This is noted in the parameter descriptions as "Must be in sequence."

Script Errors and Messages edit

Instead of the SVG code, you may get some red text saying Script error. You can click on that text to get the details of the error. If the details simply say 'Script error: No such module.', the module name is in error. WP is case-sensitive about module names, so make sure the name is 'Charts SVG'. If the details say 'Script error: The function you specified did not exist.', the first parameter is not one of the exposed function names.

Any other Script errors are errors in the module script itself. Hopefully these do not occur, but if they do, please report them at Module talk:Charts SVG, including the backtrace information and details of the module call.

Output may also, instead of SVG code, be messages headed "Charts SVG - Messages", containing messages from the module about issues with the supplied parameters. This can include a list of parameters whose names are not recognised. A maximum of 10 issues are reported at a time.

Parameters - X/Y Graphs edit

Chart Size edit

Parameter Chart Types Description Type &
Limits
ChartWidth bar
line
mixed
scatter
Sets the width of the chart, in pixels. Default: 500 Numeric
must be >= 200
ChartHeight bar
line
mixed
scatter
Sets the width of the chart, in pixels. Default: 350 Numeric
must be >= 200
ChartAdjust bar
line
mixed
scatter
Any value sets axis XMax, XMin, YMax and YMin values to be adjusted by the module to whole multiples of AxisValueStep (or if specified, AxisMark2) for the appropriate direction, and then the related chart dimension is adjusted similarly. This ensures the mark spacings are in exact whole numbers of pixels. The before and after adjustment values are written in a comment in the output SVG - using the adjusted values means further adjustment will not be required. Note: Y2Max and Y2Min are not adjusted. string

Axes edit

X and Y Axes edit

Parameter Chart Types Description Type &
Limits
XMin
YMin
bar
line
mixed
scatter
The minimum value for the range of numbers shown on the axis. Default: 0 Numeric
XMax
YMax
bar
line
mixed
scatter
The maximum value for the range of numbers shown on the axis. Default: 100 Numeric
XAxisValueStep
YAxisValueStep
bar
line
mixed
scatter
The interval between intermediate values shown on the axis, and therefore the major tick-marks. Default: 10 Numeric
XAxisMark2Step
YAxisMark2Step
bar
line
mixed
scatter
The interval between minor tick-marks shown on the axis. If not set, minor tick-marks are not shown. Numeric

Grid Lines edit

Grid lines are shown on charts with axes by default, though defining any data groups will hide the XAxis grid lines. The default grid interval is that of the major tick-marks, which is equal to the ValueStep parameter for the axis.

Parameter Chart Types Description Type &
Limits
XGrid
YGrid
bar
line
mixed
scatter
A value of none will hide the grid lines for the axis. Any numeric value sets the grid line interval. Default: the ValueStep value for the axis. Numeric or none

Second Y Axis edit

A second Y axis can be shown on the right hand side of the chart. The switch to show this axis is setting Y2Max to any value. However if Stack or Stack100 are set, a second Y Axis will not be shown.

Parameter Chart Types Description Type &
Limits
Y2Min bar
line
mixed
scatter
The minimum value for the range of numbers shown on the axis. Default: 0 Numeric
Y2Max bar
line
mixed
scatter
The maximum value for the range of numbers shown on the axis. Numeric
YAxis2ValueStep bar
line
mixed
scatter
The interval between intermediate values shown on the axis, and therefore the major tick-marks. Default: 10 Numeric
YAxis2Mark2Step bar
line
mixed
scatter
The interval between minor tick-marks shown on the axis. If not set, minor tick-marks are not shown. Numeric

Note that the zero points of the left and right Y axes will only automatically align if both YMin and Y2Min are zero.

Axis Titles and Values edit

Parameter Chart Types Description Type &
Limits
XAxisTitle
YAxisTitle
YAxis2Title
bar
line
mixed
scatter
Adds a title for the axis, which appears outside the axis values. String
XAxisValueMultiplier
YAxisValueMultiplier
YAxis2ValueMultiplier
bar
line
mixed
scatter
Multiplies the displayed values by the specified value. A common pattern would be to set YAxisValueMultiplier to 0.001, which would show '1', '2', ... on the Y axis instead of '1,000', '2,000'. Note that the Y data values in this example must still be in the range YMin to YMax. Default: 1 Numeric
XAxisValueRound
YAxisValueRound
YAxis2ValueRound
bar
line
mixed
scatter
Rounds the displayed values to the specified precision. Positive values round to that number of decimal places (but note that it will not add zeros to that number of places). Negative values round to that number of places above zero (eg: -3 will round to the nearest 1000). A value of zero will round to the nearest integer. The default is the number of decimal places in the axis ValueStep (eg: XAxisValueStep). Numeric
XAxisValueAbsolute
YAxisValueAbsolute
YAxis2ValueAbsolute
bar
line
mixed
scatter
Any value sets the axis values to display as absolute values (ie: without -ve signs). This allows graphs centered around an axis (eg: population pyramids) to show +ve values on both sides of the axis. Numeric
XAxisValuePrefix
YAxisValuePrefix
YAxis2ValuePrefix
bar
line
mixed
scatter
Add text to the start of the axis values. An underline in this text will be converted to a space when displayed. Commonly this would be $. String
XAxisValueSuffix
YAxisValueSuffix
YAxis2ValueSuffix
bar
line
mixed
scatter
Add text to the end of the axis values. An underline in this text will be converted to a space when displayed. Commonly this would be %. String
XAxisValueFormat
YAxisValueFormat
YAxis2ValueFormat
bar
line
mixed
scatter
By default the module will add numeric (eg: thousands) separators to numeric axis values if the maximum value for the axis (eg: XMax) is 10,000 or greater. Setting the ValueFormat parameter to none for an axis will suppress this formatting, any other value will force it to occur (regardless of the axis max value).
Note that group names are text, and never re-formatted by the module.
String
XAxisValueRotate
YAxisValueRotate
YAxis2ValueRotate
bar
line
mixed
scatter
Sets the angle by which the axis values are rotated. By default the module shows values in horizontal text, and centered on the mark. Numeric (-90 to 90)
XAxisValueSpace
YAxisValueSpace
YAxis2ValueSpace
bar
line
mixed
scatter
Sets the space available for the axis values outside the chart. These only need to be set if the calculated spaces are too small. Numeric
+ve only

Axis Lines edit

Parameter Chart Types Description Type &
Limits
YAxisColor
YAxis2Color
bar
line
mixed
scatter
The color for the axis line. Must be a number in the colors table or an SVG color term. Default: black String
XAxisArrows
YAxisArrows
bar
line
mixed
scatter
Any value sets the axis line to show arrow heads at the ends of the line, though an arrow pointing in the negative direction is not shown unless the axis Min value is less than zero. Any

Data Series edit

Parameter Chart Types Description Type &
Limits
Series1Values
(Series2Values, ...)
Must be in sequence.
bar
line
mixed
scatter
A string containing a series of X and Y numeric-value pairs, all separated by spaces. Each pair specifies a point (in scatter and line charts), or an X-position and bar-height (in bar charts). All points, lines and bars in a series are shown in the same style. String
Series1Type
(Series2Type, ...)
mixed
  • line sets the series to be a line graph
  • bar sets the series to be a bar graph

Default: line

String
Series1YAxis2
(Series2YAxis2, ...)
bar
line
mixed
scatter
Any value sets the series to use the 2nd Y axis (if it is displayed) as the scale for the Y values. Any
Series1Labels
(Series2Labels, ...)
bar
line
mixed
scatter
Any value sets the series to show the Y-value of each data point next to that point (or above the bar). The default size for the labels text is the same as that for chart texts. Labels are not shown if Stack100 is set. String
Series1Color
(Series2Color, ...)
bar
line
mixed
scatter
The color of the markers, lines or bars. Must be a number in the colors table or an SVG color term. The default color for series N is color N from the colors table. String
Series1Line
(Series2Line, ...)
line
mixed
scatter
A value of none will hide the line between the points of the series, any other value will show a line. The default for scatter charts is none, for line charts yes. String
Series1Width
(Series2Width, ...)
line
mixed
scatter
The width of the line for the series, as a percentage of the standard line width. Default: 100 Numeric
+ve only
Series1Dash
(Series2Dash, ...)
line
mixed
scatter
The dash pattern for the line for the series. Either a number (1 to 8), which will select a pattern from the dash patterns table, or an SVG dash pattern term. All lines are solid by default. Numeric (1-8)
or string
Series1Marker
(Series2Marker, ...)
line
mixed
scatter
The marker shown at the points for the series. A number (1 to 7) will select a marker from the markers table. none will hide the markers for the series. Markers and lines for a series are always the same color. Numeric (0-7)
or none
Series1MarkerSize
(Series2MarkerSize, ...)
line
mixed
scatter
The size of the series markers, as a percentage of the standard size. Default: 100 Numeric
+ve only
Series1MarkerFill
(Series2MarkerFill, ...)
line
mixed
scatter
The fill color for the series markers. Must be a number in the colors table or an SVG color term. The default color for series N is color N from the colors table. String
Series1Pattern
(Series2Pattern, ...)
bar
line
mixed
The fill pattern for the area fill or bar. Must be a number in the fill-patterns table. Numeric
Series1PatternColor
(Series2PatternColor, ...)
bar
line
mixed
The color used for the fill pattern. Must be a number in the colors table or an SVG color term. Default: black String

Be aware that data series are displayed in reverse order, so series 1 is shown in front of series 2.

Data Groups edit

Parameter Chart Types Description Type &
Limits
Group1Text
(Group2Text, ...)
Must be in sequence.
bar
line
mixed
scatter
The name for the data group. If Group1Text is defined, the X axis will be divided into groups as required, and the X values in Series1Values (etc.) must be integers corresponding to the defined groups. The XMin and XMax parameters will be ignored. String

Bar Width & Spacing edit

Parameter Chart Types Description Type &
Limits
BarWidth bar
mixed
The width of bars, in pixels. Only used if groups are not defined. Default: 20 Numeric
+ve only
BarSpace bar
mixed
The width of the gap between bars in a group, as a percentage of the bar width. Only used if groups are defined. Default: 0 Numeric
+ve only

Chart Variations edit

Parameter Chart Types Description Type &
Limits
Area line Any value sets all series lines to be filled with color (or a fill pattern) from the X axis to the line. It also turns off any markers or dash patterns on the lines. Any
Stack bar
line
Only valid if data groups are defined. Any value sets series Y values to be stacked. Be aware that this may require adjustment of the YMax and YMin values for proper display. Negative values are stacked separately from positive values. Any
Stack100 bar
line
Only valid if data groups are defined. Any value sets series Y values to be stacked, then re-scaled to the range 1 to 100. The Y axis will be forced to the range 0 (or -100 if there are any negative values) to 100, the YMin and YMax parameters will be ignored, and YAxisValueSuffix will be set to %. Any
HorizontalBarGraph bar Any value sets a bar graph to display horizontal bars, with X values on the vertical axis, and Y values on the horizontal axis. Be aware that, if changing a vertical bar graph to a horizontal one, parameters such as ChartHeight, ChartWidth and axis-value rotates may also need to be changed. Any
GroupsTopDown bar Any value sets a horizontal bar graph with groups on the X axis to show the groups in sequence from the top down instead of the default bottom up. It also changes the series order in each group to top-down. Any

In a lineChart, setting Stack or Stack100 will set Area to 'on'.

Parameters - Pie Charts edit

For pie charts, the chart size is calculated from the PieRadius.

Parameter Chart Types Description Type &
Limits
PieRadius pie The radius of the pie chart, in pixels. Default: 200 Numeric
+ve only
Explode pie A numeric value will explode the specified number of pie segments (starting with the first) away from the centre. Any non-numeric value will explode all segments. Any
ExplodeRadius pie The proportion of the radius that exploded segments are shifted. A percentage of the total pie radius. Default: if Explode is non-numeric, 10, otherwise 20. Numeric
+ve only
DoughnutHole pie A numeric value specifies the size of the hole in the centre of the pie (so producing a doughnut chart), as a percentage of the pie radius. Any non-numeric value will produce the default hole size of 50%. Any
SegmentText pie
  • text will show the segment text next to the segment
  • value will show the data value next to each segment
  • percent will show the percentage of the total values next to each segment

Any combination of the above three values can be in the parameter, eg: SegmentText=text percent will show the segment text followed by the percentage. The font size for segment texts is the same as that for legend texts.

String
SegmentTextWidth pie Sets the horizontal space allowed for all segment texts, as a percentage of the standard width. The standard width allows for about 10 characters. Set this parameter above 100 if any of the segment texts fail to show completely. Default: 100 Numeric
+ve only
SegmentTextRadius pie The proportion of the radius at which segment texts are placed. A percentage of the pie radius. Default: 105 Numeric
+ve only
PieStartAngle pie Sets the angle at which the first segment starts. Default: 0 The default means the first segment will start at the normal start angle for trigonometric circles, the X-axis (ie: 3 o'clock). Numeric
0 to 360 only
PieSweepDir pie Sets the direction segments sweep in. Any value other than the default will cause the segments to sweep in a clockwise direction. Default: AntiClockwise String

Pie Chart Data edit

Parameter Chart Types Description Type &
Limits
Series1Values pie A string containing text and value pairs, separated by spaces. The texts will be used for legend or segment texts, the values used to calculate the segments of the pie. In pie charts, parameters Series2Values etc. will be ignored. String

Pie Segment Appearance edit

Parameter Chart Types Description Type &
Limits
Segment1Color
(Segment2Color, ...)
pie The color of the pie segment. Must be a number in the colors table or an SVG color term. The default color for segment N is color N from the colors table. String
Segment1Pattern
(Segment2Pattern, ...)
pie The fill pattern for the pie segment. Must be a number in the fill-patterns table. Numeric
Segment1PatternColor
(Segment2PatternColor, ...)
pie The color used for the fill pattern. Must be a number in the colors table or an SVG color term. Default: black String

Parameters - General edit

SVG File Descriptive Elements edit

These parameters set values that are written to the SVG file, but are not displayed in the image.

Parameter Chart Types Description Type &
Limits
FileTitle All Adds a <title> element to the SVG file. Default: 'SVG chart' String
FileDesc All Adds a <description> element to the SVG file. Default: 'SVG chart generated by Charts SVG' String

Title and Footnote edit

Parameter Chart Types Description Type &
Limits
Title All Sets the text to show as a title. By default, the title is centered above the chart. String
TitleX
TitleY
bar
line
mixed
scatter
Moves the title from its default position to a specified location within the chart. The X and Y values are as for graph points.

The position specifies the left end and the baseline of the text. If TitleX is not specified, the title will appear in its default position. If TitleX is specified but TitleY is not, TitleY will default to zero.

Numeric
TitleX
TitleY
pie Moves the title from its default position (above the chart) to a specified location within the chart. The X and Y values are percentages of the chart size. (0, 0) = bottom left, (50, 50) = the pie origin.

The position specifies the left end and the baseline of the text. If TitleX is not specified, the title will appear in its default position. If TitleX is specified but TitleY is not, TitleY will default to zero.

Numeric
Footnote All Sets the text to show as a footnote. By default, the footnote is shown at the bottom right of the image. String
FootnoteX
FootnoteY
bar
line
mixed
scatter
Moves the footnote from its default position to a specified location within the chart. The X and Y values are as for graph points.

The position specifies the left end and the baseline of the text. If FootnoteX is not specified, the title will appear in its default position. If FootnoteX is specified but FootnoteY is not, FootnoteY will default to zero.

Numeric
FootnoteX
FootnoteY
pie Moves the footnote from its default position to a specified location within the chart. The X and Y values are percentages of the chart size. (0, 0) = bottom left, (50, 50) = the pie origin.

The position specifies the left end and the baseline of the text. If FootnoteX is not specified, the title will appear in its default position. If FootnoteX is specified but FootnoteY is not, FootnoteY will default to zero.

Numeric

Legend edit

Parameter Chart Types Description Type &
Limits
Series1Text
(Series2Text, ...)
Must be in sequence.
All Sets the text that appears in the legend as the name for the series. If no Series<N>Text parameters are set, the legend will not be shown. String
LegendType All
  • vertical will show the legend to the right of the chart, with legend elements in a vertical column
  • horizontal will show the legend below the chart, with legend elements fitted horizontally into the chart width
  • none will hide the legend

Default: vertical

String
LegendTextWidth All Sets the horizontal space allowed for all legend texts, as a percentage of the standard width. The standard width allows for about 10 characters. Set this parameter above 100 if any of the legend texts fail to show fully, or if they extend outside the legend box. Default: 100 Numeric
+ve only
LegendBorder All The color for the border surrounding the legend. Must be a number in the colors table or an SVG color term. A value of none will hide the border. Default: black String
LegendX
LegendY
All Moves the legend from its default position (which depends on the LegendType setting) to a specified location within the chart. The X and Y values are as for graph points (or, for a pie chart, as a % of the chart width/height).

The position specifies the top left corner of the legend box. If LegendX is not specified, the legend will appear in its default position. If LegendX is specified but LegendY is not, LegendY will default to zero.

Numeric
LegendSVG All Completely replaces the legend with the supplied SVG code. String

Chart Texts edit

Additional texts can be shown on the chart. Appropriately placed, they can be a substitute for a legend.

Parameter Chart Types Description Type &
Limits
ChartText1
(ChartText2, ...)
Must be in sequence.
All The text to show. String
ChartText1X
(ChartText2X, ...)
bar
line
mixed
scatter
Sets the X-Axis location for the text. (If groups are defined, each group is 1 unit.) Default: 0 Numeric
+ve only
Numeric
ChartText1Y
(ChartText2Y, ...)
bar
line
mixed
scatter
Sets the Y-Axis location for the text. Default: 0 Numeric
+ve only
ChartText1X
(ChartText2X, ...)
pie Sets the horizontal location for the text. The value is a percentage of the chart size. Default: 0 Numeric
+ve only
ChartText1X
(ChartText2X, ...)
pie Sets the vertical location for the text. The value is a percentage of the chart size. Default: 0 Numeric
+ve only
Numeric

In all cases the position specifies the baseline and left end of the text.

General Image edit

Parameter Chart Types Description Type &
Limits
ImagePadding All Sets the number of pixels in the padding. Replaces the default padding within the image border, on all 4 sides. Numeric
ImagePaddingTop
ImagePaddingBottom
ImagePaddingLeft
ImagePaddingRight
All Sets the number of pixels in the padding. Sets the padding amount on one side of the image only. Default: ImagePadding (if set), otherwise 6 pixels. Numeric
ImageBorder All The color of a border at the outside edges of the image. Must be a number in the colors table or an SVG color term. The border (if shown) is always 1px in width. String
ImageBackgroundColor All The color for the background of the whole image. Must be a number in the colors table or an SVG color term. Default: white String
ImageBackgroundSVG All Allows insertion of user-defined SVG code into the file. The specified elements are shown in front of the image background color, but behind all other elements in the image. String
ImageForegroundSVG All Allows insertion of user-defined SVG code into the file. The specified elements are shown in front of all other elements in the image. String

General Chart edit

Parameter Chart Types Description Type &
Limits
GrayScale
GreyScale
All Any value changes the color table to one of grey-scale equivalents of the colors (see colors table). It also changes the default pattern color to white.

Note that any colors defined with a number from the colors table will be shown as the grey-scale equivalent, but colors defined with an SVG color term will be shown with the color as defined.

Any
LineWidth All Changes the default width for all line components of the chart. A percentage of the default line width. Default: 100 Numeric
+ve only
GraphLineWidth line
mixed
scatter
Changes the default width for all series lines on the chart. A percentage of the default line width. Default: 100 Numeric
+ve only
ChartBackgroundColor All The background color for the chart area. Must be a number in the colors table or an SVG color term. String

Font Sizes edit

Parameter Chart Types Description Type &
Limits
FontSize All Changes the font size for all text elements in the image. A percentage of the default size for each element. Default: 100 Numeric
+ve only
LabelsFontSize All Changes the font size for data labels in the image. A percentage of the default size. Default: 100 Numeric
+ve only
TitleFontSize
FootnoteFontSize
LegendFontSize
ChartTextFontSize
All Changes the font size for the particular text element. A percentage of the default size for each element. Default: 100 Numeric
+ve only
XAxisTitleFontSize
XAxisValuesFontSize
YAxisTitleFontSize
YAxisValuesFontSize
YAxis2TitleFontSize
YAxis2ValuesFontSize
bar
line
mixed
scatter
Changes the font size for the particular text element(s). A percentage of the default size for each element. Default: 100 Numeric
+ve only

Bar, Pie-segment and Area Borders edit

Parameter Chart Types Description Type &
Limits
BorderColor bar
line
mixed
pie
The color of the border around each bar, pie segment or line area. Must be a number in the colors table or an SVG color term. No border is shown unless BorderColor is specified. String
BorderWidth bar
line
mixed
pie
The width of the border line (if shown) around bars, pie segments and line areas. A percentage of the standard line width. Default: 100 Numeric
+ve only

Original Data edit

Parameter Chart Types Description Type &
Limits
IncludeOriginalData All
  • auto will include the original data (the SeriesNValues parameters) in the SVG file as comments if the total number of data points is 100 or less, and not include them if there are more then 100.
  • yes will force inclusion of the original data no matter how many data points there are.
  • no will force non-inclusion of the original data no matter how many data points there are.

Default: auto

String

Debug edit

Parameter Chart Types Description Type &
Limits
Debug All A value of parms will show the values of all parameters used in the module. This includes the user-supplied parameters that are recognised, and the default values for all other parameters that have them. It does not include parameters that are not recognised, including because they are mis-spelled or mis-capitalised. String
rem All An existing parameter in a call to this module can be left in the call text but ignored by the module by making it a 'rem' parameter. Eg: a call with HorizontalBarGraph=yes can have the parameter ignored by changing it to rem=HorizontalBarGraph=yes. Switching between using and ignoring the parameter is then just a matter of value of removing or adding 'rem=' before the rest of the parameter text. String

Values Tables edit

Colors edit

These are the defult colors for data series and pie segments. To set any color, use either a number from this table or an SVG color term.

Number Color GreyScale equivalent   Number Color GreyScale equivalent   Number Color GreyScale equivalent
1       11       21    
2       12       22    
3       13       23    
4       14       24    
5       15       25    
6       16       26    
7       17       27    
8       18       28    
9       19       29    
10       20       30    

Markers edit

To set a marker for series <N>, set the Series<N>Marker parameter to one of these numbers.


Dash Patterns edit

To set a dash pattern for series <N>, set the Series<N>Dash parameter to one of these numbers, or to an SVG dash pattern term.


Fill Patterns edit

To set a fill pattern for series <N>, set the Series<N>Pattern parameter to one of these numbers.


Notes edit

SVG Color Terms edit

Colors in SVG files can be specified in a number of ways:

  • as a 3- or 6-digit hexadecimal number, prefixed with'#', eg: #f08, or #ff0088 (which are equivalent).
  • in rgb functional notation, eg: rgb(255, 0, 128) or rgb(100%, 0, 50%) (which are equivalent).
  • as keywords, eg: blue, red. SVG recognises 147 color keywords, which are listed in the SVG specification.

In all cases an SVG color term should be entered in a parameter without surrounding quotes, eg: Series1Color=rgb(100%, 0%, 50%) or Series1Color=blue.

SVG Dash Pattern Terms edit

Dash patterns in SVG are specified by a series of space- or comma-separated numbers, which specify the lengths of the drawn or empty sections of the pattern. Eg: '8,2' specifies a dash of length 8 units, followed by a space of 2 units.

Dash pattern terms should be entered in a parameter without surrounding quotes, eg: Series1Dash=8,2.

Spaces in Parameters edit

Some parameters (especially Series1Values, ...) contain multiple values, separated by spaces. The Series<N>Values parameters are written into the SVG code as comments (subject to the IncludeOriginalData parameter). Eg:

{{#invoke:Charts SVG|lineChart|Series1Values=
 10 90
 20 75
 35 24
 70 62
}}

Groups of spaces and line-breaks are regarded as a single space when separating values.

User-supplied SVG code edit

There a number of parameters where the text of the parameter is written directly to the SVG output, with the expectation it is valid SVG code. If it is not, this will only be known when the SVG file is viewed as an SVG image. This may produce an error, or all or part of the SVG code may be ignored.

The parameters handled this way are:

  • ImageBackgroundSVG
  • ImageForegroundSVG
  • LegendSVG
  • all Color and Dash parameters that are not numeric values

Known Rendering Issues edit

Some SVG elements will not be rendered properly, or at all, when the SVG image is shown on a Wikipedia page. In part this is because the displayed image is in fact a PNG (bitmap) image - the SVG code is not sent to the user's browser.

  • Texts in the SVG may contain web addresses, but be aware the displayed PNG will not contain text that can be either clicked or copied-and-pasted. Actual links will need to be elsewhere.
  • Texts in the SVG should not contain wiki-markup, as it will not be interpreted as such when the SVG image is rendered on a Wikipedia page.
  • It appears the renderer simply ignores SVG xlink: references. Parameters where SVG code is supplied (eg: ImageForegroundSVG) should avoid them.

Examples edit

Consistent with the various chart types, these examples use almost identical data.

{{#invoke:Charts SVG
| barChart
| FileTitle = Charts SVG Example 1 - Simple Bar Chart
| XMax = 6
| XAxisValueStep = 1
| YMax = 160
| Series1Text = 1st W
| Series1Values = 1 70
     2 10
     3 50
     4 10
     5 70
| Series2Text = 2nd W
| Series2Values = 0.8 80
     2.2 20
     2.7 65
     4.5 40
     5.1 90
}}

bar chart with narrow bars
Bar chart without groups, ie: bars can be in any X-position.

{{#invoke:Charts SVG
| lineChart
| FileTitle = Charts SVG Example 2 - Simple Line Chart
| XMax = 6
| XAxisValueStep = 1
| YMax = 160
| Series1Text = 1st W
| Series1Values = 1 70
     2 10
     3 50
     4 10
     5 70
| Series2Text = 2nd W
| Series2Values = 0.8 80
     2.2 20
     2.7 65
     4.5 40
     5.1 90
}}

line chart with one line a 'w', the other a distorted 'w'
'W' is more obvious with lines.

{{#invoke:Charts SVG
| mixedChart
| FileTitle = Charts SVG Example 3 - Simple Mixed Chart
| XMax = 6
| XAxisValueStep = 1
| YMax = 160
| Series1Text = 1st W
| Series1Values = 1 70
     2 10
     3 50
     4 10
     5 70
| Series2Type = bar
| Series2Text = 2nd W
| Series2Values = 0.8 80
     2.2 20
     2.7 65
     4.5 40
     5.1 90
}}

see caption
Mixed chart with one series a line, the other is bars.

{{#invoke:Charts SVG
| scatterChart
| FileTitle = Charts SVG Example 4 - Simple Scatter Chart
| XMax = 6
| XAxisValueStep = 1
| YMax = 160
| Series1Text = 1st W
| Series1Values = 1 70
     2 10
     3 50
     4 10
     5 70
| Series2Text = 2nd W
| Series2Values = 0.8 80
     2.2 20
     2.7 65
     4.5 40
     5.1 90
}}

scatter chart with 2 series of points
Scatter chart - points only.

{{#invoke:Charts SVG
| pieChart
| FileTitle = Charts SVG Example 5 - Simple Pie Chart
| Series1Values = A 80
     B 20
     C 60
     D 20
     E 80
}}

pie chart of 5 sectors

{{#invoke:Charts SVG
| barChart
| FileTitle = Charts SVG Example 6 - Grouped Bar Chart
| XMax = 6
| XAxisValueStep = 1
| YMax = 160
| Series1Text = 1st W
| Series1Values = 1 70
     2 10
     3 50
     4 10
     5 70
| Series2Text = 2nd W
| Series2Values = 1 80
     2 20
     3 65
     4 40
     5 90
| Group1Text = A
| Group2Text = B
| Group3Text = C
| Group4Text = D
| Group5Text = E
}}

bar chart with bars in groups of 2 bars
For bar charts with groups, each group is multiple adjacent bars.

{{#invoke:Charts SVG
| lineChart
| FileTitle = Charts SVG Example 7 - Grouped Line Chart
| XMax = 6
| XAxisValueStep = 1
| YMax = 160
| Series1Text = 1st W
| Series1Values = 1 70
     2 10
     3 50
     4 10
     5 70
| Series2Text = 2nd W
| Series2Values = 1 80
     2 20
     3 65
     4 40
     5 90
| Group1Text = A
| Group2Text = B
| Group3Text = C
| Group4Text = D
| Group5Text = E
}}

see caption
For line charts with groups, each group is a single X-position.

{{#invoke:Charts SVG
| mixedChart
| FileTitle = Charts SVG Example 8 - Grouped Mixed Chart
| YMax = 160
| Series1Text = 1st W
| Series1Values = 1 70
     2 10
     3 50
     4 10
     5 70
| Series2Text = 2nd W
| Series2Type = bar
| Series2Values = 1 80
     2 20
     3 65
     4 40
     5 90
| Series3Text = 3rd W
| Series3Type = bar
| Series3Values = 1 90
     2 15
     3 45
     4 25
     5 85
| Group1Text = A
| Group2Text = B
| Group3Text = C
| Group4Text = D
| Group5Text = E
}}

see caption
One line series and two bar series.

For more complex examples, see User:Innesw/sandbox/further examples.

Development edit

If you have questions or suggestions, share them at Module talk:Charts SVG.

Code

-- Module Charts SVG

local Args, Parms = {}, {}
local SeriesData, OriginalData = {}, {}
local SType, YAxis2, Labels, Color, LineShow, LineWidth, LineDash, Marker, MarkerFill, MarkerSize, FillPattern, FillPatternColor = {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}
local SeriesText, GroupText, ChartText = {}, {}, {}
local SeriesCount, BarSeriesCount, SeriesMaxLen, GroupsCount, ChartTextCount = 0, 0, 0, 0, 0
local AutoDataPointsLimit, DataPointsCount = 100, 0
local DoPie, DoHorizontal, DoGroupsTopDown, DoArea, DoStack, DoStack100, DoYAxis2, DoChartAdjust = false, false, false, false, false, false, false, false
local Msgs = {}

local FontSiz, Siz, Pos, Mult, Adjusts = {}, {}, {}, {}, {}
local ImageWidth, ImageHeight
local GroupWidth, UnitWidth, BarWidth, BarSpace

local BaseUnit, BaseFontSize, BaseLineWidth = 3, 3, 1

local r, tr, t = {}

-- To translate parameters, translate the values in the KeyWords table. Do not translate the keys.
local KeyWords = {
    barChart = 'barChart',
    lineChart = 'lineChart',
    scatterChart = 'scatterChart',
    mixedChart = 'mixedChart',
    pieChart = 'pieChart',

    FileTitle = 'FileTitle',
    FileDesc = 'FileDesc',

    ImagePadding = 'ImagePadding',
    ImagePaddingTop = 'ImagePaddingTop',
    ImagePaddingBottom = 'ImagePaddingBottom',
    ImagePaddingLeft = 'ImagePaddingLeft',
    ImagePaddingRight = 'ImagePaddingRight',

    ImageBackgroundColor = 'ImageBackgroundColor',
    ImageBackgroundSVG = 'ImageBackgroundSVG',
    ImageBorder = 'ImageBorder',
    ImageForegroundSVG = 'ImageForegroundSVG',

    Title = 'Title',
    TitleX = 'TitleX',
    TitleY = 'TitleY',

    Footnote = 'Footnote',
    FootnoteX = 'FootnoteX',
    FootnoteY = 'FootnoteY',

    Area = 'Area',
    Stack = 'Stack',
    Stack100 = 'Stack100',
    HorizontalBarGraph = 'HorizontalBarGraph',
    GroupsTopDown = 'GroupsTopDown',

    ChartWidth = 'ChartWidth',
    ChartHeight = 'ChartHeight',

    ChartAdjust = 'ChartAdjust',

    GreyScale = 'GreyScale',
    GrayScale = 'GrayScale',
    LineWidth = 'LineWidth',
    ChartBackgroundColor = 'ChartBackgroundColor',

    XMin = 'XMin',
    XMax = 'XMax',
    XAxisTitle = 'XAxisTitle',
    XAxisValueStep = 'XAxisValueStep',
    XAxisValueMultiplier = 'XAxisValueMultiplier',
    XAxisValueRound = 'XAxisValueRound',
    XAxisValueAbsolute = 'XAxisValueAbsolute',
    XAxisValuePrefix = 'XAxisValuePrefix',
    XAxisValueSuffix = 'XAxisValueSuffix',
    XAxisValueFormat = 'XAxisValueFormat',
    XAxisValueRotate = 'XAxisValueRotate',
    XAxisValueSpace = 'XAxisValueSpace', 
    XAxisMark2Step = 'XAxisMark2Step',
    XAxisArrows = 'XAxisArrows',

    YMin = 'YMin',
    YMax = 'YMax',
    YAxisTitle = 'YAxisTitle',
    YAxisValueStep = 'YAxisValueStep',
    YAxisValueMultiplier = 'YAxisValueMultiplier',
    YAxisValueRound = 'YAxisValueRound',
    YAxisValueAbsolute = 'YAxisValueAbsolute',
    YAxisValuePrefix = 'YAxisValuePrefix',
    YAxisValueSuffix = 'YAxisValueSuffix',
    YAxisValueFormat = 'YAxisValueFormat',
    YAxisValueRotate = 'YAxisValueRotate',
    YAxisValueSpace = 'YAxisValueSpace', 
    YAxisMark2Step = 'YAxisMark2Step',
    YAxisColor = 'YAxisColor',
    YAxisArrows = 'YAxisArrows',

    Y2Min = 'Y2Min',
    Y2Max = 'Y2Max',
    YAxis2Title = 'YAxis2Title',
    YAxis2ValueStep = 'YAxis2ValueStep',
    YAxis2ValueMultiplier = 'YAxis2ValueMultiplier',
    YAxis2ValueRound = 'YAxis2ValueRound',
    YAxis2ValueAbsolute = 'YAxis2ValueAbsolute',
    YAxis2ValuePrefix = 'YAxis2ValuePrefix',
    YAxis2ValueSuffix = 'YAxis2ValueSuffix',
    YAxis2ValueFormat = 'YAxis2ValueFormat',
    YAxis2ValueRotate = 'YAxis2ValueRotate',
    YAxis2ValueSpace = 'YAxis2ValueSpace', 
    YAxis2Mark2Step = 'YAxis2Mark2Step',
    YAxis2Color = 'YAxis2Color',

    XGrid = 'XGrid',
    YGrid = 'YGrid',

    LegendType = 'LegendType',
    LegendX = 'LegendX',
    LegendY = 'LegendY',
    LegendTextWidth = 'LegendTextWidth',
    LegendBorder = 'LegendBorder',
    LegendSVG = 'LegendSVG',

    FontSize = 'FontSize',
    LabelsFontSize = 'LabelsFontSize',
    TitleFontSize = 'TitleFontSize',
    FootnoteFontSize = 'FootnoteFontSize',
    LegendFontSize = 'LegendFontSize',
    XAxisTitleFontSize = 'XAxisTitleFontSize',
    YAxisTitleFontSize = 'YAxisTitleFontSize',
    YAxis2TitleFontSize = 'YAxis2TitleFontSize',
    XAxisValuesFontSize = 'XAxisValuesFontSize',
    YAxisValuesFontSize = 'YAxisValuesFontSize',
    YAxis2ValuesFontSize = 'YAxis2ValuesFontSize',
    ChartTextFontSize = 'ChartTextFontSize',

    GraphLineWidth = 'GraphLineWidth',

    BarWidth = 'BarWidth',
    BarSpace = 'BarSpace',
    
    PieRadius = 'PieRadius',
    Explode = 'Explode',
    ExplodeRadius = 'ExplodeRadius',
    DoughnutHole = 'DoughnutHole',
    PieStartAngle = 'PieStartAngle',
    PieSweepDir = 'PieSweepDir',

    SegmentText = 'SegmentText',
    SegmentTextWidth = 'SegmentTextWidth',
    SegmentTextRadius = 'SegmentTextRadius',

    BorderColor = 'BorderColor',
    BorderWidth = 'BorderWidth',

    Series = 'Series',
    Segment = 'Segment',

    Type = 'Type',
    Values = 'Values',
    YAxis2 = 'YAxis2',
    Labels = 'Labels',
    Color = 'Color',
    Line = 'Line',
    Width = 'Width',
    Dash = 'Dash',
    Marker = 'Marker',
    MarkerSize = 'MarkerSize',
    MarkerFill = 'MarkerFill',
    Pattern = 'Pattern',
    PatternColor = 'PatternColor',

    Text = 'Text',

    Group = 'Group',
    ChartText = 'ChartText',

    IncludeOriginalData = 'IncludeOriginalData',

    Debug = 'Debug',

    -- these are values that some parameters recognise, and can be translated
    none = 'none',
    -- series types
    bar = 'bar',
    line = 'line',
    -- legend types
    vertical = 'vertical',
    horizontal = 'horizontal',
    -- pie segment texts
    text = 'text',
    value = 'value',
    percent = 'percent',
    -- original data
    auto = 'auto',
    -- debug
    parms = 'parms',
    --
    yes = 'yes',
    no = 'no',
    
    -- rem parameter is accepted but ignored, allows user to rem out a parameter
    rem = 'rem',

    -- these are special values that make the code simpler if they exist in the KeyWords table,
    --  but they *must not* be translated
    X = 'X',
    Y = 'Y',

    }

-- To translate messages, translate the values in the MessageTexts table. Do not translate the keys.
local MessageTexts = {
    MsgHeading = "===== Charts SVG - Messages =====",
    ParmsNotFound = "Unknown Parameters:",
    NotNumeric = "%s: value \"%s\" is not numeric.",
    BelowChartMinSize = "%s: value \"%d\" is below the minimum chart size of %d.",
    NotInGroup = "Series%dValues: pair %d: X value \"%s\" is not for a defined group.",
    NotInMarkers = "Series%dMarker: value \"%s\" is not in the markers table.",
    NotInDashPatterns = "Series%dDash: value \"%s\" is not in the dash patterns table.",
    NotInFillPatterns = "Series%dPattern: value \"%s\" is not in the fill patterns table.",
    NoYAxis2 = "Series%dYAxis2 is set, but no 2nd Y axis is shown - which may be because Stack or Stack100 are set.",
    CopyText = "Select and copy the following text. Paste it into a plain text file. The text file should have an svg extension, for example ''mychart.svg''.",
    }

local DefColor = {
    'rgb(0, 0, 255)',     --  1 = blue
    'rgb(0, 192, 0)',     --  2 = mid green
    'rgb(255, 255, 0)',   --  3 = yellow
    'rgb(255, 0, 0)',     --  4 = red
    'rgb(192, 192, 0)',   --  5 = light olive
    'rgb(255, 0, 255)',   --  6 = magenta
    'rgb(0, 255, 0)',     --  7 = lime
    'rgb(128, 128, 128)', --  8 = grey
    'rgb(0, 255, 255)',   --  9 = cyan
    'rgb(255, 165, 0)',   -- 10 = orange
    'rgb(0, 0, 192)',     -- 11 = light navy
    'rgb(128, 255, 128)', -- 12 = light green
    'rgb(255, 255, 128)', -- 13 = sand yellow
    'rgb(192, 0, 0)',     -- 14 = red brown
    'rgb(240, 240, 240)', -- 15 = pale grey
    'rgb(255, 128, 255)', -- 16 = light magenta
    'rgb(192, 255, 192)', -- 17 = pale green
    'rgb(192, 0, 192)',   -- 18 = dark mauve
    'rgb(192, 192, 255)', -- 19 = blue-grey
    'rgb(0, 192, 192)',   -- 20 = dark cyan
    'rgb(192, 192, 192)', -- 21 = light grey
    'rgb(128, 128, 255)', -- 22 = dark blue-grey
    'rgb(0, 128, 0)',     -- 23 = green
    'rgb(255, 255, 192)', -- 24 = light sand
    'rgb(255, 192, 192)', -- 25 = pale red
    'rgb(128, 128, 0)',   -- 26 = olive
    'rgb(255, 192, 255)', -- 27 = pale magenta
    'rgb(255, 96, 0)',    -- 28 = dark orange
    'rgb(192, 255, 255)', -- 29 = light cyan
    'rgb(255, 128, 128)', -- 30 = light red
    }

-- grayscale equivalents of the above colours
local GrayColor = {
    'rgb(18, 18, 18)',    --  1 = blue-g
    'rgb(137, 137, 137)', --  2 = midgreen-g
    'rgb(237, 237, 237)', --  3 = yellow-g
    'rgb(54, 54, 54)',    --  4 = red-g
    'rgb(178, 178, 178)', --  5 = lightolive-g
    'rgb(73, 73, 73)',    --  6 = magenta-g
    'rgb(182, 182 ,182)', --  7 = lime-g
    'rgb(128, 128, 128)', --  8 = grey-g
    'rgb(201, 201 ,201)', --  9 = cyan-g
    'rgb(172, 172 172)',  -- 10 = orange-g
    'rgb(14, 14 ,14)',    -- 11 = lightnavy-g
    'rgb(219, 219, 219)', -- 12 = lightgreen-g
    'rgb(246, 246, 246)', -- 13 = sandyellow-g
    'rgb(41, 41, 41)',    -- 14 = redbrown-g
    'rgb(240, 240, 240)', -- 15 = palegrey-g
    'rgb(164, 164, 164)', -- 16 = lightmagenta-g
    'rgb(237, 237, 237)', -- 17 = palegreen-g
    'rgb(55, 55, 55)',    -- 18 = darkmauve-g
    'rgb(197, 197, 197)', -- 19 = bluegrey-g
    'rgb(151, 151, 151)', -- 20 = darkcyan-g
    'rgb(192, 192, 192)', -- 21 = lightgrey-g
    'rgb(137, 137, 137)', -- 22 = darkbluegrey-g 
    'rgb(92, 92, 92)',    -- 23 = green-g
    'rgb(250, 250, 250)', -- 24 = lightsand-g
    'rgb(205, 205, 205)', -- 25 = palered-g
    'rgb(119, 119, 119)', -- 26 = olive-g
    'rgb(210, 210, 210)', -- 27 = palemagenta-g
    'rgb(123, 123, 123)', -- 28 = darkorange-g
    'rgb(242, 242, 242)', -- 29 = lightcyan-g
    'rgb(155, 155, 155)'  -- 30 = lightred-g
    }

local DashPattern = {
    "8,8",            -- 1 = dashed, long
    "4,4",            -- 2 = dashed, short
    "8,2",            -- 3 = broken, long
    "4,2",            -- 4 = broken, short
    "2,2",            -- 5 = dots
    "6,2,2,2",        -- 6 = dash-dot
    "6,2,2,2,2,2",    -- 7 = dash-dot-dot
    "6,2,2,2,2,2,2,2" -- 8 = dash-dot-dot-dot
    }

--[[
The differences between the defaults for the 4 'graph' chart methods are:
                  barChart     lineChart     scatterChart     mixedChart
series type       bar*         line*         line*            must be specified
line visibility   -*           yes           none             yes
marker            -*           nil           series no.       nil

group             allowed      allowed       -*               allowed

area              -*           nil           -*               -*
stack             nil          nil           -*               -*
stack100          nil          nil           -*               -*
     (* = cannot be changed by parameters)
]]    

function barChart(frame)

    Args = frame.args

    getAllParms()

    DoStack = (Parms["Stack"] ~= nil)
    DoStack100 = (Parms["Stack100"] ~= nil)
    DoYAxis2 = (Parms["Y2Max"] ~= nil)
    DoChartAdjust = (Parms["ChartAdjust"] ~= nil)

    DoHorizontal = (Parms["HorizontalBarGraph"] ~= nil)
    DoGroupsTopDown = (Parms["HorizontalBarGraph"] ~= nil) and (Parms["GroupsTopDown"] ~= nil)

    -- fill internal tables

    for i = 1, SeriesCount do
        SeriesData[i] = {}
        transfer(Parms["Series" .. i .. "Values"], SeriesData[i], 2)
        DataPointsCount = DataPointsCount + #SeriesData[i]
        SType[i] = KeyWords["bar"]
    end
    copyTable(SeriesData, OriginalData) -- preserve original data
    BarSeriesCount = SeriesCount

    build("Series", "YAxis2", SeriesCount, YAxis2, nil, nil, nil)

    build("Series", "Labels", SeriesCount, Labels, nil, nil, nil)

    build("Series", "Color", SeriesCount, Color, nil, nil, DefColor)

    build("Series", "Pattern", SeriesCount, FillPattern, KeyWords["none"], nil, nil)
    build("Series", "PatternColor", SeriesCount, FillPatternColor, nil, nil, nil)

    build("Series", "Text", SeriesCount, SeriesText, nil, nil, nil)

    build("Group", "Text", GroupsCount, GroupText, nil, "Group ", nil)

    build("ChartText", "", ChartTextCount, ChartText, nil, nil, nil)

    if DoStack or DoStack100 then
        calcStack()
        DoYAxis2 = false
    end

    if not checkParms() then
        -- messages, instead of SVG output
        return table.concat(r, "\n")
    end

    if not outputDebugInfo() then
        -- stop after debug output
        return table.concat(r, "\n")
    end

    -- build the SVG

    prelimsCommon()
    prelimsAxes()

    commonTop()

    codeAxisGrids()

    stylesAreas()
    defsAreas()
    elementsGraphs()

    codeAxes()

    commonBottom()

    return table.concat(r, "\n")
end

function lineChart(frame)

    Args = frame.args

    getAllParms()

    DoArea = (Parms["Area"] ~= nil)
    DoStack = (Parms["Stack"] ~= nil)
    DoStack100 = (Parms["Stack100"] ~= nil)
    DoYAxis2 = (Parms["Y2Max"] ~= nil)
    DoChartAdjust = (Parms["ChartAdjust"] ~= nil)

    -- fill internal tables

    for i = 1, SeriesCount do
        SeriesData[i] = {}
        transfer(Parms["Series" .. i .. "Values"], SeriesData[i], 2)
        DataPointsCount = DataPointsCount + #SeriesData[i]
        SType[i] = KeyWords["line"]
    end
    copyTable(SeriesData, OriginalData) -- preserve original data
    BarSeriesCount = 0

    build("Series", "YAxis2", SeriesCount, YAxis2, nil, nil, nil)

    build("Series", "Labels", SeriesCount, Labels, nil, nil, nil)

    build("Series", "Color", SeriesCount, Color, nil, nil, DefColor) 

    build("Series", "Line", SeriesCount, LineShow, "yes", nil, nil) -- the default line visibility is 'yes'
    build("Series", "Width", SeriesCount, LineWidth, Parms["GraphLineWidth"], nil, nil)
    build("Series", "Dash", SeriesCount, LineDash, KeyWords["none"], nil, nil)

    build("Series", "Marker", SeriesCount, Marker, nil, nil, nil) -- the default marker type is nil
    build("Series", "MarkerSize", SeriesCount, MarkerSize, 100, nil, nil)
    build("Series", "MarkerFill", SeriesCount, MarkerFill, nil, nil, Color)

    build("Series", "Pattern", SeriesCount, FillPattern, KeyWords["none"], nil, nil)
    build("Series", "PatternColor", SeriesCount, FillPatternColor, nil, nil, nil)

    build("Series", "Text", SeriesCount, SeriesText, nil, nil, nil)

    build("Group", "Text", GroupsCount, GroupText, nil, "Group ", nil)

    build("ChartText", "", ChartTextCount, ChartText, nil, nil, nil)

    if DoStack or DoStack100 then
        calcStack()
        DoArea = true
        DoYAxis2 = false
    end

    if not checkParms() then
        -- messages, instead of SVG output
        return table.concat(r, "\n")
    end

    if not outputDebugInfo() then
        -- stop after debug output
        return table.concat(r, "\n")
    end

    -- build the SVG

    prelimsCommon()
    prelimsAxes()

    commonTop()

    codeAxisGrids()

    if DoArea then
        stylesAreas()
        defsAreas()
    else
        stylesLines()
        defsMarkers()
    end
    elementsGraphs()

    codeAxes()

    commonBottom()

    return table.concat(r, "\n")
end

function scatterChart(frame)

    Args = frame.args

    getAllParms()

    DoYAxis2 = (Parms["Y2Max"] ~= nil)
    DoChartAdjust = (Parms["ChartAdjust"] ~= nil)

    -- fill internal tables

    for i = 1, SeriesCount do
        SeriesData[i] = {}
        transfer(Parms["Series" .. i .. "Values"], SeriesData[i], 2)
        DataPointsCount = DataPointsCount + #SeriesData[i]
        SType[i] = KeyWords["line"]
    end
    copyTable(SeriesData, OriginalData) -- preserve original data
    BarSeriesCount = 0

    build("Series", "YAxis2", SeriesCount, YAxis2, nil, nil, nil)

    build("Series", "Labels", SeriesCount, Labels, nil, nil, nil)

    build("Series", "Color", SeriesCount, Color, DefColor, nil, nil)

    build("Series", "Line", SeriesCount, LineShow, KeyWords["none"], nil, nil) -- the default line visibility is 'none'
    build("Series", "Width", SeriesCount, LineWidth, Parms["GraphLineWidth"], nil, nil)
    build("Series", "Dash", SeriesCount, LineDash, KeyWords["none"], nil, nil)

    build("Series", "Marker", SeriesCount, Marker, nil, true, nil) -- the default marker type is the series number
    build("Series", "MarkerSize", SeriesCount, MarkerSize, 100, nil, nil)
    build("Series", "MarkerFill", SeriesCount, MarkerFill, nil, nil, Color)

    build("Series", "Text", SeriesCount, SeriesText, nil, nil, nil)

    build("ChartText", "", ChartTextCount, ChartText, nil, nil, nil)

    if not checkParms() then
        -- messages, instead of SVG output
        return table.concat(r, "\n")
    end

    if not outputDebugInfo() then
        -- stop after debug output
        return table.concat(r, "\n")
    end

    -- build the SVG

    prelimsCommon()
    prelimsAxes()

    commonTop()

    codeAxisGrids()

    stylesLines()
    defsMarkers()
    elementsGraphs()

    codeAxes()

    commonBottom()

    return table.concat(r, "\n")
end

function mixedChart(frame)

    Args = frame.args

    getAllParms()

    DoYAxis2 = (Parms["Y2Max"] ~= nil)
    DoChartAdjust = (Parms["ChartAdjust"] ~= nil)

    -- fill internal tables

    build("Series", "Type", SeriesCount, SType, "line", nil, nil)
    for i = 1, SeriesCount do
        if SType[i] == KeyWords["bar"] then
            BarSeriesCount = BarSeriesCount + 1
        end
    end

    for i = 1, SeriesCount do
        SeriesData[i] = {}
        transfer(Parms["Series" .. i .. "Values"], SeriesData[i], 2)
        DataPointsCount = DataPointsCount + #SeriesData[i]
    end
    copyTable(SeriesData, OriginalData) -- preserve original data

    build("Series", "YAxis2", SeriesCount, YAxis2, nil, nil, nil)

    build("Series", "Labels", SeriesCount, Labels, nil, nil, nil)

    build("Series", "Color", SeriesCount, Color, nil, nil, DefColor)

    build("Series", "Line", SeriesCount, LineShow, "yes", nil, nil) -- the default line visibility is 'yes'
    build("Series", "Width", SeriesCount, LineWidth, Parms["GraphLineWidth"], nil, nil)
    build("Series", "Dash", SeriesCount, LineDash, KeyWords["none"], nil, nil)

    build("Series", "Marker", SeriesCount, Marker, nil, nil, nil) -- the default marker type is nil
    build("Series", "MarkerSize", SeriesCount, MarkerSize, 100, nil, nil)
    build("Series", "MarkerFill", SeriesCount, MarkerFill, nil, nil, Color)

    build("Series", "Pattern", SeriesCount, FillPattern, KeyWords["none"], nil, nil)
    build("Series", "PatternColor", SeriesCount, FillPatternColor, nil, nil, nil)

    build("Series", "Text", SeriesCount, SeriesText, nil, nil, nil)

    build("Group", "Text", GroupsCount, GroupText, nil, "Group ", nil)

    build("ChartText", "", ChartTextCount, ChartText, nil, nil, nil)

    if not checkParms() then
        -- messages, instead of SVG output
        return table.concat(r, "\n")
    end

    if not outputDebugInfo() then
        -- stop after debug output
        return table.concat(r, "\n")
    end

    -- build the SVG

    prelimsCommon()
    prelimsAxes()

    commonTop()

    codeAxisGrids()

    stylesAreas()
    defsAreas()

    stylesLines()
    defsMarkers()

    elementsGraphs()

    codeAxes()

    commonBottom()

    return table.concat(r, "\n")
end

function pieChart(frame)

    Args = frame.args

    getAllParms()
    GroupsCount = 0 -- ensures any GroupNText parameters are ignored

    -- fill internal tables

    -- only transfer series 1
    if SeriesCount > 0 then
        transfer(Parms["Series" .. 1 .. "Values"], SeriesData, 2)
        DataPointsCount = DataPointsCount + #SeriesData
    end
    copyTable(SeriesData, OriginalData) -- preserve original data

    -- get segment parms for each element in the series
    -- we do this here because transfer() (above) is where SeriesMaxLen is calculated
    for i = 1, SeriesMaxLen do
        Parms["Segment" .. i .. "Color"] = checkColor(getParm("Segment", i, "Color", "s", iif(Parms["GrayScale"] ~= nil, GrayColor[i], DefColor[i])))

        Parms["Segment" .. i .. "Pattern"] = getParm("Segment", i, "Pattern", "n")
        if Parms["Segment" .. i .. "Pattern"] ~= nil then
            Parms["Segment" .. i .. "PatternColor"] = checkColor(getParm("Segment", i, "PatternColor", "s", iif(Parms["GrayScale"] ~= nil, "white", "black")))
        end
    end

    build("Segment", "Color", SeriesMaxLen, Color, nil, nil, iif(Parms["GrayScale"] ~= nil, GrayColor, DefColor))
    build("Segment", "Pattern", SeriesMaxLen, FillPattern, KeyWords["none"], nil, nil)
    build("Segment", "PatternColor", SeriesMaxLen, FillPatternColor, nil, nil, nil)

    build("ChartText", "", ChartTextCount, ChartText, nil, nil, nil)

    -- transfer the first value in each SeriesData pair to table SeriesText, for the legend
    for k, v in ipairs(SeriesData) do
        SeriesText[k] = v[1]
    end

    SeriesCount = SeriesMaxLen

    DoPie = true

    if not checkParms() then
        -- messages, instead of SVG output
        return table.concat(r, "\n")
    end

    if not outputDebugInfo() then
        -- stop after debug output
        return table.concat(r, "\n")
    end

    -- build the SVG

    prelimsCommon()
    prelimsPie()

    commonTop()

    stylesAreas()
    defsAreas()
    elementsPie()

    commonBottom()

    return table.concat(r, "\n")
end

function getAllParms()
    -- get values for all parameters

    -- Note that not all parameters have a default value, ie: it is valid for some parameters to be nil. These are generally switches for some behaviour.

    -- SVG file metadata

    Parms["FileTitle"] = getParm("FileTitle", nil, nil, "s", "SVG Chart")
    Parms["FileDesc"] = getParm("FileDesc", nil, nil, "s", "SVG chart generated by Charts SVG")

    -- general image

    Parms["ImagePadding"] = getParm("ImagePadding", nil, nil, "n") -- switch for replacing default value with user setting for the size of the padding for all 4 spaces
    Parms["ImagePaddingTop"] = getParm("ImagePaddingTop", nil, nil, "n", Parms["ImagePadding"]) -- switch for replacing default value with user setting for the size of the top padding
    Parms["ImagePaddingBottom"] = getParm("ImagePaddingBottom", nil, nil, "n", Parms["ImagePadding"]) -- switch for replacing default value with user setting for the size of the bottom padding
    Parms["ImagePaddingLeft"] = getParm("ImagePaddingLeft", nil, nil, "n", Parms["ImagePadding"]) -- switch for replacing default value with user setting for the size of the left padding
    Parms["ImagePaddingRight"] = getParm("ImagePaddingRight", nil, nil, "n", Parms["ImagePadding"]) -- switch for replacing default value with user setting for the size of the right padding

    Parms["ImageBorder"] = checkColor(getParm("ImageBorder", nil, nil, "s", KeyWords["none"]))

    Parms["ImageBackgroundColor"] = checkColor(getParm("ImageBackgroundColor", nil, nil, "s", "white"))

    Parms["ImageBackgroundSVG"] = getParm("ImageBackgroundSVG", nil, nil, "s") -- switch for background SVG for the image

    Parms["ImageForegroundSVG"] = getParm("ImageForegroundSVG", nil, nil, "s") -- switch for foreground SVG for the image

    -- display title

    Parms["Title"] = getParm("Title", nil, nil, "s") -- switch to show title text
    Parms["TitleX"] = getParm("TitleX", nil, nil, "n") -- switch to move title from default position
    Parms["TitleY"] = getParm("TitleY", nil, nil, "n", 0)

    -- footnote

    Parms["Footnote"] = getParm("Footnote", nil, nil, "s") -- switch for footnote text
    Parms["FootnoteX"] = getParm("FootnoteX", nil, nil, "n") -- switch to move footnote from default position
    Parms["FootnoteY"] = getParm("FootnoteY", nil, nil, "n", 0)

    -- general chart

    Parms["Area"] = getParm("Area", nil, nil, "s") -- switch for area graphs
    Parms["Stack"] = getParm("Stack", nil, nil, "s") -- switch for stacked graphs
    Parms["Stack100"] = getParm("Stack100", nil, nil, "s") -- switch for stacked-to-100% graphs

    Parms["HorizontalBarGraph"] = getParm("HorizontalBarGraph", nil, nil, "s") -- switch for horizontal bar graphs
    Parms["GroupsTopDown"] = getParm("GroupsTopDown", nil, nil, "s") -- switch for showing groups (and series) in top-down order on horizontal bar graphs 

    Parms["ChartWidth"] = getParm("ChartWidth", nil, nil, "n", 500)
    Parms["ChartHeight"] = getParm("ChartHeight", nil, nil, "n", 350)

    if Parms["ChartWidth"] < 200 then
        table.insert(Msgs, string.format(MessageTexts.BelowChartMinSize, "ChartWidth", Parms["ChartWidth"], 200))
    end
    if Parms["ChartHeight"] < 200 then
        table.insert(Msgs, string.format(MessageTexts.BelowChartMinSize, "ChartHeight", Parms["ChartHeight"], 200))
    end

    Parms["ChartAdjust"] = getParm("ChartAdjust", nil, nil, "s") -- switch for automatic chart size adjustment

    Parms["GreyScale"] = getParm("GreyScale", nil, nil, "s")
    Parms["GrayScale"] = getParm("GrayScale", nil, nil, "s", Parms["GreyScale"]) -- switch for grayscale instead of colours

    Parms["LineWidth"] = getParm("LineWidth", nil, nil, "n", 100)

    Parms["ChartBackgroundColor"] = checkColor(getParm("ChartBackgroundColor", nil, nil, "s")) -- switch for a background color for the chart

    -- XAxis

    Parms["XMin"] = getParm("XMin", nil, nil, "n", 0)
    Parms["XMax"] = getParm("XMax", nil, nil, "n", 100)

    Parms["XAxisTitle"] = getParm("XAxisTitle", nil, nil, "s") -- switch to show X axis title

    Parms["XAxisValueStep"] = getParm("XAxisValueStep", nil, nil, "n", 10)

    Parms["XAxisValueMultiplier"] = getParm("XAxisValueMultiplier", nil, nil, "n", 1)
    Parms["XAxisValueRound"] = getParm("XAxisValueRound", nil, nil, "n", decPlaces(Parms["XAxisValueStep"]))
    Parms["XAxisValueAbsolute"] = getParm("XAxisValueAbsolute", nil, nil, "s") -- switch for absolute values display

    Parms["XAxisValuePrefix"] = getParm("XAxisValuePrefix", nil, nil, "s", "", "_", " ")
    Parms["XAxisValueSuffix"] = getParm("XAxisValueSuffix", nil, nil, "s", "", "_", " ")

    Parms["XAxisValueFormat"] = getParm("XAxisValueFormat", nil, nil, "s") -- switch to force values formatting on or off
    Parms["XAxisValueRotate"] = getParm("XAxisValueRotate", nil, nil, "n") -- switch to rotate x-axis values

    Parms["XAxisValueSpace"] = getParm("XAxisValueSpace", nil, nil, "n") -- switch to set x-axis values space

    Parms["XAxisMark2Step"] = getParm("XAxisMark2Step", nil, nil, "n") -- switch for showing x-axis secondary marks

    Parms["XAxisArrows"] = getParm("XAxisArrows", nil, nil, "s") -- switch for showing x-axis arrows

    -- YAxis

    if Parms["Stack100"] ~= nil then
        Parms["YMin"] = 0
        Parms["YMax"] = 100
    else
        Parms["YMin"] = getParm("YMin", nil, nil, "n", 0)
        Parms["YMax"] = getParm("YMax", nil, nil, "n", 100)
    end

    Parms["YAxisTitle"] = getParm("YAxisTitle", nil, nil, "s") -- switch to show Y axis title

    Parms["YAxisValueStep"] = getParm("YAxisValueStep", nil, nil, "n", 10)

    Parms["YAxisValueMultiplier"] = getParm("YAxisValueMultiplier", nil, nil, "n", 1)
    Parms["YAxisValueRound"] = getParm("YAxisValueRound", nil, nil, "n", decPlaces(Parms["YAxisValueStep"]))
    Parms["YAxisValueAbsolute"] = getParm("YAxisValueAbsolute", nil, nil, "s") -- switch for absolute values display

    Parms["YAxisValuePrefix"] = getParm("YAxisValuePrefix", nil, nil, "s", "", "_", " ")
    if Parms["Stack100"] ~= nil then
        Parms["YAxisValueSuffix"] = getParm("YAxisValueSuffix", nil, nil, "s", "%", "_", " ")
    else
        Parms["YAxisValueSuffix"] = getParm("YAxisValueSuffix", nil, nil, "s", "", "_", " ")
    end

    Parms["YAxisValueFormat"] = getParm("YAxisValueFormat", nil, nil, "s") -- switch to force values formatting on or off
    Parms["YAxisValueRotate"] = getParm("YAxisValueRotate", nil, nil, "n") -- switch to rotate y-axis values

    Parms["YAxisValueSpace"] = getParm("YAxisValueSpace", nil, nil, "n") -- switch to set y-axis values space

    Parms["YAxisMark2Step"] = getParm("YAxisMark2Step", nil, nil, "n") -- switch for showing y-axis secondary marks

    Parms["YAxisColor"] = checkColor(getParm("YAxisColor", nil, nil, "s", "black"))

    Parms["YAxisArrows"] = getParm("YAxisArrows", nil, nil, "s") -- switch for showing y-axis arrows

    -- YAxis2

    Parms["YAxis2Title"] = getParm("YAxis2Title", nil, nil, "s") -- switch to show y-axis-2 title

    Parms["Y2Min"] = getParm("Y2Min", nil, nil, "n", 0)
    Parms["Y2Max"] = getParm("Y2Max", nil, nil, "n") -- switch to show y-axis-2

    Parms["YAxis2ValueStep"] = getParm("YAxis2ValueStep", nil, nil, "n", 10)

    Parms["YAxis2ValueMultiplier"] = getParm("YAxis2ValueMultiplier", nil, nil, "n", 1)
    Parms["YAxis2ValueRound"] = getParm("YAxis2ValueRound", nil, nil, "n", decPlaces(Parms["YAxis2ValueStep"]))
    Parms["YAxis2ValueAbsolute"] = getParm("YAxis2ValueAbsolute", nil, nil, "s") -- switch for absolute values display

    Parms["YAxis2ValuePrefix"] = getParm("YAxis2ValuePrefix", nil, nil, "s", "", "_", " ")
    Parms["YAxis2ValueSuffix"] = getParm("YAxis2ValueSuffix", nil, nil, "s", "", "_", " ")

    Parms["YAxis2ValueFormat"] = getParm("YAxis2ValueFormat", nil, nil, "s") -- switch to force values formatting on or off
    Parms["YAxis2ValueRotate"] = getParm("YAxis2ValueRotate", nil, nil, "n") -- switch to rotate y-axis-2 values

    Parms["YAxis2ValueSpace"] = getParm("YAxis2ValueSpace", nil, nil, "n") -- switch to set y-axis-2 values space

    Parms["YAxis2Mark2Step"] = getParm("YAxis2Mark2Step", nil, nil, "n") -- switch for showing y-axis-2 secondary marks

    Parms["YAxis2Color"] = checkColor(getParm("YAxis2Color", nil, nil, "s", "black"))

    -- grid lines

    Parms["XGrid"] = getParm("XGrid", nil, nil, "s", Parms["XAxisValueStep"])
    if Parms["XGrid"] ~= KeyWords["none"] then
        Parms["XGrid"] = tonumber(Parms["XGrid"])
    end

    Parms["YGrid"] = getParm("YGrid", nil, nil, "s", Parms["YAxisValueStep"])
    if Parms["YGrid"] ~= KeyWords["none"] then
        Parms["YGrid"] = tonumber(Parms["YGrid"])
    end

    -- legend

    Parms["LegendType"] = getParm("LegendType", nil, nil, "s", KeyWords["vertical"])
    i = 1
    Parms["Series" .. i .. "Text"] = getParm("Series", i, "Text", "s") -- switch for showing a legend for series N
    while Parms["Series" .. i .. "Text"] ~= nil do
        i = i + 1
        Parms["Series" .. i .. "Text"] = getParm("Series", i, "Text", "s") -- switch for showing a legend for series N
    end

    Parms["LegendX"] = getParm("LegendX", nil, nil, "n") -- switch for moving the legend from the default position
    Parms["LegendY"] = getParm("LegendY", nil, nil, "n", 0)
    Parms["LegendTextWidth"] = getParm("LegendTextWidth", nil, nil, "n", 100)
    Parms["LegendBorder"] = checkColor(getParm("LegendBorder", nil, nil, "s", "black"))
    Parms["LegendSVG"] = getParm("LegendSVG", nil, nil, "s") -- switch for replacing all legend code

    -- font sizes

    Parms["FontSize"] = getParm("FontSize", nil, nil, "n", 100)
    Parms["TitleFontSize"] = getParm("TitleFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["FootnoteFontSize"] = getParm("FootnoteFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["LegendFontSize"] = getParm("LegendFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["XAxisTitleFontSize"] = getParm("XAxisTitleFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["YAxisTitleFontSize"] = getParm("YAxisTitleFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["YAxis2TitleFontSize"] = getParm("YAxis2TitleFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["XAxisValuesFontSize"] = getParm("XAxisValuesFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["YAxisValuesFontSize"] = getParm("YAxisValuesFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["YAxis2ValuesFontSize"] = getParm("YAxis2ValuesFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["ChartTextFontSize"] = getParm("ChartTextFontSize", nil, nil, "n", Parms["FontSize"])
    Parms["LabelsFontSize"] = getParm("LabelsFontSize", nil, nil, "n", Parms["FontSize"])

    -- bar width & spacing

    Parms["BarWidth"] = getParm("BarWidth", nil, nil, "n", 20)
    Parms["BarSpace"] = getParm("BarSpace", nil, nil, "n", 0) -- % of bar width

    -- general graph line width
    Parms["GraphLineWidth"] = getParm("GraphLineWidth", nil, nil, "n", 100)

    -- pie chart

    Parms["PieRadius"] = getParm("PieRadius", nil, nil, "n", 200)
    Parms["Explode"] = getParm("Explode", nil, nil, "s") -- switch for exploding some or all pie segments
    if Parms["Explode"] ~= nil then
        if tonumber(Parms["Explode"]) ~= nil then
            Parms["Explode"] = tonumber(Parms["Explode"])
            -- default explode radius is 20%
            Parms["ExplodeRadius"] = getParm("ExplodeRadius", nil, nil, "n", 20)
        else
            -- any non-numeric value = all, default explode radius is 10%
            Parms["ExplodeRadius"] =  getParm("ExplodeRadius", nil, nil, "n", 10)
        end
    end
    
    -- Parms[] SegmentNColor, SegmentNPattern and SegmentNPatternColor are done in pieChart()

    Parms["SegmentText"] = getParm("SegmentText", nil, nil, "s") -- switch for showing series text and/or values on pie segments
    Parms["SegmentTextWidth"] = getParm("SegmentTextWidth", nil, nil, "n", 100)
    Parms["SegmentTextRadius"] = getParm("SegmentTextRadius", nil, nil, "n", 105)

    Parms["DoughnutHole"] = getParm("DoughnutHole", nil, nil, "s") -- switch for a doughnut chart
    if Parms["DoughnutHole"] ~= nil then
        if tonumber(Parms["DoughnutHole"]) ~= nil then
            -- if DoughnutHole is a number, ensure it is of numeric type
            Parms["DoughnutHole"] = tonumber(Parms["DoughnutHole"])
        else
            -- any other value, hole size is 50%
            Parms["DoughnutHole"] = 50
        end
    end
    Parms["PieStartAngle"] = getParm("PieStartAngle", nil, nil, "n", 0) -- move the start angle

    Parms["PieSweepDir"] = getParm("PieSweepDir", nil, nil, "s", "AntiClockwise") -- any value not "AntiClockwise" will switch the sweep direction

    -- bar and pie-segment borders

    Parms["BorderColor"] = checkColor(getParm("BorderColor", nil, nil, "s")) -- switch for showing borders on bars
    Parms["BorderWidth"] = getParm("BorderWidth", nil, nil, "n", 100) -- % of standard line width

    -- series

    i = 1
    Parms["Series" .. i .. "Values"] = getParm("Series", i, "Values", "s") -- switch for showing a series
    while Parms["Series" .. i .. "Values"] ~= nil do
        Parms["Series" .. i .. "Type"] = getParm("Series", i, "Type", "s")
        Parms["Series" .. i .. "YAxis2"] = getParm("Series", i, "YAxis2") -- switch for series to use YAxis2 as scale for Y values
        Parms["Series" .. i .. "Labels"] = getParm("Series", i, "Labels") -- switch for series to show data labels
        Parms["Series" .. i .. "Color"] = checkColor(getParm("Series", i, "Color", "s", iif(Parms["GrayScale"] ~= nil, GrayColor[i], DefColor[i])))

        Parms["Series" .. i .. "Line"] = getParm("Series", i, "Line", "s")
        Parms["Series" .. i .. "Width"] = getParm("Series", i, "Width", "n", Parms["GraphLineWidth"])
        Parms["Series" .. i .. "Dash"] = getParm("Series", i, "Dash", "s", KeyWords["none"])
        t = tonumber(Parms["Series" .. i .. "Dash"])
        if t ~= nil and t <= #DashPattern then
            -- if SeriesNDash is a number in the table of default dashes, use the dash pattern for that number
            Parms["Series" .. i .. "Dash"] = DashPattern[t]
        end

        Parms["Series" .. i .. "Marker"] = getParm("Series", i, "Marker", "s") -- switch for markers on the graph
        if tonumber(Parms["Series" .. i .. "Marker"]) ~= nil then
            -- if SeriesNMarker is a number, ensure it is of numeric type
            Parms["Series" .. i .. "Marker"] = tonumber(Parms["Series" .. i .. "Marker"])
        end
        Parms["Series" .. i .. "MarkerSize"] = getParm("Series", i, "MarkerSize", "n", 100)
        Parms["Series" .. i .. "MarkerFill"] = checkColor(getParm("Series", i, "MarkerFill", "s"))

        Parms["Series" .. i .. "Pattern"] = getParm("Series", i, "Pattern", "n")
        if Parms["Series" .. i .. "Pattern"] ~= nil then
            Parms["Series" .. i .. "PatternColor"] = checkColor(getParm("Series", i, "PatternColor", "s", iif(Parms["GrayScale"] ~= nil, "white", "black")))
        end
        SeriesCount = SeriesCount + 1

        i = i + 1
        Parms["Series" .. i .. "Values"] = getParm("Series", i, "Values", "s") -- switch for showing a series
    end

    -- groups

    i = 1
    Parms["Group" .. i .. "Text"] = getParm("Group", i, "Text", "s") -- switch for grouping values
    while Parms["Group" .. i .. "Text"] ~= nil do
        GroupsCount = GroupsCount + 1

        i = i + 1
        Parms["Group" .. i .. "Text"] = getParm("Group", i, "Text", "s")
    end
    if GroupsCount > 0 then
        Parms["XMax"] = GroupsCount
    end
    
    -- chart texts

    i = 1
    Parms["ChartText" .. i] = getParm("ChartText", i, nil, "s") -- switch for showing chart text N
    while Parms["ChartText" .. i] ~= nil do 
        Parms["ChartText" .. i .. "X"] = getParm("ChartText", i, "X", "n", 0)
        Parms["ChartText" .. i .. "Y"] = getParm("ChartText", i, "Y", "n", 0)
        ChartTextCount = ChartTextCount + 1

        i = i + 1
        Parms["ChartText" .. i] = getParm("ChartText", i, nil, "s") -- switch for showing chart text N
    end

    Parms["IncludeOriginalData"] = getParm("IncludeOriginalData", nil, nil, "s", KeyWords["auto"])

    -- debug

    Parms["Debug"] = getParm("Debug", nil, nil, "s") -- switch to run debug code

end

function getParm(s1, num, s2, typ, def, subst, with)
    -- returns the value of a named parameter (of a specified type) or a default
    -- the name may be built from multiple parts (s1, num, s2)

    local s = KeyWords[s1]
    if num ~= nil then
        s = s .. num
    end
    if s2 ~= nil then
        s = s .. KeyWords[s2]
    end

    local result
    if Args[s] ~= nil then
        result = Args[s]
        if typ == "n" then
            result = tonumber(result)
            if result == nil then
                table.insert(Msgs, string.format(MessageTexts.NotNumeric, s, Args[s]))
            end
        end
    else
        result = def
    end
    -- optional substitution of characters within the parameter value
    if result ~= nil and subst and with then
        result = mw.ustring.gsub(result, subst, with)
    end
    return result
end

function checkColor(p)
    -- checks if p is numeric and in the colors table(s)
    -- if so returns the color from the table
    -- otherwise returns the original parameter value

    if p == nil then
        return nil
    end
    local t = tonumber(p)
    if t ~= nil and t <= #DefColor then
        return iif(Parms["GrayScale"] ~= nil, GrayColor[t], DefColor[t])
    else
        return p
    end
end

function transfer(Parm, Tab, SetSize)
    -- transfer values from a space-delimited parameter to a table
    local i = 0
    Parm = mw.text.trim(Parm)
    if SetSize > 1 then
        local x = SetSize + 1 -- to force new table for first time
        for s in mw.text.gsplit(Parm, "%s+") do
            if x > SetSize then
                i = i + 1
                Tab[i] = {}
                x = 1
            end
            Tab[i][x] = s
            x = x + 1
        end
    else
        for s in mw.text.gsplit(Parm, "%s+") do
            i = i + 1
            Tab[i] = s
        end
    end
    SeriesMaxLen = math.max(SeriesMaxLen, i)
end

function build(ParmStart, ParmEnd, Size, Tab, DefaultValue, UseI, DefaultTable)
    -- builds table of specified size from a series of StartNEnd parameters
    -- any nil parameters in the sequence may be filled with a default value, or i (with a prefix if set), or a value from a default table
    for i = 1, Size do
        if Parms[ParmStart .. i .. ParmEnd] ~= nil then
            Tab[i] = Parms[ParmStart .. i .. ParmEnd]
        elseif DefaultValue ~= nil then
            Tab[i] = DefaultValue
        elseif type(UseI) == "boolean" then
            Tab[i] = i
        elseif type(UseI) == "string" then
            Tab[i] = UseI .. i
        elseif DefaultTable ~= nil then
            Tab[i] = DefaultTable[i]
        else
            -- nothing
        end
    end
end

function calcStack()
    -- calculate stacked totals for series

    -- the current accumlated total overwrites the SeriesData Y value

    local PosTotal, NegTotal, NegFlag = {}, {}, false

    for j = 1, #GroupText do
        PosTotal[j] = 0
        NegTotal[j] = 0
        for i = 1, #SeriesData do
            if SeriesData[i][j] ~= nil and tonumber(SeriesData[i][j][1]) == j then
                if tonumber(SeriesData[i][j][2]) < 0 then
                    NegTotal[j] = NegTotal[j] - SeriesData[i][j][2]
                    SeriesData[i][j][2] = -NegTotal[j]
                    NegFlag = true
                else
                    PosTotal[j] = PosTotal[j] + SeriesData[i][j][2]
                    SeriesData[i][j][2] = PosTotal[j]
                end
            end
        end
    end
    if DoStack100 then
        for j = 1, #GroupText do
            for i = 1, #SeriesData do
                if SeriesData[i][j] ~= nil then
                    SeriesData[i][j][2] = SeriesData[i][j][2] / iif(tonumber(SeriesData[i][j][2]) < 0, NegTotal[j], PosTotal[j]) * 100
                end
            end
        end
        if NegFlag then 
            Parms["YMin"] = -100 
        end
    end
end

function checkParms()
    -- check for parameter issues

    -- unknown parameters
    local regexps = {}

    -- add patterns to regexps
    table.insert(regexps, "Series[%d][%d]*Values")
    table.insert(regexps, "Series[%d][%d]*Type")
    table.insert(regexps, "Series[%d][%d]*YAxis2")
    table.insert(regexps, "Series[%d][%d]*Labels")
    table.insert(regexps, "Series[%d][%d]*Color")
    table.insert(regexps, "Series[%d][%d]*Line")
    table.insert(regexps, "Series[%d][%d]*Width")
    table.insert(regexps, "Series[%d][%d]*Dash")
    table.insert(regexps, "Series[%d][%d]*Marker")
    table.insert(regexps, "Series[%d][%d]*MarkerSize")
    table.insert(regexps, "Series[%d][%d]*MarkerFill")
    table.insert(regexps, "Series[%d][%d]*Pattern")
    table.insert(regexps, "Series[%d][%d]*PatternColor")
    table.insert(regexps, "Series[%d][%d]*Text")

    table.insert(regexps, "Group[%d][%d]*Text")

    table.insert(regexps, "Segment[%d][%d]*Color")
    table.insert(regexps, "Segment[%d][%d]*Pattern")
    table.insert(regexps, "Segment[%d][%d]*PatternColor")

    table.insert(regexps, "ChartText[%d][%d]*")
    table.insert(regexps, "ChartText[%d][%d]*X")
    table.insert(regexps, "ChartText[%d][%d]*Y")

    local unknowns = unknownParameters(KeyWords, regexps)
    if #unknowns > 0 then
        -- ensure parms-not-found are at the top of the list of messages
        local tmp = {}
        copyTable(Msgs, tmp)
        Msgs = {}
        table.insert(Msgs, MessageTexts.ParmsNotFound)
        for k, v in spairs(unknowns) do
            table.insert(Msgs, "  " .. unknowns[k])
        end
        for i = 1, #tmp do
            table.insert(Msgs, tmp[i])
        end
    end

    if #GroupText > 0 then
        for k1, v1 in ipairs(SeriesData) do
            for k2, v2 in ipairs(v1) do
                local t = tonumber(v2[1])
                if t ~= math.floor(t)
                        or t < 1
                        or t > #GroupText then
                    table.insert(Msgs, string.format(MessageTexts.NotInGroup, k1, k2, v2[1]))
                end
            end
        end
    end
    
    if not DoYAxis2 then
        for k, v in pairs(YAxis2) do
            table.insert(Msgs, string.format(MessageTexts.NoYAxis2, k))
        end
    end 

    for k, v in pairs(Marker) do
        local t = tonumber(v)
        if v == "none" then
            -- OK
        elseif t == nil then
            table.insert(Msgs, string.format(MessageTexts.NotInMarkers, k, v))
        elseif (t >= 1 and t <= 7) then
            -- OK
        else
            table.insert(Msgs, string.format(MessageTexts.NotInMarkers, k, v))
        end
    end

    for k, v in pairs(LineDash) do
        local t = tonumber(v)
        if v == "none" then
            -- OK
        elseif t == nil then
            -- non-numerics are OK for LineDash
        elseif (t >= 1 and t <= #DashPattern) then
            -- OK
        else
            table.insert(Msgs, string.format(MessageTexts.NotInDashPatterns, k, v))
        end
    end

    for k, v in pairs(FillPattern) do
        local t = tonumber(v)
        if v == "none" then
            -- OK
        elseif t == nil then
            table.insert(Msgs, string.format(MessageTexts.NotInFillPatterns, k, v))
        elseif (t >= 1 and t <= 8)
                or (t >= 11 and t <= 18)
                or (t >= 21 and t <= 24)
                or (t >= 31 and t <= 34)
                or (t >= 41 and t <= 49)
                or (t >= 51 and t <= 56)
                or (t >= 61 and t <= 64) then
            -- OK
        else
            table.insert(Msgs, string.format(MessageTexts.NotInFillPatterns, k, v))
        end
    end

    if #Msgs > 0 then
        -- add messages to output
        table.insert(r, " " .. MessageTexts.MsgHeading)
        for i = 1, math.min(#Msgs, 10) do
            table.insert(r, "   " .. Msgs[i])
        end
        return false
    end

    return true
end

function prelimsCommon()

    Siz.ImagePadding = {}
    Siz.Legend = {}
    Siz.Text = {}

    -- the base unit for font sizes can be changed by the user
    BaseFontSize = BaseFontSize * (Parms["FontSize"] / 100)

    FontSiz.Title = 7 * BaseFontSize * Parms["TitleFontSize"] / 100

    FontSiz.LegendText = 5 * BaseFontSize * Parms["LegendFontSize"] / 100

    FontSiz.Labels = 4 * BaseFontSize * Parms["LabelsFontSize"] / 100

    FontSiz.ChartText = 4 * BaseFontSize * Parms["ChartTextFontSize"] / 100

    FontSiz.Footnote = 3 * BaseFontSize * Parms["FootnoteFontSize"] / 100

    -- the base line width can be changed by the user
    BaseLineWidth = BaseLineWidth * Parms["LineWidth"] / 100

    -- define sizes of other image components from base spacing units

    Siz.ImagePadding.Top = 2 * BaseUnit
    if Parms["ImagePaddingTop"] ~= nil then
        Siz.ImagePadding.Top = Parms["ImagePaddingTop"]
    end
    Siz.ImagePadding.Bottom = 2 * BaseUnit
    if Parms["ImagePaddingBottom"] ~= nil then
        Siz.ImagePadding.Bottom = Parms["ImagePaddingBottom"]
    end
    Siz.ImagePadding.Left = 2 * BaseUnit
    if Parms["ImagePaddingLeft"] ~= nil then
        Siz.ImagePadding.Left = Parms["ImagePaddingLeft"]
    end
    Siz.ImagePadding.Right = 2 * BaseUnit
    if Parms["ImagePaddingRight"] ~= nil then
        Siz.ImagePadding.Right = Parms["ImagePaddingRight"]
    end

    Siz.ChartMargin = 2 * BaseUnit

    Siz.Text.Interline = 2 * BaseFontSize

    Siz.Text.Labels = FontSiz.Labels

    Siz.Text.Title = iif(Parms["Title"] ~= nil and Parms["TitleX"] == nil, FontSiz.Title + Siz.Text.Interline, 0)

    Siz.Footnote = iif(Parms["Footnote"] ~= nil and Parms["FootnoteX"] == nil, FontSiz.Footnote, 0)

    Siz.Legend.Text = FontSiz.LegendText
    Siz.Legend.TextWidth = 5 * FontSiz.LegendText * Parms["LegendTextWidth"] / 100
    Siz.Legend.Offset = 3 * BaseUnit

    Siz.Text.Chart = FontSiz.ChartText
end

function prelimsAxes()

    Pos.XAxis = {}
    Pos.YAxis = {}
    Pos.YAxis2 = {}
    Pos.Origin = {}

    Siz.Space = {}
    Siz.XAxis = {}
    Siz.YAxis = {}
    Siz.YAxis2 = {}

    Siz.ChartWidth = Parms["ChartWidth"]
    Siz.ChartHeight = Parms["ChartHeight"]
    if DoChartAdjust then
        allChartAdjust()
    end
    
    if DoHorizontal then
        Siz.XAxis.Length = Siz.ChartHeight
        Siz.YAxis.Length = Siz.ChartWidth
        Siz.YAxis2.Length = Siz.ChartWidth
    else
        Siz.XAxis.Length = Siz.ChartWidth
        Siz.YAxis.Length = Siz.ChartHeight
        Siz.YAxis2.Length = Siz.ChartHeight
    end

    prelimsLegend()

    BarWidth = Parms["BarWidth"]
    BarSpace = Parms["BarSpace"] / 100

    -- The Mult values are the number of pixels per 1 unit on each axis

    if #GroupText > 0 then
        if BarSeriesCount > 0 then
            GroupWidth = Siz.XAxis.Length / #GroupText
            if DoStack or DoStack100 then
                UnitWidth = GroupWidth / 2
            else
                UnitWidth = GroupWidth / (BarSeriesCount + 1)
            end
            BarWidth = UnitWidth / (1 + BarSpace)
            BarSpace = UnitWidth - BarWidth
            Mult.x = Siz.XAxis.Length / #GroupText
        else
            GroupWidth = Siz.XAxis.Length / #GroupText
            Mult.x = GroupWidth
        end
    else
        Mult.x = Siz.XAxis.Length / (math.abs(Parms["XMax"] - Parms["XMin"]))
    end

    Mult.y = Siz.YAxis.Length / (math.abs(Parms["YMax"] - Parms["YMin"]))

    if DoYAxis2 then
        Mult.y2 = Siz.YAxis2.Length / (math.abs(Parms["Y2Max"] - Parms["Y2Min"]))
    else
        Mult.y2 = 1
    end
    
    --

    FontSiz.XAxisTitle = 5 * BaseFontSize * Parms["XAxisTitleFontSize"] / 100
    FontSiz.XAxisValues = 4 * BaseFontSize * Parms["XAxisValuesFontSize"] / 100

    FontSiz.YAxisTitle = 5 * BaseFontSize * Parms["YAxisTitleFontSize"] / 100
    FontSiz.YAxisValues = 4 * BaseFontSize * Parms["YAxisValuesFontSize"] / 100

    FontSiz.YAxis2Title = 5 * BaseFontSize * Parms["YAxis2TitleFontSize"] / 100
    FontSiz.YAxis2Values = 4 * BaseFontSize * Parms["YAxis2ValuesFontSize"] / 100

    Siz.AxisMark = 2 * BaseUnit
    Siz.AxisMark2 = 1 * BaseUnit

    Siz.XAxis.Title = iif(Parms["XAxisTitle"] ~= nil, FontSiz.XAxisTitle + Siz.Text.Interline, 0)
    Siz.YAxis.Title = iif(Parms["YAxisTitle"] ~= nil, FontSiz.YAxisTitle + Siz.Text.Interline, 0)

    if DoHorizontal then
        Siz.XAxis.Values = iif(Parms["XAxisValueSpace"] ~= nil, Parms["XAxisValueSpace"], 3 * FontSiz.XAxisValues)
        Siz.YAxis.Values = iif(Parms["YAxisValueSpace"] ~= nil, Parms["YAxisValueSpace"], FontSiz.YAxisValues + Siz.Text.Interline)
    else
        Siz.XAxis.Values = iif(Parms["XAxisValueSpace"] ~= nil, Parms["XAxisValueSpace"], FontSiz.XAxisValues + Siz.Text.Interline)
        Siz.YAxis.Values = iif(Parms["YAxisValueSpace"] ~= nil, Parms["YAxisValueSpace"], 3 * FontSiz.YAxisValues)
    end

    Siz.YAxis2.Title = 0
    Siz.YAxis2.Values = 0
    if DoYAxis2 then
        if Parms["YAxis2Title"] ~= nil then
            Siz.YAxis2.Title = FontSiz.YAxis2Title + Siz.Text.Interline
        end
        if DoHorizontal then
            Siz.YAxis2.Values = iif(Parms["YAxis2ValueSpace"] ~= nil, Parms["YAxis2ValueSpace"], FontSiz.YAxis2Values + Siz.Text.Interline)
        else
            Siz.YAxis2.Values = iif(Parms["YAxis2ValueSpace"] ~= nil, Parms["YAxis2ValueSpace"], 3 * FontSiz.YAxis2Values)
        end
    end
    
    -- spaces around the chart (working out from the chart):
    --   AxisMarks
    --   ChartMargin
    --   AxisValues
    --   AxisTitle
    --   Title
    --   Legend
    --   Footnote
    --   ImagePadding
    
    -- Top Space
    Siz.Space.Top = Siz.ImagePadding.Top
    Pos.Title = Siz.ImagePadding.Top + FontSiz.Title
    Siz.Space.Top = Siz.Space.Top + Siz.Text.Title
    if DoHorizontal and DoYAxis2 then
        Pos.YAxis2.Title = Siz.Space.Top + FontSiz.YAxis2Title
        Siz.Space.Top = Siz.Space.Top + Siz.YAxis2.Title
        Siz.Space.Top = Siz.Space.Top + Siz.YAxis2.Values
        Siz.Space.Top = Siz.Space.Top + Siz.ChartMargin + Siz.AxisMark

        Pos.YAxis2.Line = -Parms["XMax"] * Mult.x
        Pos.YAxis2.Values = Pos.YAxis2.Line - Siz.AxisMark - Siz.ChartMargin -- pos is bottom edge of values box
    else
        Siz.Space.Top = Siz.Space.Top + Siz.ChartMargin
    end

    -- Bottom Space
    if DoHorizontal then
        if Parms["XMin"] < 0 and #GroupText == 0 then
            -- Y axis line is within chart
            Pos.YAxis.Line = 0
            Pos.YAxis.Values = Pos.YAxis.Line + Siz.AxisMark + Siz.ChartMargin + FontSiz.YAxisValues
            Siz.Space.Bottom = Siz.ChartMargin
        else
            -- Y axis line is at bottom of chart
            Pos.YAxis.Line = -Parms["XMin"] * Mult.x
            Siz.Space.Bottom = Siz.AxisMark
            Pos.YAxis.Values = Pos.YAxis.Line + Siz.Space.Bottom + Siz.ChartMargin + FontSiz.YAxisValues
            Siz.Space.Bottom = Siz.Space.Bottom + Siz.ChartMargin + Siz.YAxis.Values
        end
        if Parms["YAxisTitle"] ~= nil then
            Pos.YAxis.Title = Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom + FontSiz.YAxisTitle            
            Siz.Space.Bottom = Siz.Space.Bottom + Siz.YAxis.Title
        end
    else
        if Parms["YMin"] < 0 then
            -- X axis line is within chart
            Pos.XAxis.Line = 0
            Pos.XAxis.Values = Pos.XAxis.Line + Siz.AxisMark + Siz.ChartMargin + FontSiz.XAxisValues
            Siz.Space.Bottom = Siz.ChartMargin
        else
            -- X axis line is at bottom of chart
            Pos.XAxis.Line = -Parms["YMin"] * Mult.y
            Siz.Space.Bottom = iif(#GroupText > 0, 0, Siz.AxisMark)
            Pos.XAxis.Values = Pos.XAxis.Line + Siz.Space.Bottom + Siz.ChartMargin + FontSiz.XAxisValues
            Siz.Space.Bottom = Siz.Space.Bottom + Siz.ChartMargin + Siz.XAxis.Values
        end
        if Parms["XAxisTitle"] ~= nil then
            Pos.XAxis.Title = Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom + FontSiz.XAxisTitle            
            Siz.Space.Bottom = Siz.Space.Bottom + Siz.XAxis.Title
        end
    end
    if Parms["LegendType"] == KeyWords["horizontal"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom + Siz.Legend.Offset
        Siz.Space.Bottom = Siz.Space.Bottom + Siz.Legend.Offset + Siz.Legend.Height + Siz.Text.Interline
    end
    if Parms["Footnote"] ~= nil then
        Pos.Footnote = Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom + Siz.Footnote
        Siz.Space.Bottom = Siz.Space.Bottom + Siz.Footnote
    end
    Siz.Space.Bottom = Siz.Space.Bottom + Siz.ImagePadding.Bottom

    -- Left Space
    Siz.Space.Left = Siz.ImagePadding.Left
    if DoHorizontal then
        if Parms["XAxisTitle"] ~= nil then
            Pos.XAxis.Title = Siz.Space.Left + FontSiz.XAxisTitle -- pos is right edge of title
            Siz.Space.Left = Siz.Space.Left + Siz.XAxis.Title
        end
        if Parms["YMin"] < 0 then
            -- X axis line is within chart
            Siz.Space.Left = Siz.Space.Left + Siz.ChartMargin
            Pos.XAxis.Line = 0
            Pos.XAxis.Values = Pos.XAxis.Line - Siz.AxisMark - Siz.ChartMargin -- pos is right edge of values box
        else
            -- X axis line is at left of chart
            Siz.Space.Left = Siz.Space.Left + Siz.XAxis.Values + Siz.ChartMargin + Siz.AxisMark
            Pos.XAxis.Line = Parms["YMin"] * Mult.y
            Pos.XAxis.Values = Pos.XAxis.Line - Siz.AxisMark - Siz.ChartMargin -- pos is right edge of values box
        end
    else
        if Parms["YAxisTitle"] ~= nil then
            Pos.YAxis.Title = Siz.Space.Left + FontSiz.YAxisTitle -- pos is right edge of title
            Siz.Space.Left = Siz.Space.Left + Siz.YAxis.Title
        end
        if Parms["XMin"] < 0 and #GroupText == 0 then
            -- Y axis line is within chart
            Siz.Space.Left = Siz.Space.Left + Siz.ChartMargin
            Pos.YAxis.Line = 0
            Pos.YAxis.Values = Pos.YAxis.Line - Siz.AxisMark - Siz.ChartMargin -- pos is right edge of values box
        else
            -- Y axis line is at left of chart
            Siz.Space.Left = Siz.Space.Left + Siz.YAxis.Values + Siz.ChartMargin + Siz.AxisMark
            Pos.YAxis.Line = Parms["XMin"] * Mult.x
            Pos.YAxis.Values = Pos.YAxis.Line - Siz.AxisMark - Siz.ChartMargin -- pos is right edge of values box
        end
    end

    -- Right Space
    Siz.Space.Right = 0
    if DoHorizontal then
        Siz.Space.Right = Siz.Space.Right + Siz.ChartMargin
    else
        if DoYAxis2 then
            Pos.YAxis2.Line = Parms["XMax"] * Mult.x
            Siz.Space.Right = Siz.Space.Right + Siz.AxisMark
            Pos.YAxis2.Values = Pos.YAxis2.Line + Siz.Space.Right + Siz.ChartMargin -- pos is left edge of values box
            Siz.Space.Right = Siz.Space.Right + Siz.ChartMargin + Siz.YAxis2.Values
            if Parms["YAxis2Title"] ~= nil then
                Pos.YAxis2.Title = Siz.Space.Left + Siz.ChartWidth + Siz.Space.Right + FontSiz.YAxis2Title
                Siz.Space.Right = Siz.Space.Right + Siz.YAxis2.Title
            end
        else
            Siz.Space.Right = Siz.Space.Right + Siz.ChartMargin
        end
    end
    if Parms["LegendType"] == KeyWords["vertical"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Left + Siz.ChartWidth + Siz.Space.Right + Siz.Legend.Offset
        Siz.Space.Right = Siz.Space.Right + Siz.Legend.Offset + Siz.Legend.Width
    end
    Siz.Space.Right = Siz.Space.Right + Siz.ImagePadding.Right
    
    if DoHorizontal then
        Pos.XAxis.Zero = Siz.Space.Top + Siz.ChartHeight + (iif(#GroupText == 0, Parms["XMin"], 0) * Mult.x)
        Pos.YAxis.Zero = Siz.Space.Left - (Parms["YMin"] * Mult.y)
    else
        Pos.XAxis.Zero = Siz.Space.Left - (iif(#GroupText == 0, Parms["XMin"], 0) * Mult.x)
        Pos.YAxis.Zero = Siz.Space.Top + Siz.ChartHeight + (Parms["YMin"] * Mult.y)
    end

    -- Chart Origin offsets
    if DoHorizontal then
        Pos.Origin.v = round(Siz.Space.Top
                + (Parms["XMax"] * Mult.x), 2)
        Pos.Origin.h = round(Siz.Space.Left
                + ((0 - Parms["YMin"]) * Mult.y), 2)      
    else
        Pos.Origin.v = round(Siz.Space.Top
                + (Parms["YMax"] * Mult.y), 2)
        Pos.Origin.h = round(Siz.Space.Left
                + ((0 - Parms["XMin"]) * Mult.x), 2)
    end
    
    -- Image size
    ImageWidth = round(Siz.Space.Left + Siz.ChartWidth + Siz.Space.Right, 0)
    ImageHeight = round(Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom, 0)
end

function allChartAdjust()

    Adjusts.ChartWidth = {}
    Adjusts.ChartWidth.Old = Siz.ChartWidth
    Adjusts.ChartHeight = {}
    Adjusts.ChartHeight.Old = Siz.ChartHeight
    Adjusts.XMax = {}
    Adjusts.XMax.Old = Parms["XMax"]
    Adjusts.XMin = {}
    Adjusts.XMin.Old = Parms["XMin"]
    Adjusts.YMax = {}
    Adjusts.YMax.Old = Parms["YMax"]
    Adjusts.YMin = {}
    Adjusts.YMin.Old = Parms["YMin"]
    
    if DoHorizontal then
        Siz.ChartWidth = chartAdjust(Siz.ChartWidth,
            "YAxisValueStep", "YAxisMark2Step",
            "YMax", "YMin")
        if #GroupText == 0 then    
            Siz.ChartHeight = chartAdjust(Siz.ChartHeight,
                "XAxisValueStep", "XAxisMark2Step",
                "XMax", "XMin")
        end
    else
        if #GroupText == 0 then    
            Siz.ChartWidth = chartAdjust(Siz.ChartWidth,
                "XAxisValueStep", "XAxisMark2Step",
                "XMax", "XMin")
        end
        Siz.ChartHeight = chartAdjust(Siz.ChartHeight,
            "YAxisValueStep", "YAxisMark2Step",
            "YMax", "YMin")
    end
    
    Adjusts.ChartWidth.New = Siz.ChartWidth
    Adjusts.ChartHeight.New = Siz.ChartHeight
    Adjusts.XMax.New = Parms["XMax"]
    Adjusts.XMin.New = Parms["XMin"]
    Adjusts.YMax.New = Parms["YMax"]
    Adjusts.YMin.New = Parms["YMin"]
end

function chartAdjust(userlength, mark, mark2, max, min)

    local unit = Parms[mark]
    if Parms[mark2] ~= nil then
        unit = Parms[mark2]
    end
    
    Parms[max] = round((Parms[max] / unit) + 0.49, 0) * unit
    Parms[min] = round((Parms[min] / unit) - 0.49, 0) * unit
    
    local count = (Parms[max] - Parms[min]) / unit
    
    return round(userlength / count, 0) * count
end
    
function outputAdjusts()

    table.insert(r, " &lt;!-- Adjustments made by Charts SVG:")
    table.insert(r, "  ChartWidth: " .. Adjusts.ChartWidth.Old .. " : " .. Adjusts.ChartWidth.New)
    table.insert(r, " ChartHeight: " .. Adjusts.ChartHeight.Old .. " : " .. Adjusts.ChartHeight.New)
    table.insert(r, "        XMax: " .. Adjusts.XMax.Old .. " : " .. Adjusts.XMax.New)
    table.insert(r, "        XMin: " .. Adjusts.XMin.Old .. " : " .. Adjusts.XMin.New)
    table.insert(r, "        YMax: " .. Adjusts.YMax.Old .. " : " .. Adjusts.YMax.New)
    table.insert(r, "        YMin: " .. Adjusts.YMin.Old .. " : " .. Adjusts.YMin.New)
    table.insert(r, " -->")
    table.insert(r, " ")
end

function prelimsPie()

    Siz.Space = {}

    -- pie chart radius & origin
    PieRadius = Parms["PieRadius"] -- not local
    PieOriginX, PieOriginY = PieRadius, PieRadius -- not local

    if Parms["Explode"] ~= nil then
        PieOriginX = PieOriginX + (PieRadius * Parms["ExplodeRadius"] / 100)
        PieOriginY = PieOriginY + (PieRadius * Parms["ExplodeRadius"] / 100)
    end
    if Parms["SegmentText"] ~= nil then
        -- possibly add for segment texts outside the pie
        local TextSpaceX = (PieRadius * Parms["SegmentTextRadius"] / 100)
            + (5 * Siz.Text.Chart * Parms["SegmentTextWidth"] / 100)
        if Parms["Explode"] ~= nil then
            TextSpaceX = TextSpaceX + (PieRadius * Parms["ExplodeRadius"] / 100)
        end
        PieOriginX = math.max(PieOriginX, TextSpaceX)

        local TextSpaceY = (PieRadius * Parms["SegmentTextRadius"] / 100)
            + (Siz.Text.Chart)
        if Parms["Explode"] ~= nil then
            TextSpaceY = TextSpaceY + (PieRadius * Parms["ExplodeRadius"] / 100)
        end
        PieOriginY = math.max(PieOriginY, TextSpaceY)
    end

    -- for pie charts, chart size is calculated, not user-defined
    Siz.ChartWidth = PieOriginX * 2
    Siz.ChartHeight = PieOriginY * 2

    prelimsLegend()

    -- Top Space
    Siz.Space.Top = Siz.ImagePadding.Top + Siz.Text.Title + Siz.ChartMargin
    Pos.Title = Siz.ImagePadding.Top + FontSiz.Title

    -- Bottom Space
    Siz.Space.Bottom = Siz.ChartMargin

    if Parms["LegendType"] == KeyWords["horizontal"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom + Siz.Legend.Offset
        Siz.Space.Bottom = Siz.Space.Bottom + Siz.Legend.Offset + Siz.Legend.Height + Siz.Text.Interline
    end
    if Parms["Footnote"] ~= nil then
        Pos.Footnote = Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom + Siz.Footnote
        Siz.Space.Bottom = Siz.Space.Bottom + Siz.Footnote
    end
    Siz.Space.Bottom = Siz.Space.Bottom + Siz.ImagePadding.Bottom

    -- Left Space
    Siz.Space.Left = Siz.ImagePadding.Left + Siz.ChartMargin

    -- Right Space
    Siz.Space.Right = Siz.ChartMargin
    if Parms["LegendType"] == KeyWords["vertical"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Left + Siz.ChartWidth + Siz.Space.Right + Siz.Legend.Offset
        Siz.Space.Right = Siz.Space.Right + Siz.Legend.Offset + Siz.Legend.Width
    end
    Siz.Space.Right = Siz.Space.Right + Siz.ImagePadding.Right

    -- Image size

    ImageWidth = round(Siz.Space.Left + Siz.ChartWidth + Siz.Space.Right, 0)
    ImageHeight = round(Siz.Space.Top + Siz.ChartHeight + Siz.Space.Bottom, 0)

    -- Mult.x, Mult.y and Pos.Axis.Zero are needed for positioning of Legends and Title, Footnote and Chart texts

    Mult.x = Siz.ChartWidth / 100
    Mult.y = Siz.ChartHeight / 100
    
    Pos.XAxis = {}
    Pos.YAxis = {}
    Pos.XAxis.Zero = Siz.Space.Left
    Pos.YAxis.Zero = Siz.Space.Top + Siz.ChartHeight
end

function prelimsLegend()
    -- legend height and width

    Siz.Legend.ElementWidth = Siz.ChartMargin + (2 * Siz.Legend.Text) + Siz.ChartMargin + Siz.Legend.TextWidth
    Siz.Legend.ElementHeight = Siz.Legend.Text + Siz.Text.Interline
    LegendElementsInWidth = 1 -- not local
    if #SeriesText < 1 then
        Parms["LegendType"] = KeyWords["none"]
    elseif Parms["LegendType"] == KeyWords["horizontal"] then
        LegendElementsInWidth = math.floor(Siz.ChartWidth / Siz.Legend.ElementWidth)
        LegendElementsInWidth = math.min(LegendElementsInWidth, #SeriesText)
        local LegendLines = math.ceil(#SeriesText / LegendElementsInWidth)

        Siz.Legend.Width = (LegendElementsInWidth * Siz.Legend.ElementWidth) + Siz.ChartMargin
        Siz.Legend.Height = Siz.ChartMargin + (LegendLines * Siz.Legend.ElementHeight) + Siz.ChartMargin
    else
        Siz.Legend.Width = Siz.Legend.ElementWidth + Siz.ChartMargin
        Siz.Legend.Height = Siz.ChartMargin + (#SeriesText * Siz.Legend.ElementHeight) + Siz.ChartMargin
    end
end

function commonTop()

    -- output header

    table.insert(r, MessageTexts.CopyText)
    table.insert(r, " ")

    -- SVG header stuff

    table.insert(r, " <?xml version=\"1.0\" encoding=\"UTF-8\" ?>")
    table.insert(r, " &lt;!-- Generator: commons.wikipedia.org/wiki/" .. mw.getCurrentFrame():getTitle() .. " -->")
    table.insert(r, " &lt;!-- Generator Version: 3.0 -->")
    table.insert(r, " <svg id=\"head\"")
    table.insert(r, "   xmlns=\"http://www.w3.org/2000/svg\"")
    table.insert(r, "   xmlns:xlink=\"http://www.w3.org/1999/xlink\"")
    table.insert(r, "   version=\"1.1\"")
    table.insert(r, "   font-family=\"Liberation Sans, Arial, sans-serif\"")
    table.insert(r, "   width=\"" .. ImageWidth .. "\"")
    table.insert(r, "   height=\"" .. ImageHeight .. "\"")
    table.insert(r, " >")
    table.insert(r, " ")

    table.insert(r, " <title>" .. Parms["FileTitle"] .. "</title>")
    table.insert(r, " <desc>")
    table.insert(r, "   " .. Parms["FileDesc"] .. "")
    table.insert(r, " </desc>")
    table.insert(r, " ")

    local DT = os.date("*t") -- returns a table with the current date & time

    table.insert(r, " <metadata>")
    table.insert(r, "   <rdf:RDF")
    table.insert(r, "     xmlns:rdf = \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"")
    table.insert(r, "     xmlns:rdfs = \"http://www.w3.org/2000/01/rdf-schema#\"")
    table.insert(r, "     xmlns:dc = \"http://purl.org/dc/elements/1.1/\" >")
    table.insert(r, "     <rdf:Description")
    table.insert(r, "       dc:title=\"" .. Parms["FileTitle"] .. "\"")
    table.insert(r, "       dc:description=\"" .. Parms["FileDesc"] .. "\"")
    table.insert(r, "       dc:date=\"" .. DT.year .. "-" .. DT.month .. "-" .. DT.day .. "\"")
    table.insert(r, "       dc:format=\"image/svg+xml\"")
    table.insert(r, "       dc:language=\"en\" >")
    table.insert(r, "     </rdf:Description>")
    table.insert(r, "   </rdf:RDF>")
    table.insert(r, " </metadata>")
    table.insert(r, " ")

    if not DoPie and DoChartAdjust then
        outputAdjusts()
    end

    table.insert(r, " &lt;!-- == Backgrounds == -->")
    table.insert(r, " ")

    -- image background rectangle
    table.insert(r, " &lt;!-- image background -->")
    table.insert(r, " <rect id=\"imagebackground\" x=\"0\" y=\"0\" width=\"" .. ImageWidth .. "\" height=\"" .. ImageHeight .. "\""
        .. " stroke-width=\"1\""
        .. " stroke=\"" .. Parms["ImageBorder"] .. "\""
        .. " fill=\"" .. Parms["ImageBackgroundColor"] .. "\""
        .. "/>")
    table.insert(r, " ")

    if Parms["ImageBackgroundSVG"] ~= nil then
        table.insert(r, " &lt;!-- Image Background SVG -->")
        table.insert(r, " " .. Parms["ImageBackgroundSVG"] .. "")
        table.insert(r, " ")
    end

    -- chart background

    if Parms["ChartBackgroundColor"] ~= nil then
        table.insert(r, " &lt;!-- chart background -->")
        table.insert(r, " <rect id=\"ChartBackground\" x=\"" .. Siz.Space.Left .. "\" y=\"" .. Siz.Space.Top .. "\""
            .. " width=\"" .. Siz.ChartWidth .. "\""
            .. " height=\"" .. Siz.ChartHeight .. "\""
            .. " fill=\"" .. Parms["ChartBackgroundColor"] .. "\"/>")
        table.insert(r, " ")
    end
end

function codeAxisGrids()

    table.insert(r, " &lt;!-- == Axis Chart - Translate == -->")
    table.insert(r, " <g id=\"graphs\" transform=\"translate(" .. Pos.Origin.h .. ", " .. Pos.Origin.v .. ")\">")
    table.insert(r, " ")

    -- the 'horizontal' grid is on the horizontal axis, and has vertical lines
    -- and vice versa

    local HGridInterval, VGridInterval
    
    local HLabel = 'x'
    local VLabel = 'y'
    if DoHorizontal then
        HLabel = 'y'
        VLabel = 'x'
    end
    
    if (HLabel == 'x' and #GroupText > 0) or Parms[string.upper(HLabel) .. "Grid"] == KeyWords["none"] then
        -- no horizontal grid
    else
        HGridInterval = Parms[string.upper(HLabel) .. "Grid"] * Mult[HLabel]
    end

    if (VLabel == 'x' and #GroupText > 0) or Parms[string.upper(VLabel) .. "Grid"] == KeyWords["none"] then
        -- no vertical grid
    else
        VGridInterval = Parms[string.upper(VLabel) .. "Grid"] * Mult[VLabel]
    end

    if Parms["XGrid"] ~= KeyWords["none"] or Parms["YGrid"] ~= KeyWords["none"] then
        table.insert(r, " &lt;!-- == Axis Chart - Grids == -->")
        table.insert(r, " ")

        table.insert(r, " <style type=\"text/css\"> <![CDATA[")

        table.insert(r, "   .gridline {")
        table.insert(r, "     stroke:       lightgrey;")
        table.insert(r, "     stroke-width: " .. BaseLineWidth .. ";")
        table.insert(r, "   }")

        table.insert(r, "   ]]>")
        table.insert(r, " </style>")
        table.insert(r, " ")

        table.insert(r, " <defs>")

        if (HLabel == 'x' and #GroupText > 0) or Parms[string.upper(HLabel) .. "Grid"] == KeyWords["none"] then
            -- no horizontal grid
        else
            table.insert(r, "   &lt;!-- " .. HLabel .. "-axis grid, vertical lines -->")
            table.insert(r, "   <pattern id=\"" .. HLabel .. "-gridline\""
               .. " height=\"10\""
               .. " width=\"" .. round(HGridInterval, 2) .. "\""
               .. " patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <line x1=\"0\""
                .. " y1=\"0\""
                .. " x2=\"0\""
                .. " y2=\"10\""
                .. " class=\"gridline\"/>")
            table.insert(r, "   </pattern>")
        end

        if (VLabel == 'x' and #GroupText > 0) or Parms[string.upper(VLabel) .. "Grid"] == KeyWords["none"] then
            -- no vertical grid
        else
            table.insert(r, "   &lt;!-- " .. VLabel .. "-axis grid, horizontal lines -->")
            table.insert(r, "   <pattern id=\"" .. VLabel .. "-gridline\""
               .. " height=\"" .. round(VGridInterval, 2) .. "\""
               .. " width=\"10\""
               .. " patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <line x1=\"0\""
                .. " y1=\"0\""
                .. " x2=\"10\""
                .. " y2=\"0\""
                .. " class=\"gridline\"/>")
            table.insert(r, "   </pattern>")
        end
        table.insert(r, " </defs>")
        table.insert(r, " ")
        
        local XStart = Parms[string.upper(HLabel) .. "Min"] * Mult[HLabel]
        local YStart = -Parms[string.upper(VLabel) .. "Max"] * Mult[VLabel]

        if (HLabel == 'x' and #GroupText > 0) or Parms[string.upper(HLabel) .. "Grid"] == KeyWords["none"] then
            -- no horizontal grid
        else
            table.insert(r, " <rect id=\"" .. HLabel .. "-gridline-area\""
                .. " x=\"" .. round(XStart, 2) .. "\""
                .. " y=\"" .. round(YStart, 2) .. "\""
                .. " width=\"" .. Siz.ChartWidth + BaseLineWidth .. "\""
                .. " height=\"" .. Siz.ChartHeight .. "\""
                .. " fill=\"url(#" .. HLabel .. "-gridline)\"/>")
        end

        if (VLabel == 'x' and #GroupText > 0) or Parms[string.upper(VLabel) .. "Grid"] == KeyWords["none"] then
            -- no vertical grid
        else
            table.insert(r, " <rect id=\"" .. VLabel .. "-gridline-area\""
                .. " x=\"" .. round(XStart, 2) .. "\""
                .. " y=\"" .. round(YStart, 2) .. "\""
                .. " width=\"" .. Siz.ChartWidth .. "\""
                .. " height=\"" .. Siz.ChartHeight + BaseLineWidth .. "\""
                .. " fill=\"url(#" .. VLabel .. "-gridline)\"/>")
        end
        table.insert(r, " ")
    end
end

function stylesAreas()

    table.insert(r, " &lt;!-- == Graph - Area Styles == -->")
    table.insert(r, " ")

    table.insert(r, " <style type=\"text/css\"> <![CDATA[")

    table.insert(r, "   /*-- general style of areas --*/")
    table.insert(r, "   .series-areas-general {")
    if Parms["BorderColor"] ~= nil then
        table.insert(r, "     stroke:       " .. Parms["BorderColor"] .. ";")
        table.insert(r, "     stroke-width: " .. BaseLineWidth * Parms["BorderWidth"] / 100 .. ";")
    else
        table.insert(r, "     stroke-width: " .. 0 .. ";")
    end
    table.insert(r, "   }")

    if DoPie then
        for k, v in ipairs(SeriesText) do
            codeStyleArea(k, Color[k], FillPattern[k])
        end
    else
        for i = 1, #SeriesData do
            if SeriesData[i] ~= nil and (SType[i] == KeyWords["bar"] or DoArea) then
                codeStyleArea(i, Color[i], FillPattern[i])
            end
        end
    end

    table.insert(r, " ]]>")
    table.insert(r, " </style>")
    table.insert(r, " ")
end

function codeStyleArea(SeriesNumber, Color, Pattern)
    -- area style for a series

    -- SeriesNumber, numeric
    -- Color, text
    -- Pattern, text: 'none' or nil means a pattern is not defined

    table.insert(r, "   /*-- series " .. SeriesNumber .. " --*/")
    table.insert(r, "   .series" .. SeriesNumber .. " {")
    -- fill for the area
    if Pattern == nil or Pattern == KeyWords["none"] then
        table.insert(r, "     fill:   " .. Color .. ";")
    else
        table.insert(r, "     fill:   url(#series" .. SeriesNumber .. "pattern);")
    end
    table.insert(r, "   }")
end

function defsAreas()

    local exists = false
    for i = 1, SeriesCount do
        if FillPattern[i] ~= nil and FillPattern[i] ~= KeyWords["none"] then
            exists = true
        end
    end

    if exists then
        table.insert(r, " &lt;!-- == Fill Patterns == -->")
        table.insert(r, " ")
        table.insert(r, " <defs>")
        for i = 1, SeriesCount do
            if FillPattern[i] ~= nil and FillPattern[i] ~= KeyWords["none"] then
                -- define the pattern
                codeDefFillPattern(i, FillPattern[i], Color[i], FillPatternColor[i])
            end
        end
        table.insert(r, " </defs>")
        table.insert(r, " ")
    end
end

function codeDefFillPattern(SeriesNumber, PatternType, FillColor, PatternColor)
    -- definition of a fill pattern for a series

    -- SeriesNumber, numeric
    -- PatternType, numeric
    -- FillColor, text
    -- PatternColor, text

    local l, t = "", ""

    if PatternType == nil then
        return
    end

    table.insert(r, "   &lt;!-- Series " .. SeriesNumber .. "-->")

    -- pattern groups:
    --   1-8: line hatch, close, rotated 0 (horizontal), 90 (vertical), -45, 45, -22.5, 22,5, -67.5, 67.5
    --   11-18: line hatch, wide, rotated 0 (horizontal), 90 (vertical), -45, 45, -22.5, 22,5, -67.5, 67.5
    --   21-24: cross hatch, close, rotated 0, 45, -22.5, -67.5
    --   31-34: cross hatch, wide, rotated 0, 45, -22.5, -67.5

    -- addition of fill patterns will require changes in checkParms() to allow them

    if PatternType >= 1 and PatternType <= 8 then
        -- line hatches, close
        l = "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"6\" height=\"6\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate("
        if PatternType == 1 then
            l = l .. "0"
        elseif PatternType == 2 then
            l = l .. "90"
        elseif PatternType == 3 then
            l = l .. "-45"
        elseif PatternType == 4 then
            l = l .. "45"
        elseif PatternType == 5 then
            l = l .. "-22.5"
        elseif PatternType == 6 then
            l = l .. "22.5"
        elseif PatternType == 7 then
            l = l .. "-67.5"
        elseif PatternType == 8 then
            l = l .. "67.5"
        end
        table.insert(r, l .. ")\">")
        table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"6\" height=\"6\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
        table.insert(r, "     <line x1=\"0\" y1=\"2\" x2=\"6\" y2=\"2\" stroke=\"" .. PatternColor .. "\" stroke-width=\"2\"/>")
        table.insert(r, "   </pattern>")
    elseif PatternType >= 11 and PatternType <= 18 then
        -- line hatches, wide
        l = "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate("
        if PatternType == 11 then
            l = l .. "0"
        elseif PatternType == 12 then
            l = l .. "90"
        elseif PatternType == 13 then
            l = l .. "-45"
        elseif PatternType == 14 then
            l = l .. "45"
        elseif PatternType == 15 then
            l = l .. "-22.5"
        elseif PatternType == 16 then
            l = l .. "22.5"
        elseif PatternType == 17 then
            l = l .. "-67.5"
        elseif PatternType == 18 then
            l = l .. "67.5"
        end
        table.insert(r, l .. ")\">")
        table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"12\" height=\"12\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
        table.insert(r, "     <line x1=\"0\" y1=\"8\" x2=\"12\" y2=\"8\" stroke=\"" .. PatternColor .. "\" stroke-width=\"2\"/>")
        table.insert(r, "   </pattern>")
    elseif PatternType >= 21 and PatternType <= 24 then
        -- cross hatches, close
        l = "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"6\" height=\"6\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate("
        if PatternType == 21 then
            l = l .. "0"
        elseif PatternType == 22 then
            l = l .. "45"
        elseif PatternType == 23 then
            l = l .. "-22.5"
        elseif PatternType == 24 then
            l = l .. "-67.5"
        end
        table.insert(r, l .. ")\">")
        table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"6\" height=\"6\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
        table.insert(r, "     <line x1=\"0\" y1=\"4\" x2=\"6\" y2=\"4\" stroke=\"" .. PatternColor .. "\" stroke-width=\"2\"/>")
        table.insert(r, "     <line x1=\"4\" y1=\"0\" x2=\"4\" y2=\"6\" stroke=\"" .. PatternColor .. "\" stroke-width=\"2\"/>")
        table.insert(r, "   </pattern>")
    elseif PatternType >= 31 and PatternType <= 34 then
        -- cross hatches, wide
        l = "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate("
        if PatternType == 31 then
            l = l .. "0"
        elseif PatternType == 32 then
            l = l .. "45"
        elseif PatternType == 33 then
            l = l .. "-22.5"
        elseif PatternType == 34 then
            l = l .. "-67.5"
        end
        table.insert(r, l .. ")\">")
        table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"12\" height=\"12\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
        table.insert(r, "     <line x1=\"0\" y1=\"2\" x2=\"12\" y2=\"2\" stroke=\"" .. PatternColor .. "\" stroke-width=\"2\"/>")
        table.insert(r, "     <line x1=\"4\" y1=\"0\" x2=\"4\" y2=\"12\" stroke=\"" .. PatternColor .. "\" stroke-width=\"2\"/>")
        table.insert(r, "   </pattern>")
    elseif PatternType >= 41 and PatternType <= 49 then
        -- stipples
        if PatternType == 41 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"12\" height=\"12\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"6\" y=\"6\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 42 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"8\" height=\"8\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"4\" y=\"4\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 43 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"6\" height=\"6\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"6\" height=\"6\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"3\" y=\"3\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 44 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"4\" height=\"4\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"4\" height=\"4\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"2\" y=\"2\" width=\"1\" height=\"1\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 45 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"12\" height=\"12\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"6\" y=\"6\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 46 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"8\" height=\"8\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"4\" y=\"4\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 47 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"6\" height=\"6\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"6\" height=\"6\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"3\" y=\"3\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 48 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"16\" height=\"16\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"4\" width=\"4\" height=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"8\" y=\"12\" width=\"4\" height=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 49 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"12\" height=\"12\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"4\" height=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"6\" y=\"6\" width=\"4\" height=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        end
        table.insert(r, l .. "   </pattern>")
    elseif PatternType >= 51 and PatternType <= 56 then
        -- checks
        if PatternType == 51 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"4\" height=\"4\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"4\" height=\"4\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r,  "     <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 52 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"6\" height=\"6\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"6\" height=\"6\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"3\" height=\"3\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"3\" y=\"3\" width=\"3\" height=\"3\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 53 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"8\" height=\"8\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"4\" height=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r,  "     <rect x=\"4\" y=\"4\" width=\"4\" height=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 54 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"10\" height=\"10\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"5\" height=\"5\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"5\" y=\"5\" width=\"5\" height=\"5\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 55 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"12\" height=\"12\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"6\" height=\"6\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"6\" y=\"6\" width=\"6\" height=\"6\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 56 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"16\" height=\"16\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"8\" height=\"8\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <rect x=\"8\" y=\"8\" width=\"8\" height=\"8\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        end
        table.insert(r, "   </pattern>")
    elseif PatternType >= 61 and PatternType <= 64 then
        -- circles
        if PatternType == 61 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"16\" height=\"16\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <circle cx=\"4\" cy=\"4\" r=\"3\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r,  "     <circle cx=\"12\" cy=\"12\" r=\"3\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 62 then
            table.insert(r,  "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r,  "     <rect x=\"0\" y=\"0\" width=\"20\" height=\"20\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r,  "     <circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <circle cx=\"15\" cy=\"15\" r=\"4\" fill=\"" .. PatternColor .. "\" stroke=\"none\"/>")
        elseif PatternType == 63 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <rect x=\"0\" y=\"0\" width=\"16\" height=\"16\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <circle cx=\"4\" cy=\"4\" r=\"3\" fill=\"none\" stroke=\"" .. PatternColor .. "\"/>")
            table.insert(r, "     <circle cx=\"12\" cy=\"12\" r=\"3\" fill=\"none\" stroke=\"" .. PatternColor .. "\"/>")
        elseif PatternType == 64 then
            table.insert(r, "   <pattern id=\"series" .. SeriesNumber .. "pattern\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r,  "     <rect x=\"0\" y=\"0\" width=\"20\" height=\"20\"" .. " fill=\"" .. FillColor .. "\" stroke=\"none\"/>")
            table.insert(r, "     <circle cx=\"5\" cy=\"5\" r=\"4\" fill=\"none\" stroke=\"" .. PatternColor .. "\" stroke-width=\"1\"/>")
            table.insert(r,  "     <circle cx=\"15\" cy=\"15\" r=\"4\" fill=\"none\" stroke=\"" .. PatternColor .. "\" stroke-width=\"1\"/>")
        end
        table.insert(r, "   </pattern>")
    end
end

function stylesLines()

    table.insert(r, " &lt;!-- == Graph - Line Styles == -->")
    table.insert(r, " ")

    table.insert(r, " <style type=\"text/css\"> <![CDATA[")

    table.insert(r, "   /*-- general style of graph lines --*/")
    table.insert(r, "   .series-lines-general {")
    table.insert(r, "     stroke-width:    " .. BaseLineWidth * Parms["GraphLineWidth"] / 100 .. ";")
    table.insert(r, "     stroke-linejoin: round;")
    table.insert(r, "     stroke-linecap:  round;")
    table.insert(r, "     fill:            none;")
    table.insert(r, "   }")

    if #Marker > 0 then
        table.insert(r, "   /*-- general style of markers --*/")
        table.insert(r, "   .graph-marker {")
        table.insert(r, "     stroke-width:    " .. BaseLineWidth .. ";")
        table.insert(r, "     fill:            white;")
        table.insert(r, "     stroke-linejoin: round;")
        table.insert(r, "   }")
    end

    for i = 1, #SeriesData do
        if SeriesData[i] ~= nil and SType[i] == KeyWords["line"] then
            local lw = 0
            if LineWidth[i] ~= nil then
                lw = BaseLineWidth * LineWidth[i] / 100
            end
            codeStyleLineMarker(i, LineShow[i], Color[i], lw, LineDash[i], Marker[i], MarkerFill[i])
        end
    end

    table.insert(r, " ]]>")
    table.insert(r, " </style>")
    table.insert(r, " ")
end

function codeStyleLineMarker(SeriesNumber, LineRequired, LineColor, LineWidth, LineDash, MarkerRequired, MarkerFill)
    -- line and marker styles for a series

    -- SeriesNumber, numeric
    -- LineRequired
    -- LineColor, text
    -- LineWidth, numeric
    -- LineDash, text
    -- MarkerRequired
    -- MarkerFill, text

    if LineRequired ~= nil or MarkerRequired ~= nil then
        table.insert(r, "   /*-- series " .. SeriesNumber .. " --*/")
        -- line defined if either line or marker required
        table.insert(r, "   .series" .. SeriesNumber .. " {")
        -- stroke for the line
        if LineRequired == KeyWords["none"] then
            table.insert(r, "     stroke:           none;")
        else
            table.insert(r, "     stroke:           " .. LineColor .. ";")
            table.insert(r, "     stroke-width:     " .. LineWidth .. ";")
            -- dash array for the line
            if LineDash ~= nil and LineDash ~= KeyWords["none"] then
                table.insert(r, "     stroke-dasharray: " .. LineDash .. ";")
                table.insert(r, "     stroke-linecap:   butt;")
            end
        end
        if MarkerRequired == nil or MarkerRequired == KeyWords["none"] then
            ----close the line style
            table.insert(r, "   }")
        else
            -- note: markers are set on the lines when they are created, not here in the line style
            --   this enables the line in the legend to have only a mid-marker
            table.insert(r, "   }")
            -- define the marker stroke and fill
            table.insert(r, "   .series" .. SeriesNumber .. "-marker {")
            -- marker stroke color is always the same as the line
            table.insert(r, "     stroke: " .. LineColor .. ";")
            table.insert(r, "     fill:   " .. MarkerFill .. ";")
            table.insert(r, "   }")
        end
    end
end

function defsMarkers()

    local exists = false
    for i = 1, #SeriesData do
        if Marker[i] ~= nil and Marker[i] ~= KeyWords["none"] then
            exists = true
        end
    end

    if exists then
        table.insert(r, " &lt;!-- == Graph - Markers == -->")
        table.insert(r, " ")
        table.insert(r, " <defs>")
        table.insert(r, "   <g class=\"graph-marker\">")
        for i = 1, #SeriesData do
            if Marker[i] ~= nil and Marker[i] ~= KeyWords["none"] then
                -- define the shape for the marker
                codeDefMarkerShape(i, Marker[i], BaseUnit * 2 * MarkerSize[i] / 100, MarkerFill[i])
                -- and use the shape in the definition of the marker
                codeDefMarkerCreate(i, BaseUnit * 2 * MarkerSize[i] / 100)
                table.insert(r, " ")
            end
        end
        table.insert(r, "   </g>")
        table.insert(r, " </defs>")
        table.insert(r, " ")
    end
end

function codeDefMarkerShape(SeriesNumber, MarkerType, MarkerSize, MarkerFill)
    -- definition of a shape for a series

    -- SeriesNumber, numeric
    -- MarkerType, numeric or text, if text (eg: 'yes') the default is SeriesNumber
    -- MarkerSize, numeric
    -- MarkerFill, text

    -- addition of further markers will require changes in checkParms() to allow them

    local l, t = "", ""

    table.insert(r, "     <g id=\"series" .. SeriesNumber .. "markershape\">")
    if MarkerType ~= nil then
        -- MarkerType is a number, use it for the type
    else
        -- MarkerType is not a number, use the series number
        MarkerType = SeriesNumber
    end

    -- all shapes are defined around a centre point of 0,0

    if MarkerType == 2 then
        -- circle
        table.insert(r, "       &lt;!-- circle -->")
        l = "       <circle cx=\"0\" dx=\"0\" r=\"" .. round(MarkerSize / 2, 2) .. "\""
        if MarkerFill ~= nil then
            l = l .. " fill=\"" .. MarkerFill .. "\""
        end
        table.insert(r, l .. "/>")
    elseif MarkerType == 3 then
        -- triangle (point up)
        t = round(MarkerSize / 2, 2)
        table.insert(r, "       &lt;!-- triangle, point up -->")
        l = "       <polygon points=\"" .. -t .. "," .. t .. " " .. t .. "," .. t .. ", " .. 0 .. "," .. -t .."\""
        if MarkerFill ~= nil then
            l = l .. " fill=\"" .. MarkerFill .. "\""
        end
        table.insert(r, l .. "/>")
    elseif MarkerType == 4 then
        -- tilted square (diamond)
        table.insert(r, "       &lt;!-- diamond -->")
        l = "       <rect transform=\"rotate(45)\""
            .. " x=\"" .. round(-MarkerSize / 2, 2) .. "\""
            .. " y=\""  .. round(-MarkerSize / 2, 2) .. "\""
            .. " width=\"" .. round(MarkerSize, 2) .. "\""
            .. " height=\"" .. round(MarkerSize, 2) .. "\""
        if MarkerFill ~= nil then
            l = l .. " fill=\"" .. MarkerFill .. "\""
        end
        table.insert(r, l .. "/>")
    elseif MarkerType == 5 then
        -- tilted triangle (point down)
        t = round(MarkerSize / 2, 2)
        table.insert(r, "       &lt;!-- triangle, point down -->")
        l = "       <polygon points=\"" .. 0 .. "," .. t .. " " .. -t .. "," .. -t .. ", " .. t .. "," .. -t .. "\""
        if MarkerFill ~= nil then
            l = l .. " fill=\"" .. MarkerFill .. "\""
        end
        table.insert(r, l .. "/>")
    elseif MarkerType == 6 then
        -- cross
        t = round(MarkerSize / 2, 2)
        table.insert(r, "       &lt;!-- cross -->")
        table.insert(r, "       <path d=\"M " .. -t .. "," .. t .. " L " .. t .. "," .. -t .. " z M " .. t .. "," .. t .. " L " .. -t .. "," .. -t .. " z\"/>")
    elseif MarkerType == 7 then
        -- plus
        t = round(MarkerSize / 2, 2)
        table.insert(r, "       &lt;!-- plus -->")
        table.insert(r, "       <path d=\"M " .. 0 .. "," .. t .. " L " .. 0 .. "," .. -t .. " z M " .. -t .. "," .. 0 .. " L " .. t .. "," .. 0 .. " z\"/>")
    else
        -- default and 1
        -- square
        table.insert(r, "       &lt;!-- square -->")
        l = "       <rect x=\"" .. round(-MarkerSize / 2, 2) .. "\""
            .. " y=\""  .. round(-MarkerSize / 2, 2) .. "\""
            .. " width=\"" .. round(MarkerSize, 2) .. "\""
            .. " height=\"" .. round(MarkerSize, 2) .. "\""
        if MarkerFill ~= nil then
            l = l .. " fill=\"" .. MarkerFill .. "\""
        end
        table.insert(r, l .. "/>")
    end
    table.insert(r, "     </g>")
end

function codeDefMarkerCreate(SeriesNumber, MarkerSize)
    -- definition of a marker for a series

    -- SeriesNumber, numeric
    -- MarkerSize, numeric

    local l = ""

    l = "     <marker id=\"series" .. SeriesNumber .. "marker\""
    l = l .. " class=\"series" .. SeriesNumber .. "-marker\""
    l = l .. " viewBox=\"0 0 " .. round(MarkerSize, 2) .. " " .. round(MarkerSize, 2) .. "\""
        .. " markerWidth=\"" .. round(MarkerSize, 2) .. "\""
        .. " markerHeight=\"" .. round(MarkerSize, 2) .. "\""
        .. " overflow=\"visible\""
        .. " markerUnits=\"userSpaceOnUse\">"
    table.insert(r, l)
    table.insert(r, "       <use xlink:href=\"#series" .. SeriesNumber .. "markershape\"/>")
    table.insert(r, "     </marker>")
end

function elementsGraphs()

    local BarNumber = BarSeriesCount + 1
    local DoLabels = false
    local LabelPos = {}

    table.insert(r, " &lt;!-- == Graph - Bars and Lines == -->")
    table.insert(r, " ")

    for i = #SeriesData, 1, -1 do
        if SeriesText[i]  == nil then
            table.insert(r, "   &lt;!-- " .. "Series" .. i .. " -->")
        else
            table.insert(r, "   &lt;!-- " .. SeriesText[i] .. " -->")
        end
        if DoYAxis2 and YAxis2[i] ~= nil then
            table.insert(r, "   &lt;!-- Y values are on second Y axis -->")
        end
        if Parms["IncludeOriginalData"] == KeyWords["no"]
                or (Parms["IncludeOriginalData"] == KeyWords["auto"] and DataPointsCount > AutoDataPointsLimit) then
            table.insert(r, "   &lt;!-- original data: not included -->")
        else
            table.insert(r, "   &lt;!-- original data:")

            for k, v in ipairs(OriginalData[i]) do
                table.insert(r, "     " .. v[1] .. " " .. v[2])
            end
            table.insert(r, "   -->")
        end
        LabelPos[i] = {}
        if SType[i] == KeyWords["bar"] then
            BarNumber = BarNumber - 1
            for k, v in ipairs(SeriesData[i]) do
                if v[2] ~= nil then
                    local px = tonumber(v[1])
                    if #GroupText > 0 then
                        if DoStack or DoStack100 then
                            px = iif(DoGroupsTopDown, #GroupText - px, px - 1) * GroupWidth
                                + UnitWidth / 2
                                + (BarSpace / 2)
                                + iif(DoHorizontal, BarWidth, 0)
                        else
                            px = iif(DoGroupsTopDown, #GroupText - px, px - 1) * GroupWidth
                                + UnitWidth / 2
                                + UnitWidth * (iif(DoGroupsTopDown, BarSeriesCount - BarNumber + 1, BarNumber) - iif(DoHorizontal, 0, 1))
                                + (BarSpace / 2)
                        end
                    else
                        px = (px * Mult.x) + (iif(DoHorizontal, 1, -1) * (BarWidth / 2))
                    end
    
                    local Min, Multiply = Parms["YMin"], Mult.y
                    if DoYAxis2 and YAxis2[i] ~= nil then
                        Min = Parms["Y2Min"]
                        Multiply = Mult.y2
                    end
                    
                    local val = tonumber(v[2])
                    local posn, size = 0, 0
                    if DoHorizontal then
                        if Min >= 0 then
                            posn = 0
                            size = val - Min
                        elseif val >= 0 then
                            posn = 0
                            size = val
                        else
                            posn = -val
                            size = -val
                        end
                        posn = Pos.XAxis.Line - (posn * Multiply)
                        size = size * Multiply

                        table.insert(r, "   <rect id=\"series" .. i .. "-" .. k .. "\" class=\"series-areas-general series" .. i .. "\""
                            .. " x=\"" .. round(posn, 2) .. "\" y=\"" .. round(-px, 2) .. "\""
                            .. " width=\"" .. round(size, 2) .. "\" height=\"" .. round(BarWidth, 2) .. "\" />")
                    else
                        if Min >= 0 then
                            posn = val - Min
                            size = val - Min
                        elseif val >= 0 then
                            posn = val
                            size = val
                        else
                            posn = 0
                            size = -val
                        end
                        posn = Pos.XAxis.Line - (posn * Multiply)
                        size = size * Multiply

                        table.insert(r, "   <rect id=\"series" .. i .. "-" .. k .. "\" class=\"series-areas-general series" .. i .. "\""
                            .. " x=\"" .. round(px, 2) .. "\" y=\"" .. round(posn, 2) .. "\""
                            .. " width=\"" .. round(BarWidth, 2) .. "\" height=\"" .. round(size, 2) .. "\" />")
                    end
    
                    if Labels[i] ~= nil then
                        LabelPos[i][k] = {}
                        if DoHorizontal then
                            LabelPos[i][k]["y"] = -(px - (BarWidth / 2))
                            if val <= 0 then  
                                LabelPos[i][k]["x"] = posn - Siz.Text.Interline
                            else  
                                LabelPos[i][k]["x"] = posn + size + Siz.Text.Interline
                            end
                        else
                            LabelPos[i][k]["x"] = px + (BarWidth / 2)
                            if val <= 0 then  
                                LabelPos[i][k]["y"] = posn + size + Siz.Text.Interline + Siz.Text.Labels
                            else  
                                LabelPos[i][k]["y"] = posn - Siz.Text.Interline
                            end
                        end
                        DoLabels = true
                    end
                end
            end
        else
            -- line
            tr = "   <polyline id=\"graph" .. i .. "\" class=\"series-"
            if DoArea then
                tr = tr .. "areas"
            else
                tr = tr .. "lines"
            end
            tr = tr .. "-general series" .. i .. "\""
            table.insert(r, tr)
            if not DoArea and Marker[i] ~= nil and Marker[i] ~= KeyWords["none"] then
                -- set the line to have markers
                table.insert(r, "     marker-start=\"url(#series" .. i .. "marker)\" marker-mid=\"url(#series" .. i .. "marker)\" marker-end=\"url(#series" .. i .. "marker)\"")
            end
            table.insert(r, "     points=\"")
            local lastx = 0
            -- multiply numeric values as necessary
            for k, v in ipairs(SeriesData[i]) do
                local px = tonumber(v[1])
                if #GroupText > 0 then
                    px = iif(DoGroupsTopDown, #GroupText - px, px - 1) * GroupWidth
                        + (GroupWidth / 2)
                else
                    px = px * Mult.x
                end

                local py = tonumber(v[2])
                if DoYAxis2 and YAxis2[i] ~= nil then
                    py = py * Mult.y2
                else
                    py = py * Mult.y
                end
                
                if DoHorizontal then
                    if DoArea and k == 1 then
                        table.insert(r, "     " .. round(iif(Parms["YMin"] > 0, Parms["YMin"] * Mult.y, 0), 2) .. ", " .. round(-px, 2) .. "")
                    end
                    table.insert(r, "     " .. round(py, 2) .. ", " .. round(-px, 2) .. "")
                    lastx = px
    
                    if Labels[i] ~= nil then
                        LabelPos[i][k] = {}
                        LabelPos[i][k]["x"] = px + Siz.Text.Interline
                        LabelPos[i][k]["y"] = py + Siz.Text.Interline  
                        DoLabels = true
                    end
                else
                    if DoArea and k == 1 then
                        table.insert(r, "     " .. round(px, 2) .. ", " .. round(iif(Parms["YMin"] > 0, -Parms["YMin"] * Mult.y, 0), 2) .. "")
                    end
                    table.insert(r, "     " .. round(px, 2) .. ", " .. round(-py, 2) .. "")
                    lastx = px
    
                    if Labels[i] ~= nil then
                        LabelPos[i][k] = {}
                        LabelPos[i][k]["x"] = px + Siz.Text.Interline
                        LabelPos[i][k]["y"] = py + Siz.Text.Interline  
                        DoLabels = true
                    end
                end
            end
            if DoArea then
                if DoHorizontal then
                    table.insert(r, "     " .. round(iif(Parms["YMin"] > 0, Parms["YMin"] * Mult.y, 0), 2) .. ", " .. round(-lastx, 2) .. "")
                else
                    table.insert(r, "     " .. round(lastx, 2) .. ", " .. round(iif(Parms["YMin"] > 0, -Parms["YMin"] * Mult.y, 0), 2) .. "")
                end
            end
            table.insert(r, "   \"/>")
        end
        table.insert(r, " ")
    end

    if not DoStack100 and DoLabels then
        table.insert(r, "   &lt;!-- == Data Labels == -->")
        table.insert(r, " ")

        table.insert(r, "   <style type=\"text/css\"> <![CDATA[")

        table.insert(r, "     .labeltext {")
        table.insert(r, "       font-size:   " .. Siz.Text.Labels .. "px;")
        table.insert(r, "     }")

        table.insert(r, "   ]]>")
        table.insert(r, "   </style>")
        table.insert(r, " ")

        for i = #SeriesData, 1, -1 do
            if Labels[i] ~= nil then
                for k, v in ipairs(SeriesData[i]) do
                    if SType[i] == KeyWords["bar"] then
                        l = "   <text id=\"series" .. i .. "-" .. k .. "-label\" class=\"labeltext\""
                            .. " x=\"" .. round(LabelPos[i][k]["x"], 2) .. "\""
                            .. " y=\"" .. round(LabelPos[i][k]["y"], 2) .. "\""
                        if DoHorizontal then
                            l = l .. " text-anchor=\"" .. iif(tonumber(v[2]) <= 0, "end", "start") .. "\""
                                .. " transform=\"translate(" .. 0 .. ", " .. round(Siz.Text.Labels / 3) .. ")\"" 
                        else
                            l = l .. " text-anchor=\"middle\""
                        end       
                        l = l .. ">" .. v[2]
                            .. "</text>"
                        table.insert(r, l)
                    else
                        if DoHorizontal then
                            table.insert(r, "   <text id=\"series" .. i .. "-" .. k .. "-label\" class=\"labeltext\""
                                .. " x=\"" .. round(LabelPos[i][k]["y"], 2) .. "\""
                                .. " y=\"" .. round(-LabelPos[i][k]["x"], 2) .. "\""
                                .. " text-anchor=\"left\">"
                                .. v[2]
                                .. "</text>")
                        else
                            table.insert(r, "   <text id=\"series" .. i .. "-" .. k .. "-label\" class=\"labeltext\""
                                .. " x=\"" .. round(LabelPos[i][k]["x"], 2) .. "\""
                                .. " y=\"" .. round(-LabelPos[i][k]["y"], 2) .. "\""
                                .. " text-anchor=\"left\">"
                                .. v[2]
                                .. "</text>")
                        end       
                    end
                end
                table.insert(r, " ")
            end
        end
    end
end

function elementsPie()

    table.insert(r, " &lt;!-- == Pie Segments == -->")
    table.insert(r, " ")

    table.insert(r, " <g id=\"segments\" class=\"series-areas-general\" transform=\"translate(" .. Siz.Space.Left + PieOriginX .. ", " .. Siz.Space.Top + PieOriginY .. ")\">")

    if Parms["IncludeOriginalData"] == KeyWords["no"]
            or (Parms["IncludeOriginalData"] == KeyWords["auto"] and DataPointsCount > AutoDataPointsLimit) then
        table.insert(r, "   &lt;!-- original data: not included -->")
    else
        table.insert(r, "   &lt;!-- original data:")

        for k, v in ipairs(OriginalData) do
            table.insert(r, "     " .. v[1] .. " " .. v[2])
        end
        table.insert(r, "   -->")
    end

    local Total = 0
    for k, v in ipairs(SeriesData) do
        Total = Total + v[2]
    end

    local TwoPi = math.pi * 2
    
    local Sweep = 0
    if Parms["PieSweepDir"] ~= "AntiClockwise" then
        -- change arcs to clockwise
        Sweep = 1
    end

    local Radian = 0

    if Parms["PieStartAngle"] ~= 0 then
        Radian = math.rad(Parms["PieStartAngle"])
    end

    local InnerX, InnerY, InnerRadius = 0, 0, 0
    if Parms["DoughnutHole"] == nil then
        -- no doughnut, segments start at pie origin (0, 0)
    else
        -- doughnut, segments start at the hole radius
        InnerRadius = Parms["DoughnutHole"] / 100 * PieRadius

        InnerX = math.cos(Radian) * InnerRadius
        InnerY = math.sin(Radian) * InnerRadius
    end

    -- outer arcs start at the pie radius
    local OuterX = math.cos(Radian) * PieRadius
    local OuterY = math.sin(Radian) * PieRadius
    
    local Mid = {}

    for k, v in ipairs(SeriesData) do
        local Arc = 0 -- default is short arc (<= 180 degrees)
        if (v[2] / Total * TwoPi) > math.pi then
            Arc = 1 -- long arc
        end

        Mid[k] = Radian + ((v[2] / Total * TwoPi) / 2 * iif(Sweep == 0, 1, -1))

        -- adjust X and Y for explode
        local DX, DY = 0, 0
        if Parms["Explode"] == nil or (type(Parms["Explode"]) == "number" and k > Parms["Explode"]) then
            -- no explode for this segment
        else
            DX = math.cos(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100)
            DY = math.sin(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100)
        end

        Radian = Radian + (v[2] / Total * TwoPi * iif(Sweep == 0, 1, -1))
        local NextOuterX = math.cos(Radian) * PieRadius
        local NextOuterY = math.sin(Radian) * PieRadius

        t = "   <path id=\"segment" .. k .. "\" class=\"series" .. k .. "\" d=\"M " .. round(0 + InnerX + DX, 2) .. ", " .. -round(0 + InnerY + DY, 2)
            .. " l " .. round(OuterX - InnerX, 2) .. ", " .. -round(OuterY - InnerY, 2)
            .. " a " .. PieRadius .. ", " .. PieRadius .. " 0 " .. Arc .. " " .. Sweep  .. " " .. round(NextOuterX - OuterX, 2) .. ", " .. -round(NextOuterY - OuterY, 2)
        if Parms["DoughnutHole"] == nil then
            t = t .. " z\" />"
        else
            local NextInnerX = math.cos(Radian) * InnerRadius
            local NextInnerY = math.sin(Radian) * InnerRadius

            t = t .. " l " .. round(NextInnerX - NextOuterX, 2).. ", " .. -round(NextInnerY - NextOuterY, 2)
                .. " a " .. InnerRadius .. ", " .. InnerRadius .. " 0 " .. Arc .. " " .. iif(Sweep == 0, 1, 0) .. " " .. round(InnerX - NextInnerX, 2) .. ", " .. -round(InnerY - NextInnerY, 2) .. "\" />"

            InnerX = NextInnerX
            InnerY = NextInnerY
        end

        table.insert(r, t)

        OuterX = NextOuterX
        OuterY = NextOuterY
    end

    table.insert(r, " </g>")
    table.insert(r, " ")

    if Parms["SegmentText"] ~= nil then

        -- pie segment texts

        table.insert(r, " &lt;!-- == Pie Segment Texts == -->")
        table.insert(r, " ")

        table.insert(r, " <style type=\"text/css\"> <![CDATA[")

        table.insert(r, "   .segmenttext {")
        table.insert(r, "     font-size:   " .. FontSiz.LegendText .. "px;")
        table.insert(r, "   }")

        table.insert(r, " ]]>")
        table.insert(r, " </style>")
        table.insert(r, " ")

        table.insert(r, " <g id=\"segmenttexts\" class=\"segmenttext\" transform=\"translate(" .. Siz.Space.Left + PieOriginX .. ", " .. Siz.Space.Top + PieOriginY .. ")\">")

        for k, v in ipairs(SeriesData) do
            -- adjust X and Y for explode
            local DX, DY = 0, 0
            if Parms["Explode"] == nil or (type(Parms["Explode"]) == "number" and k > Parms["Explode"]) then
                -- no explode for this segment
            else
                DX = math.cos(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100)
                DY = math.sin(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100)
            end

            local TextX = math.cos(Mid[k]) * (PieRadius * Parms["SegmentTextRadius"] / 100)
            local TextY = math.sin(Mid[k]) * (PieRadius * Parms["SegmentTextRadius"] / 100)
            if TextY < 0 then
                TextY = TextY - FontSiz.LegendText
            end

            t = "   <text id=\"segment" .. k .. "text\""
                .. " x=\"" .. round(TextX + DX, 2) .. "\" y=\"" .. -round(TextY + DY, 2) .. "\""
            t = t .. " text-anchor=\"" .. iif(TextX < 0, "end", "start") .. "\">"

            local tt = ""            
            if string.find(Parms["SegmentText"], KeyWords["text"], 1, true) ~= nil then
                tt = SeriesText[k]
            end
            if string.find(Parms["SegmentText"], KeyWords["value"], 1, true) ~= nil then
                tt = tt .. iif(string.len(tt) > 0, " ", "") .. v[2]
            end
            if string.find(Parms["SegmentText"], KeyWords["percent"], 1, true) ~= nil then
                tt = tt .. iif(string.len(tt) > 0, " ", "") .. round(v[2] / Total * 100, 0) .. "%"
            end
            t = t .. tt
            table.insert(r, t .. "</text>")
        end
        table.insert(r, " </g>")
        table.insert(r, " ")
    end
end

function codeAxes()

    if Parms["Debug"] ~= nil and string.find(Parms["Debug"], "pos") ~= nil then
        -- show positions tables
        table.insert(r, "&lt;!--")
        listTable(Mult, "  ", "Mult")
        listTable(FontSiz, "  ", "FontSiz")
        table.sort(Pos)
        listTable(Pos, "  ", "Pos")
        table.sort(Siz)
        listTable(Siz, "  ", "Siz")
        table.insert(r, "-->")
    end
     
    -- axis styles

    table.insert(r, " &lt;!-- == Axis Styles == -->")
    table.insert(r, " ")

    table.insert(r, " <style type=\"text/css\"> <![CDATA[")

    table.insert(r, "   .axisline-x {")
    table.insert(r, "     stroke:         black;")
    table.insert(r, "     stroke-width:   " .. BaseLineWidth * 2 .. ";")
    table.insert(r, "     stroke-linecap: butt;")
    table.insert(r, "   }")

    table.insert(r, "   .axisline-y {")
    table.insert(r, "     stroke:         " .. Parms["YAxisColor"] .. ";")
    table.insert(r, "     stroke-width:   " .. BaseLineWidth * 2 .. ";")
    table.insert(r, "     stroke-linecap: butt;")
    table.insert(r, "   }")

    if DoYAxis2 then
        table.insert(r, "   .axisline-y2 {")
        table.insert(r, "     stroke:         " .. Parms["YAxis2Color"] .. ";")
        table.insert(r, "     stroke-width:   " .. BaseLineWidth * 2 .. ";")
        table.insert(r, "     stroke-linecap: butt;")
        table.insert(r, "   }")
    end

    if Parms["XAxisArrows"] ~= nil or Parms["YAxisArrows"] ~= nil then
        table.insert(r, "   .axis-arrow {")
        table.insert(r, "     stroke-width:    " .. BaseLineWidth .. ";")
        table.insert(r, "     fill:            black;")
        table.insert(r, "     stroke-linejoin: miter;")
        table.insert(r, "   }")

        if Parms["XAxisArrows"] ~= nil then
            -- marker stroke and fill color is the same as the axis
            table.insert(r, "   .axis-arrow-x {")
            table.insert(r, "     stroke: black;")
            table.insert(r, "     fill:   black;")
            table.insert(r, "   }")
        end

        if Parms["YAxisArrows"] ~= nil then
            -- marker stroke and fill color is the same as the axis
            table.insert(r, "   .axis-arrow-y {")
            table.insert(r, "     stroke: " .. Parms["YAxisColor"] .. ";")
            table.insert(r, "     fill:   " .. Parms["YAxisColor"] .. ";")
            table.insert(r, "   }")
        end
    end

    table.insert(r, "   .axismark-main {")
    table.insert(r, "     stroke:       black;")
    table.insert(r, "     stroke-width: " .. BaseLineWidth .. ";")
    table.insert(r, "   }")
    
    table.insert(r, "   .axismark-second {")
    table.insert(r, "     stroke:       black;")
    table.insert(r, "     stroke-width: " .. BaseLineWidth .. ";")
    table.insert(r, "   }")
    
    table.insert(r, "   .axistitle-x {")
    table.insert(r, "     font-size: " .. FontSiz.XAxisTitle .. "px;")
    table.insert(r, "   }")

    table.insert(r, "   .axisnumber-x {")
    table.insert(r, "     font-size: " .. FontSiz.XAxisValues .. "px;")
    table.insert(r, "   }")

    table.insert(r, "   .axistitle-y {")
    table.insert(r, "     font-size: " .. FontSiz.YAxisTitle .. "px;")
    table.insert(r, "   }")

    table.insert(r, "   .axisnumber-y {")
    table.insert(r, "     font-size: " .. FontSiz.YAxisValues .. "px;")
    table.insert(r, "   }")

    if DoYAxis2 then
        table.insert(r, "   .axistitle-y2 {")
        table.insert(r, "     font-size: " .. FontSiz.YAxis2Title .. "px;")
        table.insert(r, "   }")

        table.insert(r, "   .axisnumber-y2 {")
        table.insert(r, "     font-size: " .. FontSiz.YAxis2Values .. "px;")
        table.insert(r, "   }")

    end

    table.insert(r, " ]]>")
    table.insert(r, " </style>")
    table.insert(r, " ")

    table.insert(r, " &lt;!-- == Axis Marks == -->")
    table.insert(r, " ")

    if DoHorizontal then
        codeAxisMarks('x',
            'y',
            -1,
            Pos.XAxis.Line,
            -Parms["XMax"],
            Siz.ChartHeight)

        codeAxisMarks('y',
            'x',
            0,
            Pos.YAxis.Line,
            Parms["YMin"],
            Siz.ChartWidth)
        if DoYAxis2 then
            codeAxisMarks('y2',
                'x',
                -1,
                Pos.YAxis2.Line,
                Parms["Y2Min"],
                Siz.ChartWidth)
        end
    else
        codeAxisMarks('x',
            'x',
            0,
            Pos.XAxis.Line,
            Parms["XMin"],
            Siz.ChartWidth)
    
        codeAxisMarks('y',
            'y',
            -1,
            Pos.YAxis.Line,
            -Parms["YMax"],
            Siz.ChartHeight)
    
        if DoYAxis2 then
            codeAxisMarks('y2',
                'y',
                0,
                Pos.YAxis2.Line,
                -Parms["Y2Max"],
                Siz.ChartHeight)
        end
    end

    if Parms["XAxisArrows"] ~= nil or Parms["YAxisArrows"] ~= nil then
        table.insert(r, " &lt;!-- == Axis Arrows == -->")
        table.insert(r, " ")
        
        local ArrowSize = BaseLineWidth * 12
    
        table.insert(r, " <defs>")
        -- start arrow, for axes
        if Parms["XMin"] < 0 or Parms["YMin"] < 0 then
            table.insert(r, "   <g id=\"axis-arrow-start-shape\">")
            t = round(ArrowSize / 2, 2)
            table.insert(r, "     <polygon points=\"" .. t .. "," .. -t .. " " .. 0 .. "," .. 0 .. " " .. t .. "," .. t .. " " .. -t .. "," .. 0 .. "\"/>")
            table.insert(r, "   </g>")
        end

        table.insert(r, "   <g id=\"axis-arrow-end-shape\">")
        t = round(ArrowSize / 2, 2)
        table.insert(r, "     <polygon points=\"" .. -t .. "," .. -t .. " " .. 0 .. "," .. 0 .. " " .. -t .. "," .. t .. " " .. t .. "," .. 0 .. "\"/>")
        table.insert(r, "   </g>")
        table.insert(r, " ")
       
        if Parms["XAxisArrows"] ~= nil then
            if Parms["XMin"] < 0 then
                l = "   <marker id=\"axis-arrow-x-start\""
                l = l .. " class=\"axis-arrow axis-arrow-x\""
                l = l .. " viewBox=\"0 0 " .. round(ArrowSize, 2) .. " " .. round(ArrowSize, 2) .. "\""
                    .. " markerWidth=\"" .. round(ArrowSize, 2) .. "\""
                    .. " markerHeight=\"" .. round(ArrowSize, 2) .. "\""
                    .. " overflow=\"visible\""
                    .. " orient=\"auto\""
                    .. " markerUnits=\"userSpaceOnUse\">"
                table.insert(r, l)
                table.insert(r, "     <use xlink:href=\"#axis-arrow-start-shape\"/>")
                table.insert(r, "   </marker>")
            end

            l = "   <marker id=\"axis-arrow-x-end\""
            l = l .. " class=\"axis-arrow axis-arrow-x\""
            l = l .. " viewBox=\"0 0 " .. round(ArrowSize, 2) .. " " .. round(ArrowSize, 2) .. "\""
                .. " markerWidth=\"" .. round(ArrowSize, 2) .. "\""
                .. " markerHeight=\"" .. round(ArrowSize, 2) .. "\""
                .. " overflow=\"visible\""
                .. " orient=\"auto\""
                .. " markerUnits=\"userSpaceOnUse\">"
            table.insert(r, l)
            table.insert(r, "     <use xlink:href=\"#axis-arrow-end-shape\"/>")
            table.insert(r, "   </marker>")
        end
    
        if Parms["YAxisArrows"] ~= nil then 
            if Parms["YMin"] < 0 then
                l = "   <marker id=\"axis-arrow-y-start\""
                l = l .. " class=\"axis-arrow axis-arrow-y\""
                l = l .. " viewBox=\"0 0 " .. round(ArrowSize, 2) .. " " .. round(ArrowSize, 2) .. "\""
                    .. " markerWidth=\"" .. round(ArrowSize, 2) .. "\""
                    .. " markerHeight=\"" .. round(ArrowSize, 2) .. "\""
                    .. " overflow=\"visible\""
                    .. " orient=\"auto\""
                    .. " markerUnits=\"userSpaceOnUse\">"
                table.insert(r, l)
                table.insert(r, "     <use xlink:href=\"#axis-arrow-start-shape\"/>")
                table.insert(r, "   </marker>")
            end

            l = "   <marker id=\"axis-arrow-y-end\""
            l = l .. " class=\"axis-arrow axis-arrow-y\""
            l = l .. " viewBox=\"0 0 " .. round(ArrowSize, 2) .. " " .. round(ArrowSize, 2) .. "\""
                .. " markerWidth=\"" .. round(ArrowSize, 2) .. "\""
                .. " markerHeight=\"" .. round(ArrowSize, 2) .. "\""
                .. " overflow=\"visible\""
                .. " orient=\"auto\""
                .. " markerUnits=\"userSpaceOnUse\">"
            table.insert(r, l)
            table.insert(r, "     <use xlink:href=\"#axis-arrow-end-shape\"/>")
            table.insert(r, "   </marker>")
        end
        
        table.insert(r, " </defs>")
        table.insert(r, " ")
    end
    
    table.insert(r, " &lt;!-- == Axis Lines == -->")
    table.insert(r, " ")
    
    if DoHorizontal then
        codeAxisLine('x', 'y', Pos.XAxis.Line, -Parms["XMin"], -Siz.ChartHeight)
            
        codeAxisLine('y', 'x', Pos.YAxis.Line, Parms["YMin"], Siz.ChartWidth)

        if DoYAxis2 then
            codeAxisLine('y2', 'x', Pos.YAxis2.Line, Parms["Y2Min"], Siz.ChartWidth)
        end
    else
        codeAxisLine('x', 'x', Pos.XAxis.Line, Parms["XMin"], Siz.ChartWidth)
            
        codeAxisLine('y', 'y', Pos.YAxis.Line, -Parms["YMin"], -Siz.ChartHeight)

        if DoYAxis2 then
            codeAxisLine('y2', 'y', Pos.YAxis2.Line, -Parms["Y2Min"], -Siz.ChartHeight)
        end
    end
    table.insert(r, " ")
    
    Format = {}

    if #GroupText == 0 then
        Format.x = (Parms["XMax"] >= 10000)
        if Parms["XAxisValueFormat"] ~= nil then
            Format.x = not (Parms["XAxisValueFormat"] == 'none')
        end
    end
    Format.y = (Parms["YMax"] >= 10000)
    if Parms["YAxisValueFormat"] ~= nil then
        Format.y = not (Parms["YAxisValueFormat"] == 'none')
    end
    if DoYAxis2 then
        Format.y2 = (Parms["Y2Max"] >= 10000)
        if Parms["YAxis2ValueFormat"] ~= nil then
            Format.y2 = not (Parms["YAxis2ValueFormat"] == 'none')
        end
    end
    
    table.insert(r, " &lt;!-- == Axis Values == -->")
    table.insert(r, " ")

    if DoHorizontal then
        codeAxisValues('x', 'y',
            Parms["XAxisValueStep"],
            -1, -1,
            Parms["XMin"], Parms["XMax"],
            FontSiz["XAxisValues"] / 3, Pos.XAxis.Values,
            Parms["XAxisValueMultiplier"], Parms["XAxisValueRound"], Parms["XAxisValueAbsolute"],
            Parms["XAxisValuePrefix"], Parms["XAxisValueSuffix"],
            Format.x)
        codeAxisValues('y', 'x',
            Parms["YAxisValueStep"],
            0, 1,
            Parms["YMin"], Parms["YMax"],
            0, Pos.YAxis.Values,
            Parms["YAxisValueMultiplier"], Parms["YAxisValueRound"], Parms["YAxisValueAbsolute"],
            Parms["YAxisValuePrefix"], Parms["YAxisValueSuffix"],
            Format.y)

        if DoYAxis2 then
            codeAxisValues('y2', 'x',
                Parms["YAxis2ValueStep"],
                -1, 1,
                Parms["Y2Min"], Parms["Y2Max"],
                0, Pos.YAxis2.Values,
                Parms["YAxis2ValueMultiplier"], Parms["YAxis2ValueRound"], Parms["YAxis2ValueAbsolute"],
                Parms["YAxis2ValuePrefix"], Parms["YAxis2ValueSuffix"],
                Format.y2)
        end
    else
        codeAxisValues('x', 'x',
            Parms["XAxisValueStep"],
            0, 1,
            Parms["XMin"], Parms["XMax"],
            0, Pos.XAxis.Values,
            Parms["XAxisValueMultiplier"], Parms["XAxisValueRound"], Parms["XAxisValueAbsolute"],
            Parms["XAxisValuePrefix"], Parms["XAxisValueSuffix"],
            Format.x)
            
        codeAxisValues('y', 'y',
            Parms["YAxisValueStep"],
            -1, -1,
            Parms["YMin"], Parms["YMax"],
            FontSiz["YAxisValues"] / 3, Pos.YAxis.Values,
            Parms["YAxisValueMultiplier"], Parms["YAxisValueRound"], Parms["YAxisValueAbsolute"],
            Parms["YAxisValuePrefix"], Parms["YAxisValueSuffix"],
            Format.y)

        if DoYAxis2 then
            codeAxisValues('y2', 'y',
                Parms["YAxis2ValueStep"],
                0, -1,
                Parms["Y2Min"], Parms["Y2Max"],
                FontSiz["YAxis2Values"] / 3, Pos.YAxis2.Values,
                Parms["YAxis2ValueMultiplier"], Parms["YAxis2ValueRound"], Parms["YAxis2ValueAbsolute"],
                Parms["YAxis2ValuePrefix"], Parms["YAxis2ValueSuffix"],
                Format.y2)
        end
    end

    table.insert(r, " &lt;!-- End Axis Chart Translate== -->")
    table.insert(r, " </g>")
    table.insert(r, " ")
    
    -- axis titles are always outside the chart, so are outside the chart translate
    if Parms["XAxisTitle"] ~= nil
            or Parms["YAxisTitle"] ~= nil 
            or (DoYAxis2 and Parms["YAxis2Title"] ~= nil) then
        table.insert(r, " &lt;!-- == Axis Titles == -->")
        table.insert(r, " ")

        if DoHorizontal then
            if Parms["XAxisTitle"] ~= nil then
                codeAxisTitle('x', 'y', Siz.Space.Top + (Siz.ChartHeight * 0.5))
            end
        
            if Parms["YAxisTitle"] ~= nil then
                codeAxisTitle('y', 'x', Siz.Space.Left + (Siz.ChartWidth * 0.5))
            end
        
            if DoYAxis2 and Parms["YAxis2Title"] ~= nil then
                codeAxisTitle('y2', 'x', Siz.Space.Left + (Siz.ChartWidth * 0.5))
            end
        else    
            if Parms["XAxisTitle"] ~= nil then
                codeAxisTitle('x', 'x', Siz.Space.Left + (Siz.ChartWidth * 0.5))
            end
        
            if Parms["YAxisTitle"] ~= nil then
                codeAxisTitle('y', 'y', Siz.Space.Top + (Siz.ChartHeight * 0.5))
            end
        
            if DoYAxis2 and Parms["YAxis2Title"] ~= nil then
                codeAxisTitle('y2', 'y', Siz.Space.Top + (Siz.ChartHeight * 0.5))
            end
        end
        table.insert(r, " ")
    end
end

function codeAxisMarks(AxisName,
    ChangeDim, 
    MarkOffset,
    AxisLinePos, 
    AxisStart, 
    LineLength)

    -- AxisName, text: the name of the axis being coded, "x", "y" or "y2"
    -- ChangeDim, text: 'x' or 'y' - the dimension the axis actually changes in
    -- MarkOffset, numeric: direction of mark start away from the line, -1 or 0
    -- AxisLinePos, numeric: position of the axis, ie: distance of the line from the chart origin point
    -- AxisStart, numeric: position of the start of the axis line
    -- LineLength, numeric: the length of the axis line

    local OtherDim = "y"
    if ChangeDim == "y" then
        OtherDim = "x"
    end

    local AxisParmName = string.upper(AxisName) .. "Axis"
    if AxisName == "y2" then
        AxisParmName = "YAxis2"
    end
    
    local MarkGapDim = "width"
    local MarkSizDim = "height"
    if ChangeDim == "y" then
        MarkGapDim = "height"
        MarkSizDim = "width"
    end

    if AxisName == "x" and #GroupText ~= 0 then
        -- no marks for x axis with groups
    else
        table.insert(r, " <defs>")
        table.insert(r, "   <pattern id=\"" .. AxisName .. "-axismark-main\""
            .. " " .. MarkGapDim .. "=\"" .. round(Parms[AxisParmName .. "ValueStep"] * Mult[AxisName], 2) .. "\""
            .. " " .. MarkSizDim .. "=\"" .. Siz.AxisMark .. "\" patternUnits=\"userSpaceOnUse\">")
        table.insert(r, "     <line x1=\"0\" y1=\"0\" " .. ChangeDim .. "2=\"0\" " .. OtherDim .. "2=\"" .. Siz.AxisMark .. "\" class=\"axismark-main\"/>")
        table.insert(r, "   </pattern>")

        if Parms[AxisParmName .. "Mark2Step"] ~= nil then
            table.insert(r, "   <pattern id=\"" .. AxisName .. "-axismark-second\""
                .. " " .. MarkGapDim .. "=\"" .. round(Parms[AxisParmName .. "Mark2Step"] * Mult[AxisName], 2) .. "\""
                .. " " .. MarkSizDim .. "=\"" .. Siz.AxisMark2 .. "\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <line x1=\"0\" y1=\"0\" " .. ChangeDim .. "2=\"0\" " .. OtherDim .. "2=\"" .. Siz.AxisMark2 .. "\" class=\"axismark-second\"/>")
            table.insert(r, "   </pattern>")
        end
        table.insert(r, " </defs>")
        table.insert(r, " ")

        if Parms[AxisParmName .. "Mark2Step"] ~= nil then
            table.insert(r, " <rect id=\"" .. AxisName .. "-axismark2\""
                .. " " .. ChangeDim .. "=\"" .. round(AxisStart * Mult[AxisName], 2) .. "\""
                .. " " .. OtherDim .. "=\"" .. round(AxisLinePos + (Siz.AxisMark2 * MarkOffset), 2) .. "\""
                .. " " .. MarkGapDim .. "=\"" .. LineLength + BaseLineWidth .. "\""
                .. " " .. MarkSizDim .. "=\"" .. Siz.AxisMark2 .. "\""
                .. " fill=\"url(#" .. AxisName .. "-axismark-second)\"/>")
        end

        table.insert(r, " <rect id=\"" .. AxisName .. "-axismark\""
            .. " " .. ChangeDim .. "=\"" .. round(AxisStart * Mult[AxisName], 2) .. "\""
            .. " " .. OtherDim .. "=\"" .. round(AxisLinePos + (Siz.AxisMark * MarkOffset), 2) .. "\""
            .. " " .. MarkGapDim .. "=\"" .. LineLength + BaseLineWidth .. "\""
            .. " " .. MarkSizDim .. "=\"" .. Siz.AxisMark .. "\""
            .. " fill=\"url(#" .. AxisName .. "-axismark-main)\"/>")

        table.insert(r, " ")
    end
end

function codeAxisLine(AxisName,
    ChangeDim,
    AxisLinePos,
    AxisStart, LineLength)

    local OtherDim = "y"
    if ChangeDim == "y" then
        OtherDim = "x"
    end
    
    local l = " <line id=\"" .. AxisName .. "-axis\""
    if Parms[string.upper(AxisName) .. "AxisArrows"] ~= nil then
        if Parms[string.upper(AxisName) .. "Min"] < 0 then
            l = l .. " marker-start=\"url(#axis-arrow-" .. AxisName .. "-start)\""
        end
        l = l .. " marker-end=\"url(#axis-arrow-" .. AxisName .. "-end)\""
    end
    l = l .. " " .. ChangeDim .. "1=\"" .. round(AxisStart * Mult[AxisName], 2) .. "\""
        .. " " .. OtherDim .. "1=\"" .. round(AxisLinePos, 2) .. "\""
        .. " " .. ChangeDim .. "2=\"" .. round((AxisStart * Mult[AxisName]) + LineLength, 2) .. "\""
        .. " " .. OtherDim .. "2=\"" .. round(AxisLinePos, 2) .. "\" class=\"axisline-" .. AxisName .. "\"/>"

    table.insert(r, l)
end

function codeAxisValues(AxisName,
    ChangeDim,
    ValueStep,
    MarkOffset,
    PosChangeSign, 
    ValueMin, ValueMax, 
    TextShift, OtherValuesPos,
    ValueMultiplier, ValueRound, ValueAbsolute,
    Prefix, Suffix, 
    Format)
        
    -- AxisName, text: the name of the axis being coded, "x", "y" or "y2"
    -- Parms for location
        -- ChangeDim, text: 'x' or 'y' - the dimension the axis actually changes in
        -- ValueStep, numeric: the step between values (and major marks)
        -- MarkOffset, numeric: direction of mark start away from the line, -1 or 0
        -- PosChangeSign, numeric: the position direction for +ve changes in value, 1 or -1 
        -- ValueMin, numeric: the minimum value shown
        -- ValueMax, numeric: the maximum value shown
        -- TextShift, numeric: amount to shift text, to get it centered with its marks
        -- OtherValuesPos, numeric: alignment position for all values
    -- Parms for values format
        -- ValueMultiplier, numeric
        -- ValueRound, numeric
        -- ValueAbsulute
        -- Prefix, text
        -- Suffix, text
        -- Format, boolean
        
    local AxisParmName = string.upper(AxisName) .. "Axis"
    if AxisName == "y2" then
        AxisParmName = "YAxis2"
    end
    
    local Anchor = "middle"
    local RotateText = ""
    if ChangeDim == "y" then
        if MarkOffset == -1 then
            -- text is left of line
            Anchor = "end"
        else
            -- text is right of line
            Anchor = "start"
        end
    end
    if Parms[AxisParmName .. "ValueRotate"] ~= nil then
        RotateText = " transform=\"rotate(" .. Parms[AxisParmName .. "ValueRotate"] .. ", "
        if ChangeDim == "x" then
            Anchor = "start" 
            if Parms[AxisParmName .. "ValueRotate"] < 0 and MarkOffset == 0 then
                Anchor = "end"
            elseif Parms[AxisParmName .. "ValueRotate"] >= 0 and MarkOffset == -1 then
                Anchor = "end"
            end
        end
    end
    
    local Format = (Parms[string.upper(AxisName) .. "Max"] >= 10000)
    if Parms[AxisParmName .. "ValueFormat"] ~= nil then
        Format = not (Parms[AxisParmName .. "ValueFormat"] == 'none')
    end

    local l = ""
    local Position = 0

    if AxisName ~= 'x' or #GroupText == 0 then
        -- numeric values
        table.insert(r, " <g id=\"" .. AxisName .. "-axis-values\""
            .. " class=\"axisnumber-" .. AxisName .. "\""
            .. " text-anchor=\"" .. Anchor .. "\""
            .. " transform=\"translate("
                .. iif(ChangeDim == "x", 0, round(OtherValuesPos, 2)) .. ", "
                .. iif(ChangeDim == "x", round(OtherValuesPos, 2), TextShift) .. ")\""
            .. ">")

        local ValueStart = 0
        if ValueMin ~= 0 then
            ValueStart = math.ceil(ValueMin / ValueStep) * ValueStep
        end
        local Value = ValueStart
        while Value <= ValueMax do
            Position = Value * Mult[AxisName] * PosChangeSign
            l = "   <text " .. ChangeDim .. "=\"" .. round(Position, 2) .. "\""
            if ChangeDim == 'x' then
                if string.len(RotateText) > 0 then
                    l = l .. RotateText .. round(Position, 2) .. ", 0)\""
                end
            else
                if string.len(RotateText) > 0 then
                    l = l .. RotateText ..  "0, " .. round(Position - TextShift, 2) .. ")\""
                end
            end
            l = l .. ">"
    
            l = l .. Prefix
            local v = Value
            if ValueAbsolute ~= nil then
                v = math.abs(v)
            end
            if Format then
                l = l .. mw.getContentLanguage():formatNum(round(v * ValueMultiplier, ValueRound))
            else
                l = l .. round(v * ValueMultiplier, ValueRound)
            end
            l = l .. Suffix .. "</text>"
            table.insert(r, l)
            Value = Value + ValueStep
        end
    else
        -- group name values
        table.insert(r, " <g id=\"" .. AxisName .. "-axis-values\""
            .. " class=\"axisnumber-" .. AxisName .. "\""
            .. " text-anchor=\"" .. Anchor .. "\""
            .. " transform=\"translate("
                .. iif(ChangeDim == "x", 0, round(OtherValuesPos, 2)) .. ", "
                .. iif(ChangeDim == "x", round(OtherValuesPos, 2), TextShift) .. ")\""
            .. ">")

        for k, v in ipairs(GroupText) do
            Position = ((iif(DoGroupsTopDown, #GroupText - k, k - 1) * GroupWidth)
                    + (GroupWidth / 2)) * PosChangeSign
            l = "   <text " .. ChangeDim .. "=\"" .. round(Position, 2) .. "\""
            if ChangeDim == 'x' then
                if string.len(RotateText) > 0 then
                    l = l .. RotateText .. round(Position, 2) .. ", 0)\""
                end
            else
                if string.len(RotateText) > 0 then
                    l = l .. RotateText ..  "0, " .. round(Position - TextShift, 2) .. ")\""
                end
            end
            l = l .. ">" .. v .. "</text>"

            table.insert(r, l)
        end
    end
    table.insert(r, " </g>")
    table.insert(r, " ")
end

function codeAxisTitle(AxisName,
    ChangeDim,
    TitleCentre)

    local OtherDim = "y"
    if ChangeDim == "y" then
        OtherDim = "x"
    end

    local AxisParmName = string.upper(AxisName) .. "Axis"
    if AxisName == "y2" then
        AxisParmName = "YAxis2"
    end
    
    local l = " <text id=\"title-" .. AxisName .. "\" class=\"axistitle-" .. AxisName .. "\""
        .. " " .. ChangeDim .. "=\"" .. round(TitleCentre, 2) .. "\""
        .. " " .. OtherDim .. "=\"" .. round(Pos[AxisParmName]["Title"], 2) .. "\""
    if ChangeDim == 'y' then
        l = l .. " transform = \"rotate(-90, "
            .. round(Pos[AxisParmName]["Title"], 2)
            .. ", "
            .. round(TitleCentre, 2)
            .. ")\""
    end    
    l = l .. " text-anchor=\"middle\">"
        .. Parms[AxisParmName .. "Title"]
        .. "</text>"
    table.insert(r, l)
end

function commonBottom()

    if Parms["LegendType"] == KeyWords["none"]
        and Parms["Title"] == nil
        and Parms["Footnote"] == nil
        and #ChartText <= 0 then
        -- no common-element styles
    else
        table.insert(r, " &lt;!-- == Common-element Styles == -->")
        table.insert(r, " ")

        table.insert(r, " <style type=\"text/css\"> <![CDATA[")

        if Parms["LegendType"] == KeyWords["none"] then
            -- no legend
        else
            table.insert(r, "   .legendbox {")
            table.insert(r, "     stroke:       " .. Parms["LegendBorder"] .. ";")
            table.insert(r, "     stroke-width: " .. BaseLineWidth .. ";")
            table.insert(r, "     fill:         white;")
            table.insert(r, "   }")
    
            table.insert(r, "   .legendtext {")
            table.insert(r, "     font-size:   " .. FontSiz.LegendText .. "px;")
            table.insert(r, "     text-anchor: start;")
            table.insert(r, "   }")
        end
    
        if #ChartText > 0 then
            table.insert(r, "   .charttext {")
            table.insert(r, "     font-size: " .. FontSiz.ChartText .. "px;")
            table.insert(r, "   }")
        end
    
        if Parms["Title"] ~= nil then
            table.insert(r, "   .titletext {")
            table.insert(r, "     font-size: " .. FontSiz.Title .. "px;")
            table.insert(r, "   }")
        end
    
        if Parms["Footnote"] ~= nil then
            table.insert(r, "   .footnotetext {")
            table.insert(r, "     font-size: " .. FontSiz.Footnote .. "px;")
            table.insert(r, "   }")
        end
    
        table.insert(r, " ]]>")
        table.insert(r, " </style>")
        table.insert(r, " ")
    end

    -- legend

    if Parms["LegendType"] == KeyWords["none"] then
        -- no legend
    else
        table.insert(r, " &lt;!-- == Legend == -->")
        table.insert(r, " ")
        if Parms["LegendSVG"] ~= nil then
            -- replace all of legend with user-supplied SVG code
            table.insert(r, " " .. Parms["LegendSVG"])
            table.insert(r, " ")
        else
            tr = " <g id=\"legend\" transform=\"translate("
            if Parms["LegendType"] == KeyWords["horizontal"] then
                -- horizontal legend
                if Parms["LegendX"] ~= nil then
                    if DoHorizontal then
                        tr = tr .. round(Pos.YAxis.Zero + (Parms["LegendY"] * Mult.y), 2) .. ""
                            .. ", " .. round(Pos.XAxis.Zero - (Parms["LegendX"] * Mult.x), 2)
                    else
                        tr = tr .. round(Pos.XAxis.Zero + (Parms["LegendX"] * Mult.x), 2) .. ""
                            .. ", " .. round(Pos.YAxis.Zero - (Parms["LegendY"] * Mult.y), 2)
                    end
                else
                    tr = tr .. round(Siz.Space.Left, 2) .. ", " .. round(Pos.Legend, 2)
                end
                table.insert(r, tr .. ")\">")

                table.insert(r, "   <rect id=\"legend-background\" class=\"legendbox\" x=\"0\" y=\"0\""
                    .. " width=\"" .. Siz.Legend.Width .. "\""
                    .. " height=\"" .. Siz.Legend.Height .. "\"/>")

                local PosX, PosY = Siz.ChartMargin, Siz.ChartMargin
                for k, v in ipairs(SeriesText) do
                    table.insert(r, " ")
                    codeLegendElement((SType[k] == KeyWords["line"] and not DoArea), k, PosX, PosY, Siz.ChartMargin, Siz.Legend.Text, v, Marker[k])
                    PosX = PosX + Siz.Legend.ElementWidth
                    if math.fmod(k, LegendElementsInWidth) == 0 then
                        -- new line of legend elements
                        PosX = Siz.ChartMargin
                        PosY = PosY + Siz.Legend.ElementHeight
                    end
                end
            else
                -- vertical legend
                if Parms["LegendX"] ~= nil then
                    if DoHorizontal then
                        tr = tr .. round(Pos.YAxis.Zero + (Parms["LegendY"] * Mult.y), 2)
                            .. ", " .. round(Pos.XAxis.Zero - (Parms["LegendX"] * Mult.x), 2)
                    else
                        tr = tr .. round(Pos.XAxis.Zero + (Parms["LegendX"] * Mult.x), 2)
                            .. " " .. round(Pos.YAxis.Zero - (Parms["LegendY"] * Mult.y), 2)
                    end
                else
                    tr = tr .. round(Pos.Legend, 2) .. ", " .. round(Siz.Space.Top + (0.1 * Siz.ChartHeight), 2)
                end
                table.insert(r, tr .. ")\">")

                table.insert(r, "   <rect id=\"legend-background\" class=\"legendbox\" x=\"0\" y=\"0\""
                    .. " width=\"" .. Siz.Legend.Width .. "\""
                    .. " height=\"" .. Siz.Legend.Height .. "\"/>")

                local PosX, PosY = Siz.ChartMargin, Siz.ChartMargin
                for k, v in ipairs(SeriesText) do
                    table.insert(r, " ")
                    codeLegendElement((SType[k] == KeyWords["line"] and not DoArea), k, PosX, PosY, Siz.ChartMargin, Siz.Legend.Text, v, Marker[k])
                    PosY = PosY + Siz.Legend.ElementHeight
                end
            end
            table.insert(r, " </g>")
            table.insert(r, " ")
        end
    end

    -- chart texts

    if #ChartText > 0 then
        table.insert(r, " &lt;!-- == Chart Texts == -->")
        table.insert(r, " ")

        table.insert(r, " <g id=\"charttexts\" class=\"charttext\">")
        for i = 1, #ChartText do
            if ChartText[i] ~= nil then
                if DoHorizontal then
                    table.insert(r, "   <text "
                        .. "x=\"" .. round(Pos.YAxis.Zero + (Parms["ChartText" .. i .. "Y"] * Mult.y), 2) .. "\""
                        .. " " .. "y=\"" .. round(Pos.XAxis.Zero - (Parms["ChartText" .. i .. "X"] * Mult.x), 2) .. "\">"
                        .. ChartText[i] .. "</text>")
                else
                    table.insert(r, "   <text "
                        .. "x=\"" .. round(Pos.XAxis.Zero + (Parms["ChartText" .. i .. "X"] * Mult.x), 2) .. "\""
                        .. " " .. "y=\"" .. round(Pos.YAxis.Zero - (Parms["ChartText" .. i .. "Y"] * Mult.y), 2) .. "\">"
                        .. ChartText[i] .. "</text>")
                end
            end
        end
        table.insert(r, " </g>")
        table.insert(r, " ")
    end
    
    -- title and footnote

    if Parms["Title"] ~= nil then
        table.insert(r, " &lt;!-- == Title Text == -->")
        tr = " <text id=\"title\" class=\"titletext\" text-anchor=\"" .. iif(Parms["TitleX"] ~= nil, "left", "middle") .. "\""
        if Parms["TitleX"] ~= nil then
            if DoHorizontal then
                tr = tr .. " " .. "x=\"" .. round(Pos.YAxis.Zero + (Parms["TitleY"] * Mult.y), 2) .. "\""
                    .. " " .. "y=\"" .. round(Pos.XAxis.Zero - (Parms["TitleX"] * Mult.x), 2) .. "\">"
            else
                tr = tr .. " " .. "x=\"" .. round(Pos.XAxis.Zero + (Parms["TitleX"] * Mult.x), 2) .. "\""
                    .. " " .. "y=\"" .. round(Pos.YAxis.Zero - (Parms["TitleY"] * Mult.y), 2) .. "\">"
            end
        else
            tr = tr .. " x=\"" .. round(Siz.Space.Left + Siz.ChartWidth * 0.5, 2) .. "\""
                .. " y=\"" .. round(Pos.Title, 2) .. "\">"
        end
        table.insert(r, tr .. Parms["Title"] .. "</text>")
        table.insert(r, " ")
    end

    if Parms["Footnote"] ~= nil then
        table.insert(r, " &lt;!-- == Footnote Text == -->")

        tr = " <text id=\"footnote\" class=\"footnotetext\" text-anchor=\"" .. iif(Parms["FootnoteX"] ~= nil, "left", "end") .. "\""
        if Parms["FootnoteX"] ~= nil then
            if DoHorizontal then
                tr = tr .. " " .. "x=\"" .. round(Pos.YAxis.Zero + (Parms["FootnoteY"] * Mult.y), 2) .. "\""
                    .. " " .. "y=\"" .. round(Pos.XAxis.Zero - (Parms["FootnoteX"] * Mult.x), 2) .. "\">"
            else
                tr = tr .. " " .. "x=\"" .. round(Pos.XAxis.Zero + (Parms["FootnoteX"] * Mult.x), 2) .. "\""
                    .. " " .. "y=\"" .. round(Pos.YAxis.Zero - (Parms["FootnoteY"] * Mult.y), 2) .. "\">"
            end
        else
            tr = tr .. " x=\"" .. round(ImageWidth - Siz.ImagePadding.Right, 2) .. "\""
                .. " y=\"" .. round(Pos.Footnote, 2) .. "\">"
        end
        table.insert(r, tr .. Parms["Footnote"] .. "</text>")
        table.insert(r, " ")
    end

    if Parms["ImageForegroundSVG"] ~= nil then
        table.insert(r, " &lt;!-- Image Foreground SVG -->")
        table.insert(r, " " .. Parms["ImageForegroundSVG"] .. "")
        table.insert(r, " ")
    end

    table.insert(r, " </svg>")
    table.insert(r, " ")
end

function codeLegendElement(Lines, SeriesNumber, PosX, PosY, Padding, TextSize, Text, Marker)
    -- one entry in the legend

    -- Lines, boolean
    -- SeriesNumber, numeric
    -- PosX, numeric
    -- PosY, numeric (= the top of the legend element)
    -- Padding, numeric
    -- TextSize, numeric
    -- Text, text
    -- Marker, text

    local l = ""

    if Lines then
        local lineY = round(PosY + TextSize / 2, 2)

        l = "   <polyline id=\"legend-line" .. SeriesNumber .. "\" class=\"series-lines-general series" .. SeriesNumber .. "\""
            .. " points=\""
            .. round(PosX, 2) .. "," .. lineY .. " "
            .. round(PosX + (TextSize * 1), 2) .. "," .. lineY .. " "
            .. round(PosX + (TextSize * 2), 2) .. "," .. lineY .. "\""
        if Marker ~= nil and Marker ~= 0 then
            l = l .. " marker-mid=\"url(#series" .. SeriesNumber .. "marker)\""
        end
        table.insert(r, l .. "/>")
        table.insert(r, "   <text id=\"legend-text" .. SeriesNumber .. "\" class=\"legendtext\""
            .. " x=\"" .. round(PosX + (TextSize * 2) + Padding, 2) .. "\""
            .. " y=\"" .. round(PosY + TextSize, 2) .. "\">"
            .. Text .. "</text>")
    else
        table.insert(r, "   <rect id=\"legend-area" .. SeriesNumber .. "\" class=\"series-areas-general series" .. SeriesNumber .. "\""
            .. " x=\"" .. round(PosX, 2) .. "\""
            .. " y=\"" .. round(PosY, 2) .. "\""
            .. " width=\"" .. TextSize * 2 .. "\""
            .. " height=\"" .. TextSize .. "\"/>")
        table.insert(r, "   <text id=\"legend-text" .. SeriesNumber .. "\" class=\"legendtext\""
            .. " x=\"" .. round(PosX + (TextSize * 2) + Padding, 2) .. "\""
            .. " y=\"" .. round(PosY + TextSize, 2) .. "\">"
            .. Text .. "</text>")
    end
end

function outputDebugInfo()

    if Parms["Debug"] ~= nil and string.find(Parms["Debug"], KeyWords["parms"]) ~= nil then
        -- list all parameters
        table.insert(r, " All Parameters :")
        for k, v in pairs(Args) do
            table.insert(r, "  " .. k .. "=" .. v)
        end
        table.insert(r, " ")

        -- list all recognised parameters sorted by key
        table.insert(r, " Parameters :")
        for k, v in spairs(Parms) do
            table.insert(r, "  " .. k .. "=" .. v .. " (" .. type(v) .. ")")
        end
        table.insert(r, " ")

        if string.find(Parms["Debug"], "parmsstop") ~= nil then
            return false
        end
    end

    if Parms["Debug"] ~= nil and string.find(Parms["Debug"], "tables") ~= nil then
        -- list contents of internal tables

        if DoStack or DoStack100 then
            listTable(OriginalData, "  ", "OriginalData")
        end 
        listTable(SeriesData, "  ", "SeriesData")
        listTable(SType, "  ", "SType")
        listTable(YAxis2, "  ", "YAxis2")
        listTable(Labels, "  ", "Labels")
        listTable(Color, "  ", "Color")
        listTable(LineShow, "  ", "LineShow")
        listTable(LineWidth, "  ", "LineWidth")
        listTable(LineDash, "  ", "LineDash")
        listTable(Marker, "  ", "Marker")
        listTable(MarkerFill, "  ", "MarkerFill")
        listTable(MarkerSize, "  ", "MarkerSize")
        listTable(FillPattern, "  ", "FillPattern")
        listTable(FillPatternColor, "  ", "FillPatternColor")
        listTable(SeriesText, "  ", "SeriesText")
        listTable(GroupText, "  ", "GroupText")
        listTable(ChartText, "  ", "ChartText")
        -- listTable(FontSiz, "  ", "FontSiz")
        -- listTable(Siz, "  ", "Siz")
        -- listTable(Pos, "  ", "Pos")
        -- listTable(Mult, "  ", "Mult")
        -- listTable(Adjusts, "  ", "Adjusts")
        table.insert(r, " ")

        if string.find(Parms["Debug"], "tablesstop") ~= nil then
            return false
        end
    end

    table.insert(r, "----------")
    table.insert(r, " ")
    return true
end

function listTable(Tab, Indent, Title)
    -- lists all contents of a table, iterating over all sub-tables

    if Title ~= nil then
        table.insert(r, " " .. Title .. ":")
    end
    for k, v in pairs(Tab) do
        if type(v) == "table" then
            table.insert(r, Indent .. k .. ":")
            listTable(v, Indent .. " ", nil)
        else
            table.insert(r, Indent .. k .. ": " .. v .. " (" .. type(v) .. ")")
        end
    end
end

function unknownParameters(knowns, regexps)
    -- outputs a table of parameters either not in knowns, or not matching a pattern in regexps

    -- knowns - a table where the values are the known parameters
    -- regexps - a table where the values are lua patterns that parameters may match

    -- modified from module at en.wikipedia.org/wiki/Module:Check_for_unknown_parameters

    local knownparms = {}

    local output = {}

    -- create the lists of known parms and regular expressions
    for k, v in spairs(knowns) do
        v = trim(v)
        knownparms[v] = 1
    end
    
    for k, v in pairs(regexps) do
        regexps[k] = '^' .. v .. '$'
    end
    
    -- check each entry in Args
    for k, v in pairs(Args) do
        if type(k) == 'string' and knownparms[k] == nil then
            local knownflag = false
            for i, regexp in ipairs(regexps) do
                if mw.ustring.match(k, regexp) then
                    knownflag = true
                    break
                end
            end
            if not knownflag then
                local vlen = mw.ustring.len(v)
                v = mw.ustring.sub(v, 1, (vlen < 25) and vlen or 25) 
                table.insert(output, k .. "=" .. v .. ((vlen >= 25) and ' ...' or ''))
            end
        elseif type(k) == 'number' and knownparms[tostring(k)] == nil then
            local vlen = mw.ustring.len(v)
            v = mw.ustring.sub(v, 1, (vlen < 25) and vlen or 25) 
            table.insert(output, k .. "=" .. v .. ((vlen >= 25) and ' ...' or ''))
        end
    end
    
    return output
end

-- utility functions

function iif(test, tret, fret)
    -- if test is true, returns tret if defined, otherwise returns true
    -- if test is false, returns fret if defined, otherwise returns false

    -- note that this function cannot be used to avoid evaluating either tret or fret, as they are both evaluated in the function call

    if test then
        if tret == nil then
            return true
        else
            return tret
        end
    end
    if fret == nil then
        return false
    end
    return fret
end

function trim(s)
    -- the match this pattern returns is the string with any spaces (%s) at the start(^) and end ($) not included
    return s:match('^%s*(.-)%s*$') 
end
    
function round(x, p)
    -- round number x to precision p

    -- p > 0 rounds to p decimal places, eg: round(4.57, 1) = 4.6
    -- p = 0 (or not given) rounds to nearest integer, eg: round(6.6) = 7, round(6.5) = 6
    -- p < 0 rounds to p places above zero, eg: round(147, -1) = 150

    local res

    if type(x) ~= "number" then
        return nil
    end
    if type(p) == "number" then
        -- any decimal places in p are ignored
        p = math.floor(p)
    else
        p = nil
    end
    if p == nil or p == 0 then
        return math.floor(x + 0.5)
    else
        res = x * 10^p
        res = math.floor(res + 0.5)
        res = res * 10^-p
        return res
    end
end

function decPlaces(val)
    -- returns the number of decimal places in a numeric value, or a string convertible to a number
    
    val = tonumber(val)
    if val == nil then
        return 0
    end
    val = tostring(val)
    local point = string.find(val, ".")
    if point == 0 then
        return 0
    end
    return string.len(val) - point
end

function copyTable(from, to)
    -- copies a table to another table
    
    local k, v
    for k, v in ipairs(from) do
        if type(v) == "table" then
            to[k] = {}
            copyTable(v, to[k])
        else
            to[k] = v
        end
    end
end

function spairs(t, order)
    -- returns an iterator function that in turn returns table t in the order of the keys
    --  sort is done using an optional order function

    -- collect the keys
    local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end

    -- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys 
    if order then
        table.sort(keys, function(a,b) return order(t, a, b) end)
    else
        table.sort(keys)
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if keys[i] then
            return keys[i], t[keys[i]]
        end
    end
end

return {
    [KeyWords.barChart] = barChart,
    [KeyWords.lineChart] = lineChart,
    [KeyWords.scatterChart] = scatterChart,
    [KeyWords.mixedChart] = mixedChart,
    [KeyWords.pieChart] = pieChart,
}