Module:Charts SVG

From Omniversalis

Documentation for this module may be created at Module:Charts SVG/doc

-- 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, DoArea, DoStack, DoStack100, DoYAxis2 = false, false, false, false, false
local Msgs = {}

local FontSiz, Siz, Pos, Mult = {}, {}, {}, {}
local ChartWidth, ChartHeight, 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',

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

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

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

    YMin = 'YMin',
    YMax = 'YMax',
    YAxisTitle = 'YAxisTitle',
    YAxisValueStep = 'YAxisValueStep',
    YAxisValueMultiplier = 'YAxisValueMultiplier',
    YAxisValueRound = 'YAxisValueRound',
    YAxisValuePrefix = 'YAxisValuePrefix',
    YAxisValueSuffix = 'YAxisValueSuffix',
    YAxisValueFormat = 'YAxisValueFormat',
    YAxisMark2Step = 'YAxisMark2Step',
    YAxisColor = 'YAxisColor',

    Y2Min = 'Y2Min',
    Y2Max = 'Y2Max',
    YAxis2Title = 'YAxis2Title',
    YAxis2ValueStep = 'YAxis2ValueStep',
    XAxis2ValueMultiplier = 'XAxis2ValueMultiplier',
    YAxis2ValueRound = 'YAxis2ValueRound',
    YAxis2ValuePrefix = 'YAxis2ValuePrefix',
    YAxis2ValueSuffix = 'YAxis2ValueSuffix',
    YAxis2ValueFormat = 'YAxis2ValueFormat',
    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',

    -- 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 = {
    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.",
    }

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)

    -- 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)

    -- 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)

    -- 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)

    -- 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

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

    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 have a default value, ie: it is valid for some parameters to be nil. These are genarally 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 original X position
    Parms["TitleY"] = getParm("TitleY", nil, nil, "n") -- switch to move title from original Y position

    -- footnote

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

    -- 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["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["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["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["XAxisMark2Step"] = getParm("XAxisMark2Step", nil, nil, "n") -- switch for showing x-axis secondary marks

    -- YAxis (Left)

    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["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["YAxisMark2Step"] = getParm("YAxisMark2Step", nil, nil, "n") -- switch for showing y-axis secondary marks

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

    -- YAxis2 (Right)

    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 the second Y axis

    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["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["YAxis2Mark2Step"] = getParm("YAxis2Mark2Step", nil, nil, "n") -- switch for showing y-axis2 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, and for not adding space for it to right space
    Parms["LegendY"] = getParm("LegendY", nil, nil, "n") -- switch for moving the legend from the default position
    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["ChartTextFontSize"])

    -- 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
            -- if Explode is a number, ensure it is of numeric type
            Parms["Explode"] = tonumber(Parms["Explode"])
            -- default explode is 20%
            Parms["ExplodeRadius"] = getParm("ExplodeRadius", nil, nil, "n", 20)
        else
            -- any other value = all, default explode radius is 10%
            Parms["ExplodeRadius"] =  getParm("ExplodeRadius", nil, nil, "n", 10)
        end
    end

    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
    
    -- 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 a named argument (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 Total = {}

    for j = 1, #GroupText do
        Total[j] = 0
        local PosTotal, NegTotal = 0, 0
        for i = 1, #SeriesData do
            if SeriesData[i][j] ~= nil and tonumber(SeriesData[i][j][1]) == j then
                Total[j] = Total[j] + SeriesData[i][j][2]
                if tonumber(SeriesData[i][j][2]) < 0 then
                    NegTotal = NegTotal + SeriesData[i][j][2]
                    SeriesData[i][j][2] = NegTotal
                else
                    PosTotal = PosTotal + SeriesData[i][j][2]
                    SeriesData[i][j][2] = PosTotal
                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] / Total[j] * 100
                end
            end
        end
    end
end

function checkParms()
    -- check for parameter issues

    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, " ===== Charts SVG messages =====")
        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.Labels = 5 * BaseFontSize * Parms["LabelsFontSize"] / 100

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

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

    FontSiz.ChartText = 5 * BaseFontSize * Parms["ChartTextFontSize"] / 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.ChartPadding = 2 * BaseUnit

    Siz.Text.Interline = 2 * BaseFontSize

    Siz.Text.Labels = FontSiz.Labels

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

    Siz.Footnote = iif(Parms["Footnote"] ~= nil and Parms["FootnoteY"] == 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

    -- chart size

    ChartWidth = Parms["ChartWidth"]
    ChartHeight = Parms["ChartHeight"]

    -- legend height and width

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

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

end

function prelimsAxes()

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

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

    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 = ChartWidth / #GroupText
            if DoStack or DoStack100 then
                UnitWidth = round(GroupWidth / 2, 0)
            else
                UnitWidth = round(GroupWidth / (BarSeriesCount + 1), 0)
            end
            BarWidth = round(UnitWidth / (1 + BarSpace), 0)
            BarSpace = round(UnitWidth - BarWidth, 0)
            Mult.X = ChartWidth / #GroupText
        else
            GroupWidth = ChartWidth / #GroupText
            Mult.X = GroupWidth
        end
    else
        Mult.X = ChartWidth / (math.abs(Parms["XMax"] - Parms["XMin"]))
    end

    Mult.Y = ChartHeight / math.abs(Parms["YMax"] - Parms["YMin"])

    if DoYAxis2 then
        Mult.Y2 = ChartHeight / math.abs(Parms["Y2Max"] - Parms["Y2Min"])
    else
        Mult.Y2 = 1
    end

    --

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

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

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

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

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

    Siz.YAxis.Values = (2 * FontSiz.YAxisValues) + Siz.Text.Interline
    Siz.YAxis.Title = iif(Parms["YAxisTitle"] ~= nil, FontSiz.YAxisTitle + Siz.Text.Interline, 0)

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

    -- Bottom Space
    if Parms["YMin"] < 0 then
        -- X axis line is within chart
        Pos.XAxis.Line = Siz.Space.Top + ChartHeight
            - round(((0 - Parms["YMin"]) * Mult.Y), 1)
        Pos.XAxis.Marks = Pos.XAxis.Line
        Pos.XAxis.Values = Pos.XAxis.Line + Siz.AxisMark + FontSiz.XAxisValues
        Siz.Space.Bottom = Siz.ChartPadding
    else
        -- X axis line is at bottom of chart
        Pos.XAxis.Line = Siz.Space.Top + ChartHeight
        Pos.XAxis.Marks = Pos.XAxis.Line
        Siz.Space.Bottom = iif(#GroupText > 0, 0, Siz.AxisMark)
        Pos.XAxis.Values = Siz.Space.Top + ChartHeight + Siz.Space.Bottom + Siz.ChartPadding + FontSiz.XAxisValues
        Siz.Space.Bottom = Siz.Space.Bottom + Siz.ChartPadding + Siz.XAxis.Values
    end
    if Parms["XAxisTitle"] ~= nil then
        Pos.XAxis.Title = Siz.Space.Top + ChartHeight + Siz.Space.Bottom + FontSiz.XAxisTitle
        Siz.Space.Bottom = Siz.Space.Bottom + Siz.XAxis.Title
    end
    if Parms["LegendType"] == KeyWords["horizontal"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Top + 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 + 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 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.ChartPadding
        Pos.YAxis.Line = Siz.Space.Left
            + round(((0 - Parms["XMin"]) * Mult.X), 1)
        Pos.YAxis.Marks = Pos.YAxis.Line - Siz.AxisMark -- pos is left edge of marks box
        Pos.YAxis.Values = Pos.YAxis.Marks - Siz.ChartPadding -- 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.ChartPadding + Siz.AxisMark
        Pos.YAxis.Line = Siz.Space.Left
        Pos.YAxis.Marks = Pos.YAxis.Line - Siz.AxisMark -- pos is left edge of marks box
        Pos.YAxis.Values = Pos.YAxis.Marks - Siz.ChartPadding -- pos is right edge of values box
    end

    -- Right Space
    Pos.YAxis2.Line = Siz.Space.Left + ChartWidth
    Pos.YAxis2.Marks = Pos.YAxis2.Line
    Siz.Space.Right = Siz.AxisMark
    Pos.YAxis2.Values = Pos.YAxis2.Line + Siz.Space.Right + Siz.ChartPadding -- pos is left edge of values box
    Siz.Space.Right = Siz.Space.Right + Siz.ChartPadding + Siz.YAxis2.Values
    Pos.YAxis2.Title = Siz.Space.Left + ChartWidth + Siz.Space.Right + FontSiz.YAxis2Title
    Siz.Space.Right = Siz.Space.Right + Siz.YAxis2.Title
    if Parms["LegendType"] == KeyWords["vertical"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Left + 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

    Pos.XAxis.Zero = Siz.Space.Left - (iif(#GroupText == 0, Parms["XMin"], 0) * Mult.X)
    Pos.YAxis.Zero = Siz.Space.Top + ChartHeight + (Parms["YMin"] * Mult.Y)
    Pos.YAxis2.Zero = Siz.Space.Top + ChartHeight + (Parms["Y2Min"] * Mult.Y2)

    -- Image size

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

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 = iif(Parms["Explode"] ~= nil, (PieRadius * Parms["ExplodeRadius"] / 100), 0)
            + (PieRadius * Parms["SegmentTextRadius"] / 100)
            + (5 * Siz.Text.Chart * Parms["SegmentTextWidth"] / 100)
        PieOriginX = math.max(PieOriginX, TextSpaceX)

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

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

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

    -- Bottom Space
    Siz.Space.Bottom = Siz.ChartPadding

    if Parms["LegendType"] == KeyWords["horizontal"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Top + 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 + 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.ChartPadding

    -- Right Space
    Siz.Space.Right = Siz.ChartPadding
    if Parms["LegendType"] == KeyWords["vertical"] and Parms["LegendX"] == nil then
        Pos.Legend = Siz.Space.Left + 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 + ChartWidth + Siz.Space.Right, 0)
    ImageHeight = round(Siz.Space.Top + ChartHeight + Siz.Space.Bottom, 0)

    -- Mult.X and Mult.Y are needed for positioning of Title, Footnote and Chart texts

    Mult.X = round(ChartWidth / 100, 2)
    Mult.Y = round(ChartHeight / 100, 2)
end

function commonTop()

    -- output header

    table.insert(r, "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''.")
    table.insert(r, " ")

    -- SVG header stuff

    table.insert(r, " &lt;?xml version=\"1.0\" encoding=\"UTF-8\" ?>")
    table.insert(r, " &lt;!-- Generator: en.wikipedia.org/wiki/Module:Charts SVG -->")
    table.insert(r, " &lt;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, " ")

    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=\"" .. ChartWidth .. "\""
            .. " height=\"" .. ChartHeight .. "\""
            .. " fill=\"" .. Parms["ChartBackgroundColor"] .. "\"/>")
        table.insert(r, " ")
    end
end

function codeAxisGrids()

    local XAxisGridStepStretch
    if #GroupText == 0 and tonumber(Parms["XGrid"]) ~= nil then
        XAxisGridStepStretch = round(Parms["XGrid"] * Mult.X, 2)
    end

    local YAxisGridStepStretch
    if tonumber(Parms["YGrid"]) ~= nil then
        YAxisGridStepStretch = round(Parms["YGrid"] * Mult.Y, 2)
    end

    if Parms["XGrid"] ~= KeyWords["none"] or Parms["YGrid"] ~= KeyWords["none"] then
        table.insert(r, " &lt;!-- == Axis 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 #GroupText == 0 and Parms["XGrid"] ~= KeyWords["none"] then
            table.insert(r, "   &lt;!-- x-axis grid lines, vertical -->")
            table.insert(r, "   <pattern id=\"x-gridline\""
               .. " x=\"" .. Pos.XAxis.Zero .. "\""
               .. " y=\"" .. Pos.YAxis.Zero .. "\""
               .. " height=\"" .. ChartHeight .. "\""
               .. " width=\"" .. XAxisGridStepStretch .. "\""
               .. " patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <line x1=\"0\""
                .. " y1=\"0\""
                .. " x2=\"0\""
                .. " y2=\"" .. ChartHeight .. "\""
                .. " class=\"gridline\"/>")
            table.insert(r, "   </pattern>")
        end

        if Parms["YGrid"] ~= KeyWords["none"] then
            table.insert(r, "   &lt;!-- y-axis grid lines, horizontal -->")
            table.insert(r, "   <pattern id=\"y-gridline\""
               .. " x=\"" .. Pos.XAxis.Zero .. "\""
               .. " y=\"" .. Pos.YAxis.Zero .. "\""
               .. " height=\"" .. YAxisGridStepStretch .. "\""
               .. " width=\"" .. ChartWidth .. "\""
               .. " patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <line x1=\"0\""
                .. " y1=\"0\""
                .. " x2=\"" .. ChartWidth .. "\""
                .. " y2=\"0\""
                .. " class=\"gridline\"/>")
            table.insert(r, "   </pattern>")
        end
        table.insert(r, " </defs>")
        table.insert(r, " ")

        if #GroupText == 0 and Parms["XGrid"] ~= KeyWords["none"] then
            table.insert(r, " <rect id=\"x-gridline-area\" x=\"" .. Siz.Space.Left .. "\" y=\"" .. Siz.Space.Top .. "\""
                .. " width=\"" .. round(ChartWidth * 1.01, 1) .. "\""
                .. " height=\"" .. ChartHeight .. "\""
                .. " fill=\"url(#x-gridline)\"/>")
        end

        if Parms["YGrid"] ~= KeyWords["none"] then
            table.insert(r, " <rect id=\"y-gridline-area\" x=\"" .. Siz.Space.Left .. "\" y=\"" .. Siz.Space.Top .. "\""
                .. " width=\"" .. ChartWidth .. "\""
                .. " height=\"" .. round(ChartHeight * 1.01, 1) .. "\""
                .. " fill=\"url(#y-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 LabelPos = {}

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

    table.insert(r, " <g id=\"graphs\" transform=\"translate(" .. Siz.Space.Left .. ", " .. Siz.Space.Top + ChartHeight .. ")\">")
    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 right 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 = round((px - 1) * GroupWidth
                                + UnitWidth / 2
                                + (BarSpace / 2), 1)
                        else
                            px = round((px - 1) * GroupWidth
                                + UnitWidth / 2
                                + UnitWidth * (BarNumber - 1)
                                + (BarSpace / 2), 1)
                        end
                    else
                        px = round(((px - Parms["XMin"]) * Mult.X) - (BarWidth / 2), 1)
                    end
    
                    local val = tonumber(v[2])
                    local top, ht = 0, 0
                    local Min, Multiply = Parms["YMin"], Mult.Y
                    if DoYAxis2 and YAxis2[i] ~= nil then
                        Min = Parms["Y2Min"]
                        Multiply = Mult.Y2
                    end
                    if Min >= 0 then
                        top = Min - val
                        ht = val - Min
                    elseif val >= 0 then
                        top = Min - val
                        ht = val
                    else
                        top = Min
                        ht = -val
                    end
                    top = round(top * Multiply, 1)
                    ht = round(ht * Multiply, 1)
    
                    table.insert(r, "   <rect id=\"series" .. i .. "-" .. k .. "\" class=\"series-areas-general series" .. i .. "\""
                        .. " x=\"" .. px .. "\" y=\"" .. top .. "\""
                        .. " width=\"" .. BarWidth .. "\" height=\"" .. ht .. "\" />")
                    
                    if Labels[i] ~= nil then
                        LabelPos[i][k] = {}
                        LabelPos[i][k]["x"] = px + (BarWidth / 2)
                        if val <= 0 then  
                            LabelPos[i][k]["y"] = top + ht + Siz.Text.Interline + Siz.Text.Labels
                        else  
                            LabelPos[i][k]["y"] = top - Siz.Text.Interline
                        end  
                    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 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 = round((px - 1) * GroupWidth
                        + (GroupWidth / 2), 1)
                else
                    px = round((px - Parms["XMin"]) * Mult.X, 1)
                end

                local py = tonumber(v[2])
                if DoYAxis2 and YAxis2[i] ~= nil then
                    py = round((Parms["Y2Min"] - py) * Mult.Y2, 1)
                else
                    py = round((Parms["YMin"] - py) * Mult.Y, 1)
                end

                if DoArea and k == 1 then
                    table.insert(r, "     " .. px .. ", " .. iif(Parms["YMin"] < 0, Parms["YMin"] * Mult.Y, 0) .. "")
                end
                table.insert(r, "     " .. px .. ", " .. py .. "")
                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  
                end
            end
            if DoArea then
                table.insert(r, "     " .. lastx .. ", " .. iif(Parms["YMin"] < 0, Parms["YMin"] * Mult.Y, 0) .. "")
            end
            table.insert(r, "   \"/>")
        end
        table.insert(r, " ")
    end

    if not DoStack100 and #Labels > 0 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
                        table.insert(r, "   &lt;text id=\"series" .. i .. "-" .. k .. "-label\" class=\"labelstext\""
                            .. " x=\"" .. LabelPos[i][k]["x"] .. "\""
                            .. " y=\"" .. LabelPos[i][k]["y"] .. "\""
                            .. " text-anchor=\"middle\">"
                            .. v[2]
                            .. "</text>")
                    else
                        table.insert(r, "   &lt;text id=\"series" .. i .. "-" .. k .. "-label\" class=\"labelstext\""
                            .. " x=\"" .. LabelPos[i][k]["x"] .. "\""
                            .. " y=\"" .. LabelPos[i][k]["y"] .. "\""
                            .. " text-anchor=\"left\">"
                            .. v[2]
                            .. "</text>")
                    end
                end
            end
        end
    end

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

function elementsPie()

    -- pie segments

    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 = round(math.cos(Radian) * InnerRadius, 1)
        InnerY = round(math.sin(Radian) * InnerRadius, 1)
    end

    -- outer arcs start at the pie radius
    local OuterX = round(math.cos(Radian) * PieRadius, 1)
    local OuterY = round(math.sin(Radian) * PieRadius, 1)
    
    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 = round(math.cos(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100), 1)
            DY = round(math.sin(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100), 1)
        end

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

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

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

            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 = round(math.cos(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100), 1)
                DY = round(math.sin(Mid[k]) * (PieRadius * Parms["ExplodeRadius"] / 100), 1)
            end

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

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

            local tt = ""            
            if string.find(Parms["SegmentText"], KeyWords["text"], 1, true) > 0 then
                tt = SeriesText[k]
            end
            if string.find(Parms["SegmentText"], KeyWords["value"], 1, true) > 0 then
                tt = tt .. iif(string.len(tt) > 0, " ", "") .. v[2]
            end
            if string.find(Parms["SegmentText"], KeyWords["percent"], 1, true) > 0 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()

--[=[ to display positioning tables
    table.insert(r, "&lt;!--")
    listTable(Mult, "  ", "Mult")
    listTable(FontSiz, "  ", "FontSiz")
    listTable(Pos, "  ", "Pos")
    listTable(Siz, "  ", "Siz")
    table.insert(r, "-->")
]=]
 
    -- 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, "   }")

    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, "   }")

    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, " ")

    -- calculate stretched values for grid & mark spacings

    local XAxisMarkStepStretch = round(Parms["XAxisValueStep"] * Mult.X, 3)
    local XAxisMark2StepStretch = 0
    if Parms["XAxisMark2Step"] ~= nil then
        XAxisMark2StepStretch = round(Parms["XAxisMark2Step"] * Mult.X, 3)
    end

    local YAxisMarkStepStretch = round(Parms["YAxisValueStep"] * Mult.Y, 3)
    local YAxisMark2StepStretch = 0
    if Parms["YAxisMark2Step"] ~= nil then
        YAxisMark2StepStretch = round(Parms["YAxisMark2Step"] * Mult.Y, 3)
    end

    local YAxis2MarkStepStretch = round(Parms["YAxis2ValueStep"] * Mult.Y2, 3)
    local YAxis2Mark2StepStretch = 0
    if Parms["YAxis2Mark2Step"] ~= nil then
        YAxis2Mark2StepStretch = round(Parms["YAxis2Mark2Step"] * Mult.Y2, 3)
    end

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

    -- X Axis Marks
    
    if #GroupText == 0 then
        table.insert(r, " <defs>")
        table.insert(r, "   <pattern id=\"x-axismark-main\""
            .. " x=\"" .. Pos.XAxis.Zero .. "\""
            .. " width=\"" .. XAxisMarkStepStretch .. "\""
            .. " height=\"" .. Siz.AxisMark .. "\" patternUnits=\"userSpaceOnUse\">")
        table.insert(r, "     <line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"" .. Siz.AxisMark .. "\" class=\"axismark-main\"/>")
        table.insert(r, "   </pattern>")

        if Parms["XAxisMark2Step"] ~= nil then
            table.insert(r, "   <pattern id=\"x-axismark-second\""
                .. " x=\"" .. Pos.XAxis.Zero .. "\""
                .. " width=\"" .. XAxisMark2StepStretch .. "\""
                .. " height=\"" .. Siz.AxisMark2 .. "\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"" .. Siz.AxisMark2 .. "\" class=\"axismark-second\"/>")
            table.insert(r, "   </pattern>")
        end
        table.insert(r, " </defs>")
        table.insert(r, " ")

        if Parms["XAxisMark2Step"] ~= nil then
            table.insert(r, " <rect id=\"x-axismark2\""
                .. " x=\"" .. Siz.Space.Left .. "\""
                .. " y=\"" .. Pos.XAxis.Marks .. "\""
                .. " width=\"" .. round(ChartWidth * 1.01, 1) .. "\""
                .. " height=\"" .. Siz.AxisMark2 .. "\""
                .. " fill=\"url(#x-axismark-second)\"/>")
        end

        table.insert(r, " <rect id=\"x-axismark\""
            .. " x=\"" .. Siz.Space.Left .. "\""
            .. " y=\"" .. Pos.XAxis.Marks .. "\""
            .. " width=\"" .. round(ChartWidth * 1.01, 1) .. "\""
            .. " height=\"" .. Siz.AxisMark .. "\""
            .. " fill=\"url(#x-axismark-main)\"/>")
        table.insert(r, " ")
    end

    -- Y Axis Marks
    
    table.insert(r, " <defs>")
    table.insert(r, "   <pattern id=\"y-axismark-main\""
        .. " y=\"" .. Pos.YAxis.Zero .. "\""
        .. " width=\"" .. Siz.AxisMark .. "\""
        .. " height=\"" .. YAxisMarkStepStretch .. "\" patternUnits=\"userSpaceOnUse\">")
    table.insert(r, "     <line x1=\"0\" y1=\"0\" x2=\"" .. Siz.AxisMark .. "\" y2=\"0\" class=\"axismark-main\"/>")
    table.insert(r, "   </pattern>")

    if Parms["YAxisMark2Step"] ~= nil then
        table.insert(r, "   <pattern id=\"y-axismark-second\""
            .. " y=\"" .. Pos.YAxis.Zero .. "\""
            .. " width=\"" .. Siz.AxisMark2 .. "\""
            .. " height=\"" .. YAxisMark2StepStretch .. "\" patternUnits=\"userSpaceOnUse\">")
        table.insert(r, "     <line x1=\"0\" y1=\"0\" x2=\"" .. Siz.AxisMark2 .. "\" y2=\"0\" class=\"axismark-second\"/>")
        table.insert(r, "   </pattern>")
    end
    table.insert(r, " </defs>")
    table.insert(r, " ")

    if Parms["YAxisMark2Step"] ~= nil then
        table.insert(r, " <rect id=\"y-axismark2\""
            .. " x=\"" .. Pos.YAxis.Marks + Siz.AxisMark - Siz.AxisMark2 .. "\""
            .. " y=\"" .. Siz.Space.Top .. "\""
            .. " width=\"" .. Siz.AxisMark2 .. "\""
            .. " height=\"" .. round(ChartHeight * 1.01, 1) .. "\""
            .. " fill=\"url(#y-axismark-second)\"/>")

    end
    table.insert(r, " <rect id=\"y-axismark\""
        .. " x=\"" .. Pos.YAxis.Marks .. "\""
        .. " y=\"" .. Siz.Space.Top .. "\""
        .. " width=\"" .. Siz.AxisMark .. "\""
        .. " height=\"" .. round(ChartHeight * 1.01, 1) .. "\""
        .. " fill=\"url(#y-axismark-main)\"/>")
    table.insert(r, " ")

    -- Y Axis 2 Marks
    
    if DoYAxis2 then
        table.insert(r, " <defs>")
        table.insert(r, "   <pattern id=\"y-axis2mark-main\""
            .. " y=\"" .. Pos.YAxis2.Zero .. "\""
            .. " width=\"" .. Siz.AxisMark .. "\""
            .. " height=\"" .. YAxis2MarkStepStretch .. "\" patternUnits=\"userSpaceOnUse\">")
        table.insert(r, "     <line x1=\"0\" y1=\"0\" x2=\"" .. Siz.AxisMark .. "\" y2=\"0\" class=\"axismark-main\"/>")
        table.insert(r, "   </pattern>")

        if Parms["YAxis2Mark2Step"] ~= nil then
            table.insert(r, "   <pattern id=\"y-axis2mark-second\""
                .. " y=\"" .. Pos.YAxis2.Zero.. "\""
                .. " width=\"" .. Siz.AxisMark2 .. "\""
                .. " height=\"" .. YAxis2Mark2StepStretch .. "\" patternUnits=\"userSpaceOnUse\">")
            table.insert(r, "     <line x1=\"0\" y1=\"0\" x2=\"" .. Siz.AxisMark2 .. "\" y2=\"0\" class=\"axismark-second\"/>")
            table.insert(r, "   </pattern>")
        end
        table.insert(r, " </defs>")
        table.insert(r, " ")

        if Parms["YAxis2Mark2Step"] ~= nil then
            table.insert(r, " <rect id=\"y-axis2mark2\""
                .. " x=\"" .. Pos.YAxis2.Marks .. "\""
                .. " y=\"" .. Siz.Space.Top .. "\""
                .. " width=\"" .. Siz.AxisMark2 .. "\""
                .. " height=\"" .. round(ChartHeight * 1.01, 1) .. "\""
                .. " fill=\"url(#y-axis2mark-second)\"/>")
        end
        table.insert(r, " <rect id=\"y-axis2mark\""
            .. " x=\"" .. Pos.YAxis2.Marks .. "\""
            .. " y=\"" .. Siz.Space.Top .. "\""
            .. " width=\"" .. Siz.AxisMark .. "\""
            .. " height=\"" .. round(ChartHeight * 1.01, 1) .. "\""
            .. " fill=\"url(#y-axis2mark-main)\"/>")
        table.insert(r, " ")
    end

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

    table.insert(r, " <line id=\"x-axis\""
        .. " x1=\"" .. Siz.Space.Left .. "\""
        .. " y1=\"" .. Pos.XAxis.Line .. "\""
        .. " x2=\"" .. Siz.Space.Left + ChartWidth .. "\""
        .. " y2=\"" .. Pos.XAxis.Line .. "\" class=\"axisline-x\"/>")

    table.insert(r, " <line id=\"y-axis\""
        .. " x1=\"" .. Pos.YAxis.Line .. "\""
        .. " y1=\"" .. Siz.Space.Top .. "\""
        .. " x2=\"" .. Pos.YAxis.Line .. "\""
        .. " y2=\"" .. Siz.Space.Top + ChartHeight .. "\" class=\"axisline-y\"/>")

    if DoYAxis2 then
        table.insert(r, " <line id=\"y-axis2\""
           .. " x1=\"" .. Pos.YAxis2.Line .. "\""
           .. " y1=\"" .. Siz.Space.Top .. "\""
           .. " x2=\"" .. Pos.YAxis2.Line .. "\""
           .. " y2=\"" .. Siz.Space.Top + ChartHeight .. "\" class=\"axisline-y2\" />")
    end
    table.insert(r, " ")
    
    table.insert(r, " &lt;!-- == Axis Values == -->")
    table.insert(r, " ")

    local Anchor = "middle"
    local RotateText = ""
    if Parms["XAxisValueRotate"] ~= nil then
        if Parms["XAxisValueRotate"] < 0 then
            Anchor = "end"
        else
            Anchor = "start" 
        end
        RotateText = " transform=\"rotate(" .. Parms["XAxisValueRotate"] .. ", "
    end

    if #GroupText == 0 then
        t = (Parms["XMax"] >= 10000)
        if Parms["XAxisValueFormat"] ~= nil then
            t = not (Parms["XAxisValueFormat"] == 'none')
        end
        
        table.insert(r, " <g id=\"x-axis-values\" class=\"axisnumber-x\" text-anchor=\"" .. Anchor .. "\">")
        codeAxisValues("x",
            Pos.XAxis.Zero,
            Parms["XAxisValueStep"],
            Parms["XMin"], Parms["XMax"],
            Mult.X, 0, round(Pos.XAxis.Values, 1), 
            Parms["XAxisValueMultiplier"], Parms["XAxisValueRound"],
            Parms["XAxisValuePrefix"], Parms["XAxisValueSuffix"],
            t, RotateText)
        table.insert(r, " </g>")
        table.insert(r, " ")
    else
        table.insert(r, " <g id=\"x-axis-values\" class=\"axisnumber-x\" transform=\"translate("
            .. Siz.Space.Left .. ", "
            .. Pos.XAxis.Values
            .. ")\" text-anchor=\"" .. Anchor .. "\">")
        for k, v in ipairs(GroupText) do
            local bx = ((k - 1) * GroupWidth)
                    + (GroupWidth / 2)
            table.insert(r, "   <text x=\"" .. round(bx, 1) .. "\""
                .. iif(string.len(RotateText) > 0, RotateText .. round(bx, 1) .. ", " .. -round(FontSiz.XAxisValues / 3, 1) .. ")\"", "")
                .. ">" .. v .. "</text>")
        end
        table.insert(r, " </g>")
        table.insert(r, " ")
    end

    t = (Parms["YMax"] >= 10000)
    if Parms["YAxisValueFormat"] ~= nil then
        t = not (Parms["YAxisValueFormat"] == 'none')
    end

    table.insert(r, " <g id=\"y-axis-values\" class=\"axisnumber-y\" text-anchor=\"end\">")
    codeAxisValues("y",
        Pos.YAxis.Zero,
        Parms["YAxisValueStep"],
        Parms["YMin"], Parms["YMax"],
        Mult.Y, FontSiz["YAxisValues"] / 3, round(Pos.YAxis.Values, 1), 
        Parms["YAxisValueMultiplier"], Parms["YAxisValueRound"],
        Parms["YAxisValuePrefix"], Parms["YAxisValueSuffix"],
        t, "")
    table.insert(r, " </g>")
    table.insert(r, " ")

    if DoYAxis2 then
        t = (Parms["Y2Max"] >= 10000)
        if Parms["YAxis2ValueFormat"] ~= nil then
            t = not (Parms["YAxis2ValueFormat"] == 'none')
        end

        table.insert(r, " <g id=\"y-axis2-values\" class=\"axisnumber-y2\" text-anchor=\"start\">")
        codeAxisValues("y",
            Pos.YAxis2.Zero,
            Parms["YAxis2ValueStep"],
            Parms["Y2Min"], Parms["Y2Max"],
            Mult.Y2, FontSiz["YAxis2Values"] / 3, round(Pos.YAxis2.Values, 1),
            Parms["YAxis2ValueMultiplier"], Parms["YAxis2ValueRound"],
            Parms["YAxis2ValuePrefix"], Parms["YAxis2ValueSuffix"],
            t, "")
        table.insert(r, " </g>")
        table.insert(r, " ")
    end
    
    if Parms["XAxisTitle"] ~= nil
        or Parms["YAxisTitle"] ~= nil 
        or (DoYAxis2 and Parms["YAxis2Title"] ~= nil) then
        table.insert(r, " &lt;!-- == Axis Titles == -->")
        table.insert(r, " ")
    end
    
    if Parms["XAxisTitle"] ~= nil then
        table.insert(r, " <text id=\"title-x\" class=\"axistitle-x\""
            .. " x=\"" .. Siz.Space.Left + (ChartWidth * 0.5) .. "\""
            .. " y=\"" .. round(Pos.XAxis.Title, 1) .. "\" text-anchor=\"middle\">"
            .. Parms["XAxisTitle"]
            .. "</text>")
        table.insert(r, " ")
    end

    if Parms["YAxisTitle"] ~= nil then
        table.insert(r, " <text id=\"title-y\" class=\"axistitle-y\""
            .. " x=\"-" .. Siz.Space.Top + (ChartHeight * 0.5) .. "\""
            .. " y=\"" .. round(Pos.YAxis.Title, 1) .. "\""
            .. " transform=\"rotate(-90)\" text-anchor=\"middle\">"
            .. Parms["YAxisTitle"]
            .. "</text>")
        table.insert(r, " ")
    end

    if DoYAxis2 and Parms["YAxis2Title"] ~= nil then
        table.insert(r, " <text id=\"title-y2\" class=\"axistitle-y2\""
            .. " x=\"-" .. Siz.Space.Top + (ChartHeight * 0.5) .. "\""
            .. " y=\"" .. round(Pos.YAxis2.Title, 1) .. "\""
            .. " transform=\"rotate(-90)\" text-anchor=\"middle\">"
            .. Parms["YAxis2Title"]
            .. "</text>")
        table.insert(r, " ")
    end
end

function codeAxisValues(Axis, PosAxisZero, ValueStep, AxisMin, AxisMax, Stretch, YShift, OtherPos, ValueMultiplier, ValueRound, Prefix, Suffix, Format, XRotateText)

    -- Parms for location
        -- Axis, text: 'x' or 'y'
        -- PosAxisZero, numeric
        -- ValueStep, numeric
        -- AxisMin, numeric
        -- AxisMax, numeric
        -- Stretch, numeric
        -- YShift, numeric
        -- OtherPos, numeric
    -- Parms for format
        -- ValueMultiplier, numeric
        -- ValueRound, numeric
        -- Prefix, text
        -- Suffix, text
        -- Format, boolean
        -- XRotateText, text

    local l = ""

    local ValueStart = 0
    if AxisMin ~= 0 then
        ValueStart = math.ceil(AxisMin / ValueStep) * ValueStep
    end
    local Value = ValueStart
    while Value <= AxisMax do
        if Axis == 'x' then
            Position = PosAxisZero + (Value * Stretch)
            l = "   <text x=\"" .. round(Position, 1) .. "\" y=\"" .. OtherPos .. "\""
                .. iif(string.len(XRotateText) > 0, XRotateText .. round(Position, 1) .. ", " .. OtherPos-round(FontSiz.XAxisValues / 3, 1) .. ")\"", "")
                .. ">"
        else
            Position = PosAxisZero - (Value * Stretch) + YShift
            l = "   <text x=\"" .. OtherPos .. "\" y=\"" .. round(Position, 1) .. "\">"
        end

        if Format then
            l = l .. Prefix .. mw.getContentLanguage():formatNum(round(Value * ValueMultiplier, ValueRound)) .. Suffix .. "</text>"
        else
            l = l .. Prefix .. round(Value * ValueMultiplier, ValueRound) .. Suffix .. "</text>"
        end
        table.insert(r, l)
        Value = Value + ValueStep
    end
end

function commonBottom()

    if (#SeriesText < 1 or 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 #SeriesText < 1 or 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 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
    
        if #ChartText > 0 then
            table.insert(r, "   .charttext {")
            table.insert(r, "     font-size: " .. FontSiz.ChartText .. "px;")
            table.insert(r, "   }")
        end
    
        table.insert(r, " ]]>")
        table.insert(r, " </style>")
        table.insert(r, " ")
    end

    -- legend

    if #SeriesText < 1 or 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
                    tr = tr .. round(Siz.Space.Left + (Parms["LegendX"] * Mult.X), 1)
                else
                    tr = tr .. round(Siz.Space.Left, 1)
                end
                tr = tr .. ", "
                if Parms["LegendY"] ~= nil then
                    tr = tr .. round(Siz.Space.Top + ChartHeight - (Parms["LegendY"] * Mult.Y), 1)
                else
                    tr = tr .. round(Pos.Legend, 1)
                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.ChartPadding, Siz.ChartPadding
                for k, v in ipairs(SeriesText) do
                    table.insert(r, " ")
                    codeLegendElement((SType[k] == KeyWords["line"] and not DoArea), k, PosX, PosY, Siz.ChartPadding, Siz.Legend.Text, v, Marker[k])
                    PosX = PosX + Siz.Legend.ElementWidth
                    if k == LegendElementsInWidth then
                        -- new line of legend elements
                        PosX = Siz.ChartPadding
                        PosY = PosY + Siz.Legend.ElementHeight
                    end
                end
            else
                -- vertical legend
                if Parms["LegendX"] ~= nil then
                    tr = tr .. round(Siz.Space.Left + (Parms["LegendX"] * Mult.X), 1)
                else
                    tr = tr .. round(Pos.Legend, 1)
                end
                tr = tr .. ", "
                if Parms["LegendY"] ~= nil then
                    t = Parms["LegendY"]
                else
                    t = 0.9 * math.abs(Parms["YMax"] - Parms["YMin"])
                end
                table.insert(r, tr .. round(Siz.Space.Top + ChartHeight - (t * Mult.Y), 1) .. ")\">")

                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.ChartPadding, Siz.ChartPadding
                for k, v in ipairs(SeriesText) do
                    table.insert(r, " ")
                    codeLegendElement((SType[k] == KeyWords["line"] and not DoArea), k, PosX, PosY, Siz.ChartPadding, Siz.Legend.Text, v, Marker[k])
                    PosY = PosY + Siz.Legend.ElementHeight
                end
            end
            table.insert(r, " </g>")
            table.insert(r, " ")
        end
    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 or Parms["TitleY"] ~= nil, "left", "middle") .. "\""
            .. " x=\""
        if Parms["TitleX"] ~= nil then
            tr = tr .. round(Siz.Space.Left + (Parms["TitleX"] * Mult.X), 1) .. "\""
        else
            tr = tr .. round(Siz.Space.Left + ChartWidth * 0.5, 1) .. "\""
        end
        tr = tr .. " y=\""
        if Parms["TitleY"] ~= nil then
            tr = tr .. round(Siz.Space.Top + ChartHeight - (Parms["TitleY"] * Mult.Y), 1) .. "\">"
        else
            tr = tr .. round(Pos.Title, 1) .. "\">"
        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 or Parms["FootnoteY"] ~= nil, "left", "end") .. "\""
            .. " x=\""
        if Parms["FootnoteX"] ~= nil then
            tr = tr .. round(Siz.Space.Left + (Parms["FootnoteX"] * Mult.X), 1) .. "\""
        else
            tr = tr .. round(ImageWidth - Siz.ImagePadding.Right, 1) .. "\""
        end
        tr = tr .. " y=\""
        if Parms["FootnoteY"] ~= nil then
            tr = tr .. round(Siz.Space.Top + ChartHeight - (Parms["FootnoteY"] * Mult.Y), 1) .. "\">"
        else
            tr = tr .. round(Pos.Footnote, 1) .. "\">"
        end
        table.insert(r, tr .. Parms["Footnote"] .. "</text>")
        table.insert(r, " ")
    end

    -- chart texts

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

        table.insert(r, " <g id=\"charttexts\" class=\"charttext\" transform=\"translate(" .. Siz.Space.Left .. ", " .. Siz.Space.Top + ChartHeight .. ")\">")
        for i = 1, #ChartText do
            if ChartText[i] ~= nil then
                table.insert(r, "   <text x=\"" .. round((Parms["ChartText" .. i .. "X"] - iif(#GroupText == 0, Parms["XMin"], 0)) * Mult.X, 1) .. "\""
                    .. " y=\"" .. -round((Parms["ChartText" .. i .. "Y"] - Parms["YMin"]) * Mult.Y, 1) .. "\">"
                    .. ChartText[i] .. "</text>")
            end
        end
        table.insert(r, " </g>")
        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, 1)

        l = "   <polyline id=\"legend-line" .. SeriesNumber .. "\" class=\"series-lines-general series" .. SeriesNumber .. "\""
            .. " points=\""
            .. round(PosX, 1) .. "," .. lineY .. " "
            .. round(PosX + (TextSize * 1), 1) .. "," .. lineY .. " "
            .. round(PosX + (TextSize * 2), 1) .. "," .. 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, 1) .. "\""
            .. " y=\"" .. round(PosY + TextSize, 1) .. "\">"
            .. Text .. "</text>")
    else
        table.insert(r, "   <rect id=\"legend-area" .. SeriesNumber .. "\" class=\"series-areas-general series" .. SeriesNumber .. "\""
            .. " x=\"" .. round(PosX, 1) .. "\""
            .. " y=\"" .. round(PosY, 1) .. "\""
            .. " width=\"" .. (TextSize * 2) .. "\""
            .. " height=\"" .. TextSize .. "\"/>")
        table.insert(r, "   <text id=\"legend-text" .. SeriesNumber .. "\" class=\"legendtext\""
            .. " x=\"" .. round(PosX + (TextSize * 2) + Padding, 1) .. "\""
            .. " y=\"" .. round(PosY + TextSize, 1) .. "\">"
            .. Text .. "</text>")
    end
end

function outputDebugInfo()

    if Parms["Debug"] ~= nil and string.find(Parms["Debug"], KeyWords["parms"]) ~= nil then
        -- list all arguments
        table.insert(r, " All Arguments :")
        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")
        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

-- 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 round(x, p)
    -- round number x to precision p

    -- p > 0 rounds to p decimal places, eg: round(4.57, 1) = 4.5
    -- p = 0 (or not given) rounds to nearest integer
    -- 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,
}