Module:Chart data
Helps create the json to generate charts using Chart.js through MediaWiki:Gadget-Charts-core.js.
This module is a helper module to be used by other modules; it may not designed to be invoked directly. See RuneScape:Lua/Helper modules for a full list and more information. For a full list of modules using this helper click here
Function | Type | Use |
---|---|---|
_main( args ) | table/chart | Turns a table/chart object into a json string. |
convertToXYFormat( ys, [xs|{}] ) | table, table | Converts the ys array into an array of {x = n, y = y[n]} tables. If xs is already partially filled it will use {x = x[n], y = y[n]} until all values in xs are used, then it will use {x = n, y = y[n]} again for the remaining values in ys . |
generateXYFromFunc( func, start_x, end_x, [step|1] ) | function, number, number, number | Returns an array of {x = n, y = fun(n)} tables where n ranges from start_x to end_x in step increments. Be careful when using decimal step values as floating point error can cause the generator to stop one element too soon. |
jagexInterpolation( low_chance, high_chance, start_level, end_level ) | number, number, number, number | |
newChart( [options] ) | table | Returns a new chart object. options is a table with options in the Chart.js format. Most options will be set automatically or will be set later with other functions if not already defined. Usually all you need to define here is the chart type e.g. newChart{ type = 'scatter' } . Check the modules documentation for more info. |
chart:addDataSets( ... ) | table/dataSet object | Appends all given data sets to the chart.data.datasets table. |
chart:addDataLabels( labels ) | table | Appends all items in labels to the chart.data.labels table. |
chart:setDimensions( width, [height|width], [minWidth|400], [minHeight|400], [resizable|false] ) | number/string, number/string, number/string, number/string, boolean | Sets the dimensions of the chart. If a number is given to width , height , minWidth and minHeight it will be assumed you meant pixels. You can also use strings like 100% to fill the available space, 40vw /40vh to dynamically scale the simensions to the viewport size (i.e. 40vw = 40% of browser's window width). If resizable is true, the chart can be dragged by the lower right corner to change its size. |
chart:setTitle( [text|nil], [position|'top'] ) | string, string | Sets the label title of the chart. A value of nil will remove the current title. |
chart:setXLabel( [label|nil] ) | string | Sets the label for the x axis. Only works on chart types 'line', 'bar', 'horizontalBar', 'bubble' and 'scatter'. If used without arguments it will remove the current label. |
chart:setYLabel( [label|nil] ) | string | Sets the label for the y axis. Only works on chart types 'line', 'bar', 'horizontalBar', 'bubble' and 'scatter'. If used without arguments it will remove the current label. |
chart:setXLimits( [min|nil], [max|nil], [step|nil] ) | number, number, number | Sets the start, stop and step size of the x axis. Any argument with a value of nil will remove that setting. Only works on chart types 'horizontalBar', 'bubble' and 'scatter'. |
chart:setYLimits( [min|nil], [max|nil], [step|nil] ) | number, number, number | Sets the start, stop and step size of the y axis. Any argument with a value of nil will remove that setting. Only works on chart types 'line', 'bar', 'bubble' and 'scatter'. |
chart:setRadialLimits( [min|nil], [max|nil], [step|nil] ) | number, number, number | Sets the start, stop and step size of the r axis on polar plots. Any argument with a value of nil will remove that setting. Only works on chart types 'radar' and 'polarArea'. |
chart:setXAxisType( [type|nil] ) | string | Sets the axis type. Supported values are 'linear', 'logarithmic', 'category' and 'time'. If called without arguments it will reset back to the default value 'linear'. Only works on chart types 'bubble', 'scatter' and 'horizontalBar'. |
chart:setYAxisType( [type|nil] ) | string | Same as chart:setXAxisType() but for the y axis. Only works on chart types 'line', 'bubble', 'scatter' and 'bar'. |
chart:setOptions( options ) | table | Sets options using Chart.js format but makes sure you only change the given settings and not accidentally delete already existing settings. i.e. using chart:setOptions{ options = {scales = {ticks = {max = 100}}} } while {options = {scales = {ticks = {min = 0}}}} already exists will result in {options = {scales = {ticks = {min = 0, max = 100}}}} . |
chart:makeMwLoadDataCompatible() | N/A | Strips metatables so that it can be loaded by mw.loadData() making it possible to display the table using {{Chart data|<module name>}} . This should only be used at the very end when you are done creating your chart. |
chart:newDataSet( [options] ) | table | Returns a new dataSet object which is also automatically added to the chart datasets table. |
dataSet:addData( data ) | table | Appends the values of data to the already existing data stored in the dataSet.data array. Using dataset.data = data will overwrite any stored data. |
dataSet:addDataPoint( data ) | number/table | Append a single value to the dataSet.data array. Same as table.insert( dataSet.data, data ) . |
dataSet:setOptions( options ) | table | Sets options using Chart.js format. |
local p = {}
local chart = require( 'Module:Chart data' )
-- This chart can then be added to a page using {{Chart data|<module name>}}
function p.pie()
local plot = chart.newChart{ type = 'pie' }
plot:setDimensions( '40vw', nil, 400, nil, true ) -- Pie chart is always square
plot.colorPallet = chart.colorPallets.green
local labels = {}
local set = plot:newDataSet()
for i = 1, 6 do
set:addDataPoint( math.floor( math.sqrt( i ) * 10 + 0.5 ) / 10 )
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
return p.pie()
-------------------------------------------------------------
local p = {}
local chart = require( 'Module:Chart data' )
-- This chart cound be used by another module or drawn using {{#Invoke:<module name>|bar}}
function p.bar()
local plot = chart.newChart{ type = 'bar' }
plot:setDimensions( '40%', 600, 400, 400, true )
plot:setXLabel( 'x axis label' )
plot:setYLabel( 'y axis label' )
for i = 1, 2 do
local set = plot:newDataSet()
for j = 1, 6 do
set:addDataPoint( math.sqrt( i*j ) )
end
set.label = 'Set ' .. i
if i == 1 then
set.color = chart.colorPallets.green[3]
else
set.color = chart.colorPallets.orange[3]
end
end
local labels = {}
for i = 1, 6 do
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot
end
return p
Usage
To create a chart we need to create JSON in a format described by chart.js.
To make this a bit easier we can write a lua table instead which is then converted to a JSON using chart._main()
.
An example of this is Module:Chart data/xp chart, which can then be displayed on a page using {{Chart data|Chart data/xp chart|height=40vh|width=40vw}}
resulting in:
{"type":"scatter","data":{"datasets":[{"data":[{"y":0,"x":1},{"y":83,"x":2},{"y":174,"x":3},{"y":276,"x":4},{"y":388,"x":5},{"y":512,"x":6},{"y":650,"x":7},{"y":801,"x":8},{"y":969,"x":9},{"y":1154,"x":10},{"y":1358,"x":11},{"y":1584,"x":12},{"y":1833,"x":13},{"y":2107,"x":14},{"y":2411,"x":15},{"y":2746,"x":16},{"y":3115,"x":17},{"y":3523,"x":18},{"y":3973,"x":19},{"y":4470,"x":20},{"y":5018,"x":21},{"y":5624,"x":22},{"y":6291,"x":23},{"y":7028,"x":24},{"y":7842,"x":25},{"y":8740,"x":26},{"y":9730,"x":27},{"y":10824,"x":28},{"y":12031,"x":29},{"y":13363,"x":30},{"y":14833,"x":31},{"y":16456,"x":32},{"y":18247,"x":33},{"y":20224,"x":34},{"y":22406,"x":35},{"y":24815,"x":36},{"y":27473,"x":37},{"y":30408,"x":38},{"y":33648,"x":39},{"y":37224,"x":40},{"y":41171,"x":41},{"y":45529,"x":42},{"y":50339,"x":43},{"y":55649,"x":44},{"y":61512,"x":45},{"y":67983,"x":46},{"y":75127,"x":47},{"y":83014,"x":48},{"y":91721,"x":49},{"y":101333,"x":50},{"y":111945,"x":51},{"y":123660,"x":52},{"y":136594,"x":53},{"y":150872,"x":54},{"y":166636,"x":55},{"y":184040,"x":56},{"y":203254,"x":57},{"y":224466,"x":58},{"y":247886,"x":59},{"y":273742,"x":60},{"y":302288,"x":61},{"y":333804,"x":62},{"y":368599,"x":63},{"y":407015,"x":64},{"y":449428,"x":65},{"y":496254,"x":66},{"y":547953,"x":67},{"y":605032,"x":68},{"y":668051,"x":69},{"y":737627,"x":70},{"y":814445,"x":71},{"y":899257,"x":72},{"y":992895,"x":73},{"y":1096278,"x":74},{"y":1210421,"x":75},{"y":1336443,"x":76},{"y":1475581,"x":77},{"y":1629200,"x":78},{"y":1798808,"x":79},{"y":1986068,"x":80},{"y":2192818,"x":81},{"y":2421087,"x":82},{"y":2673114,"x":83},{"y":2951373,"x":84},{"y":3258594,"x":85},{"y":3597792,"x":86},{"y":3972294,"x":87},{"y":4385776,"x":88},{"y":4842295,"x":89},{"y":5346332,"x":90},{"y":5902831,"x":91},{"y":6517253,"x":92},{"y":7195629,"x":93},{"y":7944614,"x":94},{"y":8771558,"x":95},{"y":9684577,"x":96},{"y":10692629,"x":97},{"y":11805606,"x":98},{"y":13034431,"x":99},{"y":14391160,"x":100},{"y":15889109,"x":101},{"y":17542976,"x":102},{"y":19368992,"x":103},{"y":21385073,"x":104},{"y":23611006,"x":105},{"y":26068632,"x":106},{"y":28782069,"x":107},{"y":31777943,"x":108},{"y":35085654,"x":109},{"y":38737661,"x":110},{"y":42769801,"x":111},{"y":47221641,"x":112},{"y":52136869,"x":113},{"y":57563718,"x":114},{"y":63555443,"x":115},{"y":70170840,"x":116},{"y":77474828,"x":117},{"y":85539082,"x":118},{"y":94442737,"x":119},{"y":104273167,"x":120},{"y":115126838,"x":121},{"y":127110260,"x":122},{"y":140341028,"x":123},{"y":154948977,"x":124},{"y":171077457,"x":125},{"y":188884740,"x":126}],"label":"Standard skill","borderColor":"rgba(166,206,227,1)","showLine":true,"backgroundColor":"rgba(166,206,227,0.2)","fill":false}]},"options":{"maintainAspectRatio":false,"scales":{"y":{"ticks":{"beginAtZero":true},"scaleLabel":{"display":true,"labelString":"Experience"}},"x":{"ticks":{"beginAtZero":true},"scaleLabel":{"display":true,"labelString":"Level"}}},"tooltips":{"mode":"x","intersect":false,"position":"nearest"}}}
To make the construction of this table a bit easier, the chart class is provided which internally has the extact same structure as if we manually created the table. The chart class just sets a bunch of default settings based on what chart type you are creating; it also makes it a lot easier to deal with the colors of your data and provides a bunch of functions to easily set axis labels, axis start and stop values, axis type, etc.
Color pallets
chart.colorPallets has a few pre-defined color pallets. These pallets are made up of Module:Rgba objects. They also have a metatable set which allows them to automatically use the next color pallet in case the current pallet has less colors than your number of datasets.
You can set your preferred pallet for your chart with myChart.colorPallet = chart.collorPallets.blue
, in case of chart type bar of horizontalbar your can also set it per data set myDataSet.colorPallet = chart.collorPallets.orange
or if you want all bars of a given set to be the same color myDataSet.color = chart.collorpallets.orange[3]
.
It is possible to define your own color pallet as an array of Module:Rgba objects.
The chart class has the following color opions:
backgroundAlpha
- sets thergba:fade()
value for background colors.hoverLightenValue
- sets thergba:lighten()
value for when you hover over a data point.hoverAlpha
- sets thergba:fade()
value for when you hover over a data point.hoverSaturateValue
- sets thergba:saturate()
value for when you hover over a data point.
Their default value depend on the cart type but you can manually set them using:
chart:setOptions{ backgroundAlpha = <number>, hoverLightenValue = <number>, hoverAlpha = <number>, hoverSaturateValue = <number> }
Code |
function p.colorQualitative()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Qualitative' )
plot.fill = false
plot.colorPallet = chart.colorPallets.qualitative
for i = 1, #plot.colorPallet do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return i*x^2 end, 0, 50 )
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.colorBlue()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Blue' )
plot.fill = false
plot.colorPallet = chart.colorPallets.blue
for i = 1, #plot.colorPallet do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return i*x^2 end, 0, 50 )
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.colorGreen()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Green' )
plot.fill = false
plot.colorPallet = chart.colorPallets.green
for i = 1, #plot.colorPallet do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return i*x^2 end, 0, 50 )
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.colorBlueGreen()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'BlueGreen' )
plot.fill = false
plot.colorPallet = chart.colorPallets.blueGreen
for i = 1, #plot.colorPallet do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return i*x^2 end, 0, 50 )
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.colorOrange()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Orange' )
plot.fill = false
plot.colorPallet = chart.colorPallets.orange
for i = 1, #plot.colorPallet do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return i*x^2 end, 0, 50 )
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.colorOverflow()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Color overflow' )
plot:setOptions{
options = {
legend = {
display = false
}
}
}
plot.fill = false
plot.colorPallet = chart.colorPallets.orange
for i = 1, 25 do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return i*x^2 end, 0, 50 )
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.customColorPallet()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Custom color pallet' )
plot.fill = false
plot.colorPallet = {
chart.rgba.new(0,0,0),
chart.rgba.new(126,0,0),
chart.rgba.new(0,126,0),
chart.rgba.new(0,0,126),
chart.rgba.new(255,0,0),
chart.rgba.new(0,255,0),
chart.rgba.new(0,0,255),
}
for i = 1, #plot.colorPallet do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return i*x^2 end, 0, 50 )
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.colorOptions()
local plot = chart.newChart{ type = 'bar' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Color options' )
plot:setOptions{
backgroundAlpha = 0.8,
hoverLightenValue = 0.5,
hoverAlpha = 0.4,
hoverSaturateValue = 1.5,
}
for i = 1, 2 do
local set = plot:newDataSet()
for j = 1, 6 do
set:addDataPoint( math.sqrt( i*j ) )
end
set.label = 'Set ' .. i
if i == 1 then
set.color = chart.colorPallets.green[3]
else
set.color = chart.colorPallets.orange[3]
end
end
local labels = {}
for i = 1, 6 do
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
|
Examples
Code |
function p.line()
local plot = chart.newChart{ type = 'line' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Line chart' )
plot:setXLabel( 'x axis label' )
plot:setYLabel( 'y axis label' )
for i = 1, 2 do
local set = plot:newDataSet()
set.data = { i^2, (i+1)^2, (i+2)^2, (i+3^2), (i+4)^2 }
set.label = 'Set ' .. i
set.borderDash = {5, 5}
end
local labels = {}
for i = 1, 5 do
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return 'Prefix text message' .. plot .. 'Affix text message'
end
|
Code |
function p.bar()
local plot = chart.newChart{ type = 'bar' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Bar chart' )
plot:setXLabel( 'x axis label' )
plot:setYLabel( 'y axis label' )
for i = 1, 2 do
local set = plot:newDataSet()
for j = 1, 6 do
set:addDataPoint( math.sqrt( i*j ) )
end
set.label = 'Set ' .. i
if i == 1 then
set.color = chart.colorPallets.green[3]
else
set.color = chart.colorPallets.orange[3]
end
end
local labels = {}
for i = 1, 6 do
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.horizontalBar()
local plot = chart.newChart{ type = 'horizontalBar' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'HorizontalBar chart' )
plot:setXLabel( 'x axis label' )
plot:setYLabel( 'y axis label' )
for i = 1, 2 do
local set = plot:newDataSet()
for j = 1, 6 do
set:addDataPoint( math.sqrt( i*j ) )
end
set.label = 'Set ' .. i
if i == 1 then
set.color = chart.colorPallets.green[3]
else
set.color = chart.colorPallets.orange[3]
end
end
local labels = {}
for i = 1, 6 do
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.radar()
local plot = chart.newChart{ type = 'radar' }
plot:setDimensions( '10vw', nil, 300, nil, true ) -- Radar chart is always square. height = width
plot:setTitle( 'Radar chart' )
plot:setRadialLimits( 0, 5 )
plot.fill = false
for i = 1, 5 do
local set = plot:newDataSet()
for j = 1, 6 do
set:addDataPoint( math.sqrt( i*j*(math.random()+1) ) )
end
set.label = 'Set ' .. i
if i == 3 then
set.fill = true
end
end
local labels = {}
for i = 1, 6 do
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.pie()
local plot = chart.newChart{ type = 'pie' }
plot:setDimensions( '10vw', nil, 300, nil, true ) -- Pie chart is always square. height = width
plot:setTitle( 'Pie chart' )
local labels = {}
local set = plot:newDataSet()
for i = 1, 6 do
set:addDataPoint( math.floor( math.sqrt( i ) * 10 + 0.5 ) / 10 )
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.doughnut()
local plot = chart.newChart{ type = 'doughnut' }
plot:setDimensions( '10vw', nil, 300, nil, true ) -- doughnut chart is always square. height = width
plot:setTitle( 'Doughnut chart' )
local labels = {}
local set = plot:newDataSet()
for i = 1, 6 do
set:addDataPoint( math.floor( math.sqrt( i ) * 10 + 0.5 ) / 10 )
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.polarArea()
local plot = chart.newChart{ type = 'polarArea' }
plot:setDimensions( '10vw', nil, 300, nil, true ) -- polarArea chart is always square. height = width
plot:setTitle( 'PolarArea chart' )
local labels = {}
local set = plot:newDataSet()
for i = 1, 6 do
set:addDataPoint( math.floor( math.sqrt( i ) * 10 + 0.5 ) / 10 )
table.insert( labels, 'Value ' .. i )
end
plot:addDataLabels( labels )
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.bubble()
local plot = chart.newChart{ type = 'bubble' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Bubble chart' )
plot:setXLabel( 'x axis label' )
plot:setYLabel( 'y axis label' )
for i = 1, 5 do
local set = plot:newDataSet()
for j = 1, 5 do
set:addDataPoint{ x = math.random() * 10, y = math.random() * 20, r = math.random() * 15 + 5 }
end
set.label = 'Set ' .. i
end
return plot:makeMwLoadDataCompatible()
end
|
Code |
function p.scatter()
local plot = chart.newChart{ type = 'scatter' }
plot:setDimensions( '10vw', '10vh', 300, 300, true )
plot:setTitle( 'Scatter chart' )
plot:setXLabel( 'x axis label' )
plot:setYLabel( 'y axis label' )
plot:setYAxisType( 'logarithmic' )
plot.fill = false
for i = 1, 5 do
local set = plot:newDataSet()
set.data = chart.generateXYFromFunc( function(x) return (math.sin( x/5 ) * 5 + x)^i end, 0, 100 )
set.label = 'Set ' .. i
if i == 2 then
set.fill = '+2'
end
end
return plot:makeMwLoadDataCompatible()
end
|
-- <nowiki>
---@class chart
local chart = {}
---@class dataSet
local dataSet = {}
local libraryUtil = require( 'Module:libraryUtil' )
local rgba = require( 'Module:Rgba' )
local isEmpty = require( 'Module:Paramtest' ).is_empty
local arr = require( 'Module:Array' )
---@alias xyData table<string, number>
---@alias xyDataSet xyData[]
---@alias dataArr number[]
---@alias options_t table<string, string|number|options_t>
---@param val any
---@param default any
---@return any
local function default( val, default )
if val == nil or (type( val ) == 'string' and isEmpty( val )) then
return default
else
return val
end
end
---@param obj chart | dataSet
---@param options options_t
local function defaultOptions( obj, options )
local function _setOptions( tbl, options )
for k, v in pairs( options ) do
if type( v ) == 'table' and type( tbl[k] ) == 'table' then
_setOptions( tbl[k], v )
else
tbl[k] = default( tbl[k], v )
end
end
end
_setOptions( obj, options )
end
local function stripNonChartJsVariables( tbl )
local stripVarNames = {
colorPallet = true,
backgroundAlpha = true,
hoverLightenValue = true,
hoverSaturateValue = true,
hoverAlpha = true,
widthVal = true,
heightVal = true,
flippedXY = true
}
local function _strip( tbl )
for k, v in pairs( tbl ) do
if stripVarNames[k] then
tbl[k] = nil
elseif type( v ) == 'table' then
_strip( v )
end
end
end
_strip( tbl )
end
---@param tbl table
---@param seen table | nil @Only for internal use
local function stripInvalidValues( tbl, seen )
seen = seen or {}
seen[tbl] = true
for k, v in pairs( tbl ) do
if type( k ) == 'table' or type( k ) == 'function' then
tbl[k] = nil
elseif type( v ) == 'function' then
tbl[k] = nil
elseif seen[v] then
tbl[k] = nil
elseif type( v ) == 'table' then
local mt = getmetatable( v )
if mt and mt.__tostring then
tbl[k] = tostring( v )
else
stripInvalidValues( v, seen )
end
end
end
setmetatable( tbl, nil )
stripNonChartJsVariables( tbl )
end
-- =====================
-- Chart class
-- =====================
local checkChartClass = libraryUtil.makeCheckClassFunction( 'Module:Chart data' , 'chart', chart, 'chart object' )
chart.rgba = rgba
chart.colorPallets = {
qualitative = {
name = 'qualitative',
rgba.new( 166,206,227 ),
rgba.new( 31,120,180 ),
rgba.new( 178,223,138 ),
rgba.new( 51,160,44 ),
rgba.new( 251,154,153 ),
rgba.new( 227,26,28 ),
rgba.new( 253,191,111 ),
rgba.new( 255,127,0 ),
rgba.new( 202,178,214 ),
rgba.new( 106,61,154 )
},
blue = {
name = 'blue',
rgba.new( 208,209,230 ),
rgba.new( 166,189,219 ),
rgba.new( 103,169,207 ),
rgba.new( 54,144,192 ),
rgba.new( 2,129,138 ),
rgba.new( 1,108,89 ),
rgba.new( 1,70,54 )
},
green = {
name = 'green',
rgba.new( 217,240,163 ),
rgba.new( 173,221,142 ),
rgba.new( 120,198,121 ),
rgba.new( 65,171,93 ),
rgba.new( 35,132,67 ),
rgba.new( 0,104,55 ),
rgba.new( 0,69,41 )
},
blueGreen = {
name = 'blueGreen',
rgba.new( 204,236,230 ),
rgba.new( 153,216,201 ),
rgba.new( 102,194,164 ),
rgba.new( 65,174,118 ),
rgba.new( 35,139,69 ),
rgba.new( 0,109,44 ),
rgba.new( 0,68,27 )
},
orange = {
name = 'orange',
rgba.new( 254,227,145 ),
rgba.new( 254,196,79 ),
rgba.new( 254,153,41 ),
rgba.new( 236,112,20 ),
rgba.new( 204,76,2 ),
rgba.new( 153,52,4 ),
rgba.new( 102,37,6 )
}
}
local colorPalletMt = {
__index = function ( self, i ) -- Use the next color pallet if one pallet is too small for all the lines
local pallet = next( chart.colorPallets, self.name ) or next( chart.colorPallets )
pallet = chart.colorPallets[ pallet ]
return pallet[ i - #self ]
end,
__tostring = function ( t ) return t.name end
}
for _, set in pairs( chart.colorPallets ) do
if type( set ) == 'table' then
setmetatable( set, colorPalletMt )
end
end
function chart.main( frame )
return chart._main( (frame:getParent() or frame).args )
end
---@param args options_t
---@return string
function chart._main( args )
libraryUtil.checkType( 'Module:Chart_data._main', 1, args, 'table' )
local data = args
if not data.isChartObj then
local moduleName = args.dataModule or args[1]
local funcName = args['function'] or args[2]
if isEmpty( moduleName ) then
error( 'Data module was not provided or is empty' )
end
if moduleName:find( '[Mm]odule:' ) == nil then
moduleName = 'Module:' .. moduleName
end
if funcName then
data = require( moduleName )[ funcName ]( args )
assert( type( data )=='table', string.format(
"Error: function '%s' from module '%s' returned an invalid type (table expected, got %s)",
funcName, moduleName, type( data ) ) )
if data.isChartObj and not data.isFinished then
data:finish()
end
else
data = mw.loadData( moduleName )
end
end
local width = args.width or data.width or '60vw'
local height = args.height or data.height or '60vh'
local minWidth = args.minWidth or args.min_width or data.minWidth or '400px'
local minHeight = args.minHeight or args.min_height or data.minHeight or '400px'
return string.format(
'<div class="rsw-chartjs rsw-chartjs-config" style="position:relative; margin:1em 0; width:%s; height:%s; min-width:%s; min-height:%s;%s"><pre>%s</pre></div>',
width,
height,
minWidth,
minHeight,
data.resizable and ' resize:both; overflow:hidden; padding:0 5px 5px 0;' or '',
mw.text.jsonEncode( data )
)
end
---@param ys number[]
---@param xs number[] | nil
---@return xyDataSet
function chart.convertToXYFormat( ys, xs )
libraryUtil.checkType( 'Module:Chart_data.convertToXYFormat', 1, ys, 'table' )
libraryUtil.checkType( 'Module:Chart_data.convertToXYFormat', 2, xs, 'table', true )
xs = xs or {}
local ret = {}
for i,v in ipairs( ys ) do
table.insert( ret, { x=(xs[i] or i), y=v } )
end
return ret
end
---@param func function
---@param start_x number
---@param end_x number
---@param step number
---@return xyDataSet
function chart.generateXYFromFunc( func, start_x, end_x, step )
libraryUtil.checkType( 'Module:Chart_data.generateXYFromFunc', 1, func, 'function' )
libraryUtil.checkType( 'Module:Chart_data.generateXYFromFunc', 2, start_x, 'number' )
libraryUtil.checkType( 'Module:Chart_data.generateXYFromFunc', 3, end_x, 'number' )
libraryUtil.checkType( 'Module:Chart_data.generateXYFromFunc', 4, step, 'number', true )
step = step or 1
local ret = {}
for i = start_x, end_x, step do
table.insert( ret, { x=i, y=func( i ) } )
end
return ret
end
---@param low_chance number
---@param high_chance number
---@param start_level number
---@param end_level number
---@return xyDataSet
function chart.jagexInterpolation( low_chance, high_chance, start_level, end_level )
libraryUtil.checkType( 'Module:Chart_data.jagexInterpolation', 1, low_chance, 'number' )
libraryUtil.checkType( 'Module:Chart_data.jagexInterpolation', 2, high_chance, 'number' )
libraryUtil.checkType( 'Module:Chart_data.jagexInterpolation', 3, start_level, 'number' )
libraryUtil.checkType( 'Module:Chart_data.jagexInterpolation', 4, end_level, 'number' )
local function interpolate( x )
return math.floor( ( (99-x)*low_chance + (x-1)*high_chance ) / 98 )
end
return chart.generateXYFromFunc( interpolate, start_level, end_level )
end
---@param setup options_t
---@return chart
function chart.newChart( setup )
libraryUtil.checkType( 'Module:Chart_data.newChart', 1, setup, 'table', true )
local obj = setup or {}
if obj.type == 'bar' then
defaultOptions( obj, {
options = {
fill = true,
backgroundAlpha = 0.4,
hoverLightenValue = 0.95,
hoverAlpha = 0.5,
hoverSaturateValue = 1,
scales = {
x = { -- This is used for horizontal bar graphs
ticks = {
beginAtZero = true
}
},
y = {
ticks = {
beginAtZero = true
}
}
}
}
} )
elseif obj.type == 'bubble' then
defaultOptions( obj, {
options = {
fill = true,
backgroundAlpha = 0.4,
hoverLightenValue = 0.95,
hoverAlpha = 0.5,
hoverSaturateValue = 1
}
} )
elseif obj.type == 'radar' then
defaultOptions( obj, {
options = {
scale = {
ticks = {
showLabelBackdrop = false,
z = 1
}
}
}
} )
elseif obj.type == 'pie' or obj.type == 'doughnut' then
defaultOptions( obj, {
options = {
fill = true,
backgroundAlpha = 0.8,
hoverLightenValue = 0.95,
hoverAlpha = 0.9,
hoverSaturateValue = 1
}
} )
elseif obj.type == 'polarArea' then
defaultOptions( obj, {
options = {
fill = true,
backgroundAlpha = 0.5,
hoverLightenValue = 0.95,
hoverAlpha = 0.6,
hoverSaturateValue = 1,
scale = {
ticks = {
showLabelBackdrop = false,
z = 1
}
}
}
} )
end
defaultOptions( obj, {
isChartObj = true,
type = 'scatter',
widthVal = '60vw',
heightVal = '60vh',
minWidth = 400,
resizable = true,
data = {
datasets = {}
},
options = {
fill = false,
colorPallet = chart.colorPallets.qualitative,
backgroundAlpha = 0.3,
hoverLightenValue = 0.95,
hoverAlpha = 0.5,
hoverSaturateValue = 1,
maintainAspectRatio = false,
title = {
font = {
size = 18
}
},
tooltips = {
intersect = false
}
}
} )
setmetatable( obj, chart )
obj:setDimensions( obj.widthVal, obj.heightVal, obj.minWidth, obj.minHeight, obj.resizable )
return obj
end
---@vararg xyDataSet | dataArr
---@return chart
function chart:addDataSets( ... )
checkChartClass( self, 'addDataSets' )
local sets = { ... }
for i, set in ipairs( sets ) do
libraryUtil.checkType( 'chart.addDataSets', i, set, 'table' )
if not arr.contains( self.data.datasets, set, false ) then
table.insert( self.data.datasets, set )
end
end
return self
end
---@param labels string
---@return chart
function chart:addDataLabels( labels )
checkChartClass( self, 'addDataLabels' )
libraryUtil.checkType( 'Module:Chart_data.addDataLabels', 1, labels, 'table' )
self.data.labels = default( self.data.labels, {} )
for _, label in ipairs( labels ) do
table.insert( self.data.labels, label )
end
return self
end
---@param width number | string
---@param height number | string | nil
---@param minWidth number | string | nil
---@param minHeight number | string | nil
---@param resizable boolean | nil
---@return chart
function chart:setDimensions( width, height, minWidth, minHeight, resizable )
checkChartClass( self, 'setDimensions' )
libraryUtil.checkTypeMulti( 'chart.setDimensions', 1, width, {'number', 'string'} )
libraryUtil.checkTypeMulti( 'chart.setDimensions', 2, height, {'number', 'string', 'nil'} )
libraryUtil.checkTypeMulti( 'chart.setDimensions', 3, minWidth, {'number', 'string', 'nil'} )
libraryUtil.checkTypeMulti( 'chart.setDimensions', 4, minHeight, {'number', 'string', 'nil'} )
libraryUtil.checkType( 'chart.setDimensions', 5, resizable, 'boolean', true )
if type( width ) == 'number' then
self.widthVal = width
self.width = width .. 'px'
elseif type( width ) == 'string' then
self.width = width
self.widthVal = tonumber( width:match( '%d+' ) )
if type( self.widthVal ) ~= 'number' then
error( "bad argument #1 to 'chart.setDimensions' (input doesn't contain a number)", 2 )
end
else
self.widthVal = nil
self.width = ''
end
if type( height ) == 'number' then
self.heightVal = height
self.height = height .. 'px'
elseif type( height ) == 'string' then
self.height = height
self.heightVal = tonumber( height:match( '%d+' ) )
if type( self.heightVal ) ~= 'number' then
error( "bad argument #2 to 'chart.setDimensions' (input doesn't contain a number)", 2 )
end
else
self.heightVal = nil
self.height = ''
end
if type( minWidth ) == 'number' then
self.minWidth = minWidth .. 'px'
elseif type( minWidth ) == 'string' then
self.minWidth = minWidth
else
self.minWidth = nil
end
if type( minHeight ) == 'number' then
self.minHeight = minHeight .. 'px'
elseif type( minHeight ) == 'string' then
self.minHeight = minHeight
else
self.minHeight = nil
end
self.resizable = default( resizable, self.resizable )
if arr.contains( {'pie', 'doughnut', 'radar', 'polarArea'}, self.type ) then
self.height = self.width
self.minHeight = self.minWidth
self.options.aspectRatio = 1
else
if self.widthVal and self.heightVal then
self.options.aspectRatio = self.widthVal / self.heightVal
elseif self.widthVal then
self.height = self.width
self.options.aspectRatio = 1
else
self.options.aspectRatio = 1
end
end
return self
end
---@param text string | table | nil
---@param position string | nil
---@return chart
function chart:setTitle( text, position )
checkChartClass( self, 'setTitle' )
libraryUtil.checkTypeMulti( 'chart.setTitle', 1, text, {'string', 'table', 'nil'} )
libraryUtil.checkType( 'chart.setTitle', 1, position, 'string', true )
self:setOptions{
title = {
display = text and true or false,
text = text,
position = position or 'top'
}
}
return self
end
---@param label string | nil
---@return chart
function chart:setXLabel( label )
checkChartClass( self, 'setXLabel' )
libraryUtil.checkType( 'chart.setXLabel', 1, label, 'string', true )
if arr.contains( {'line', 'bar', 'bubble', 'scatter'}, self.type ) then
self:setOptions{
scales = {
x = {
scaleLabel = {
display = label and true or false,
labelString = label
}
}
}
}
end
return self
end
---@param label string | nil
---@return chart
function chart:setYLabel( label )
checkChartClass( self, 'setYLabel' )
libraryUtil.checkType( 'chart.setYLabel', 1, label, 'string', true )
if arr.contains( {'line', 'bar', 'bubble', 'scatter'}, self.type ) then
self:setOptions{
scales = {
y = {
scaleLabel = {
display = label and true or false,
labelString = label
}
}
}
}
end
return self
end
---@param min number | nil
---@param max number | nil
---@param step number | nil
---@return chart
function chart:setXLimits( min, max, step )
checkChartClass( self, 'setXLimits' )
libraryUtil.checkType( 'chart.setXLimits', 1, min, 'number', true )
libraryUtil.checkType( 'chart.setXLimits', 2, max, 'number', true )
libraryUtil.checkType( 'chart.setXLimits', 3, step, 'number', true )
if arr.contains( {'bubble', 'scatter'}, self.type ) then
self:setOptions{
scales = {
x = {
min = min,
max = max,
ticks = {
stepSize = step
}
}
}
}
end
return self
end
---@param min number | nil
---@param max number | nil
---@param step number | nil
---@return chart
function chart:setYLimits( min, max, step )
checkChartClass( self, 'setYLimits' )
libraryUtil.checkType( 'chart.setYLimits', 1, min, 'number', true )
libraryUtil.checkType( 'chart.setYLimits', 2, max, 'number', true )
libraryUtil.checkType( 'chart.setYLimits', 3, step, 'number', true )
if arr.contains( {'line', 'bar', 'bubble', 'scatter'}, self.type ) then
self:setOptions{
scales = {
y = {
min = min,
max = max,
ticks = {
stepSize = step
}
}
}
}
end
return self
end
---@param format string
---@return chart
function chart:setYFormat( format )
checkChartClass( self, 'setYFormat' )
libraryUtil.checkType( 'chart.setYFormat', 1, format, 'string', true )
self:setOptions{
scales = {
y = {
ticks = {
format = format
}
}
}
}
return self
end
---@param min number | nil
---@param max number | nil
---@param step number | nil
---@return chart
function chart:setRadialLimits( min, max, step )
checkChartClass( self, 'setRadialLimits' )
libraryUtil.checkType( 'chart.setRadialLimits', 1, min, 'number', true )
libraryUtil.checkType( 'chart.setRadialLimits', 2, max, 'number', true )
libraryUtil.checkType( 'chart.setRadialLimits', 3, step, 'number', true )
if arr.contains( {'radar', 'polarArea'}, self.type ) then
self:setOptions{
scale = {
min = min,
max = max,
ticks = {
stepSize = step
}
}
}
end
return self
end
---@param type string | nil
---@return chart
function chart:setXAxisType( type )
checkChartClass( self, 'setXAxisType' )
libraryUtil.checkType( 'chart.setXAxisType', 1, type, 'string', true )
if arr.contains( {'bar', 'bubble', 'line', 'scatter'}, self.type ) then
self:setOptions{
scales = {
x = {
type = type
}
}
}
end
return self
end
---@param type string | nil
---@return chart
function chart:setYAxisType( type )
checkChartClass( self, 'setYAxisType' )
libraryUtil.checkType( 'chart.setYAxisType', 1, type, 'string', true )
if arr.contains( {'bar', 'bubble', 'line', 'scatter'}, self.type ) then
self:setOptions{
scales = {
y = {
type = type
}
}
}
end
return self
end
---@param val boolean
---@return chart
function chart:showLegend( val )
checkChartClass( self, 'showLegend' )
libraryUtil.checkType( 'chart.showLegend', 1, val, 'boolean' )
self:setOptions{
legend = {
display = val
}
}
return self
end
---@param format string
---@return chart
function chart:setTooltipFormat( format )
checkChartClass( self, 'setTooltipFormat' )
libraryUtil.checkType( 'chart.setTooltipFormat', 1, format, 'string', true )
self:setOptions{
tooltips = {
format = format
}
}
return self
end
---@param count number
---@return chart
function chart:setDatasetsPerGroup( count )
checkChartClass( self, 'setDatasetsPerGroup' )
libraryUtil.checkType( 'chart.setDatasetsPerGroup', 1, label, 'number', true )
self:setOptions{
datasetsPerGroup = count
}
return self
end
---@return chart
function chart:flipXY()
checkChartClass( self, 'flipXY' )
if self.type == 'bar' then
self:setXAxisType( 'linear' )
:setYAxisType( 'category' )
:setOptions{
scales = {
x = {
offset = false,
gridLines = {
offsetGridLines = false
}
}
}
}
self.flippedXY = true
for _, v in ipairs( self.data.datasets ) do
v.indexAxis = 'y'
end
end
return self
end
---@param options options_t
---@return chart
function chart:setOptions( options )
checkChartClass( self, 'setOptions' )
libraryUtil.checkType( 'chart.setOptions', 1, options, 'table' )
local function _setOptions( tbl, options )
for k, v in pairs( options ) do
if type( v ) == 'table' and type( tbl[k] ) == 'table' then
_setOptions( tbl[k], v )
else
tbl[k] = v
end
end
end
_setOptions( self.options, options )
return self
end
---@return string | table
function chart:finish()
checkChartClass( self, 'finish' )
for i, set in ipairs( self.data.datasets ) do
if (self.options.colorPallet or set.colorPallet) and
self.options.backgroundAlpha and
self.options.hoverLightenValue and
self.options.hoverSaturateValue and
self.options.hoverAlpha
then
if arr.contains( {'bar', 'pie', 'doughnut', 'polarArea'}, self.type ) then
if set.color and not set.colorPallet then
set.borderColor = tostring( set.color )
set.hoverBorderColor = tostring( set.color:lighten( self.options.hoverLightenValue )
:saturate( self.options.hoverSaturateValue ) )
set.backgroundColor = tostring( set.color:fade( self.options.backgroundAlpha ) )
set.hoverBackgroundColor = tostring( set.color:lighten( self.options.hoverLightenValue )
:saturate( self.options.hoverSaturateValue )
:fade( self.options.hoverAlpha ) )
else
set.borderColor = {}
set.hoverBorderColor = {}
set.backgroundColor = {}
set.hoverBackgroundColor = {}
local pallet = default( set.colorPallet, self.options.colorPallet )
for j = 1, #set.data do
table.insert( set.borderColor, tostring( pallet[j] ) )
table.insert( set.hoverBorderColor,
tostring( pallet[j]:lighten( self.options.hoverLightenValue )
:saturate( self.options.hoverSaturateValue ) ) )
table.insert( set.backgroundColor, tostring( pallet[j]:fade( self.options.backgroundAlpha ) ) )
table.insert( set.hoverBackgroundColor,
tostring( pallet[j]:lighten( self.options.hoverLightenValue )
:saturate( self.options.hoverSaturateValue )
:fade( self.options.hoverAlpha ) ) )
end
end
else
local pallet = default( set.colorPallet, self.options.colorPallet )
set.borderColor = default( set.borderColor, pallet[i] )
if not set.hoverBorderColor then
set.hoverBorderColor = set.borderColor:lighten( self.options.hoverLightenValue )
:saturate( self.options.hoverSaturateValue )
end
set.backgroundColor =
default( set.backgroundColor, self.options.colorPallet[i]:fade( self.options.backgroundAlpha ) )
if not set.hoverBackgroundColor then
set.hoverBackgroundColor = set.backgroundColor:lighten( self.options.hoverLightenValue )
:saturate( self.options.hoverSaturateValue )
:fade( self.options.hoverAlpha )
end
set.fill = default( default( set.fill, self.options.fill ), false )
end
end
end
stripInvalidValues( self )
self.isFinished = true
return self
end
---@return string
function chart:tostring()
checkChartClass( self, 'tostring' )
self:finish()
return chart._main( self )
end
function chart:debug()
checkChartClass( self, 'debug' )
local temp = colorPalletMt.__index -- By design this makes the color pallet tables behave like they are infinitely long,
-- so we have to turn this off first
local temp2 = chart.__tostring -- Prevent chart from being turned into a json
colorPalletMt.__index = nil
chart.__tostring = nil
mw.logObject( self )
colorPalletMt.__index = temp
chart.__tostring = temp2
end
chart.__index = chart
chart.__tostring = chart.tostring
chart.__concat = function( x, y )
return tostring( x ) .. tostring( y )
end
-- =======================
-- DataSet class
-- =======================
local checkDataSetClass = libraryUtil.makeCheckClassFunction( 'Module:Chart data' , 'dataSet', dataSet, 'dataSet object' )
---@param setup options_t
---@return dataSet
function chart:newDataSet( setup )
checkChartClass( self, 'newDataSet' )
libraryUtil.checkType( 'chart.newDataSet', 1, setup, 'table', true )
local obj = setup or {}
if self.type == 'scatter' then
obj.showLine = default( obj.showLine, true )
end
if self.type == 'bar' then
obj.borderWidth = default( obj.borderWidth, 1 )
if self.flippedXY then
obj.indexAxis = 'y'
end
end
obj.data = default( obj.data, {} )
obj.clip = 5
obj.parent = self
self:addDataSets( obj )
return setmetatable( obj, dataSet )
end
---@param data xyDataSet | dataArr
---@return dataSet
function dataSet:addData( data )
checkDataSetClass( self, 'addData' )
libraryUtil.checkType( 'dataSet.addData', 1, data, 'table' )
for _, v in ipairs( data ) do
table.insert( self.data, v )
end
return self
end
---@param data xyData | number
---@return dataSet
function dataSet:addDataPoint( data )
checkDataSetClass( self, 'addDataPoint' )
libraryUtil.checkTypeMulti( 'dataSet.addDataPoint', 1, data, {'table', 'number'} )
table.insert( self.data, data )
return self
end
---@param options options_t
---@return dataSet
function dataSet:setOptions( options )
checkDataSetClass( self, 'setOptions' )
libraryUtil.checkType( 'dataSet.setOptions', 1, options, 'table' )
local function _setOptions( tbl, options )
for k, v in pairs( options ) do
if type( v ) == 'table' and type( tbl[k] ) == 'table' then
_setOptions( tbl[k], v )
else
tbl[k] = v
end
end
end
_setOptions( self, options )
return self
end
---@return chart
function dataSet:done()
checkDataSetClass( self, 'done' )
return self.parent
end
dataSet.__index = dataSet
return chart
-- </nowiki>