@isoftdata/utility-dashboard-backend 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # utility-dashboard-backend
2
+ A backend utility for formatting and loading dashboard chart and report data to be usable by the [frontend dashboard component](https://github.com/ISoft-Data-Systems/ractive-component-web-dashboard).
3
+
4
+ ## Prerequisites
5
+
6
+ * A MySQL database schema as outlined in [schema.md](schema.md)
7
+ * A backend that can fetches rows from the chart, report, and report chart tables from the database with their primary keys. Any json columns should be parsed as objects.
8
+ * A connection or pool of connections from the `mysql` NPM module for running the charts' queries
9
+ * The [dashboard Ractive component](https://github.com/ISoft-Data-Systems/ractive-component-web-dashboard) on the client
10
+
11
+ ## API
12
+
13
+ ### formatReportMetadata
14
+
15
+ This function returns data in the format expected for the `loadReportMetadata` function on the dashboard client component.
16
+
17
+ #### Function signature:
18
+
19
+ ```js
20
+ async formatReportMetadata(mysqlConnection, { report, chartsInReport })
21
+ ```
22
+
23
+ #### Example Usage (From Pro Web `server/chart-loader.js`):
24
+
25
+ ```js
26
+ async function loadReportMetadata({ dashboardReportId, chartId }) {
27
+ const report = await db.dashboardReport.loadById({ dashboardReportId }) // gets a report by its id
28
+ const chartsInReport = await db.dashboardChart.loadByReport({ dashboardReportId, chartId }) // gets all charts for a specified report, or if `chartId` is specified, only gets that chart for that report.
29
+ return await formatReportMetadata(db.pool, { report, chartsInReport })
30
+ }
31
+ ```
32
+
33
+ #### mysqlConnection
34
+ A connection or pool of connections from the `mysql` npm module.
35
+
36
+ #### report
37
+
38
+ A report `object`, a single row from the `dashboard_report`/`wa_dashboard_report` table/view. The `parameters` Array from the `json` column should also be parsed from JSON and appended to this object.
39
+
40
+ Example:
41
+
42
+ ```js
43
+ {
44
+ dashboardReportId: 1,
45
+ reportName: 'report_overview',
46
+ reportTitle: 'Overview',
47
+ shareType: 'everyone',
48
+ shareId: null,
49
+ ownerId: null,
50
+ parameters: [ // parameters deconstructed from `json` column after parsing
51
+ {
52
+ name: 'date_range',
53
+ type: 'dateRange',
54
+ title: 'Date Range',
55
+ default: 'This Year'
56
+ }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ #### chartsInReport
62
+
63
+ An `Array` of chart `object`s, one for each chart in the report, multiple rows from the `dashboard_chart`/`wa_dashboard_chart` table/view joined with `dashboard_report_chart`/`wa_dashboard_report_chart` to get the chart's rank.
64
+
65
+ Example:
66
+
67
+ ```js
68
+ [
69
+ {
70
+ chartId: 10,
71
+ name: 'line_sales_over_time_and_previous_period',
72
+ title: 'Sales over Time',
73
+ supertype: 'google',
74
+ formatting: { Sales: [Object] },
75
+ multiSeries: {
76
+ value: 'Sales',
77
+ series: 'Period',
78
+ tooltip: 'Tooltip',
79
+ groupXAxisBy: 'Date'
80
+ },
81
+ chartWrapper: { options: [Object], chartType: 'LineChart' },
82
+ rank: 1
83
+ },
84
+ {
85
+ chartId: 16,
86
+ name: 'map_sales_by_lat_long',
87
+ title: 'Sales Map',
88
+ supertype: 'google',
89
+ formatting: { Sales: [Object] },
90
+ chartWrapper: { options: [Object], chartType: 'GeoChart' },
91
+ rank: 2
92
+ }
93
+ ]
94
+ ```
95
+
96
+ ### formatChartDataForReport
97
+
98
+ This function returns data in the format expected for the `loadChartDataForReport` function on the dashboard client component, including applying any report-specific chart options overrides and running the chart's query.
99
+
100
+ #### Function signature
101
+
102
+ ```js
103
+ async formatChartDataForReport(mysqlConnection, { reportParameters, chart, reportChart, parameterSelectionList })
104
+ ```
105
+
106
+ #### Example usage: (from Pro Web `server/chart-loader.js`)
107
+
108
+ ```js
109
+ async function loadChartDataForReport ({ dashboardReportId, chartId, parameterSelectionList }) {
110
+ const report = await db.dashboardReport.loadById({ dashboardReportId }) // get the report object
111
+ const chart = await db.dashboardChart.loadById({ chartId }) // get the chart object
112
+ const reportChart = await db.dashboardReportChart.loadByReportAndChartId({ chartId, dashboardReportId }) // get the report chart for the rank/overrides
113
+ try {
114
+ const chartData = await formatChartDataForReport(db.pool, { reportParameters: report.parameters, chart, reportChart, parameterSelectionList })
115
+ return chartData
116
+ } catch (err) {
117
+ console.error(err)
118
+ return { isErrored: true }
119
+ }
120
+ }
121
+ ```
122
+
123
+ #### mysqlConnection
124
+ A connection or pool of connections from the `mysql` npm module.
125
+
126
+ #### reportParameters
127
+ The `parameters` array from a report object.
128
+
129
+ #### chartId
130
+ The id of a chart row in `wa_dashboard_chart`.
131
+
132
+ #### parameterSelectionList
133
+ The array of selected parameters sent from the client dashboard.
@@ -0,0 +1,254 @@
1
+ const { formatterTemplates } = require('./formatter-templates.json')
2
+ const toSnakeCase = require('to-snake-case')
3
+ const mysqlDataTypes = require('mysql/lib/protocol/constants/types')
4
+ const klona = require('klona')
5
+
6
+ const mysqlToGoogleChartsDataTypeMap = mysqlDataType => {
7
+ switch (mysqlDataType) {
8
+ case 'VAR_STRING':
9
+ case 'STRING':
10
+ case 'ENUM':
11
+ case 'VARCHAR':
12
+ return 'string'
13
+ case 'DECIMAL':
14
+ case 'NEWDECIMAL':
15
+ case 'BIT':
16
+ case 'TINY':
17
+ case 'SHORT':
18
+ case 'LONG':
19
+ case 'FLOAT':
20
+ case 'DOUBLE':
21
+ case 'LONGLONG':
22
+ case 'INT24':
23
+ case 'YEAR':
24
+ return 'number'
25
+ case 'TIMESTAMP':
26
+ case 'DATETIME':
27
+ case 'TIMESTAMP2':
28
+ case 'DATETIME2':
29
+ return 'datetime'
30
+ case 'DATE':
31
+ case 'NEWDATE':
32
+ return 'date'
33
+ case 'TIME':
34
+ case 'TIME2':
35
+ return 'timeofday'
36
+ default:
37
+ return 'string'
38
+ }
39
+ }
40
+
41
+ const makeDataTableCol = (key, fields, role = null) => {
42
+ return {
43
+ id: toSnakeCase(key).toLowerCase(),
44
+ label: key,
45
+ type: mysqlToGoogleChartsDataTypeMap(fields[key]),
46
+ role,
47
+ }
48
+ }
49
+
50
+ module.exports = {
51
+ queryWithFields: (connection, query) => { // the only function here that talks to the server at all
52
+ return new Promise((resolve, reject) => {
53
+ connection.query(query, (err, results, fields) => {
54
+ if (err) {
55
+ reject(err)
56
+ } else {
57
+ resolve({
58
+ results,
59
+ fields: fields.reduce((sum, field) => {
60
+ return {
61
+ ...sum,
62
+ [field.name]: mysqlDataTypes[field.type] || field.type,
63
+ }
64
+ }, {}),
65
+ })
66
+ }
67
+ })
68
+ })
69
+ },
70
+ getMultiSeriesData: ({ data: queryResult, seriesOptions, formatting, fields }) => {
71
+ const { groupXAxisBy, series, value, tooltip } = seriesOptions
72
+ //Get an array of all of the keys for the series
73
+ const allOfTheKeysInTheSeries = new Set(queryResult.map(row => row[series]))
74
+
75
+ let objectOfAllKeysWithNullValues = {}
76
+ for (const key of allOfTheKeysInTheSeries) {
77
+ objectOfAllKeysWithNullValues[key] = null
78
+ //add in a spot for the tooltips for each column to go
79
+ if (tooltip) {
80
+ objectOfAllKeysWithNullValues[`${tooltip} ${key}`] = null
81
+ }
82
+ }
83
+
84
+ let grouped = {}
85
+
86
+ //create objects for each thing they're grouping by
87
+ // console.time(`create objects for each thing they're grouping by`)
88
+ for (const row of queryResult) {
89
+ //if we don't yet have a prop for what we're grouping by, then we need to initialize an object of all null values for it
90
+ if (!grouped[row[groupXAxisBy]]) {
91
+ grouped[row[groupXAxisBy]] = klona(objectOfAllKeysWithNullValues) //give nulls for all values by default
92
+ }
93
+
94
+ grouped[row[groupXAxisBy]][row[series]] = row[value]
95
+ // add the tooltips for each series in each row
96
+ if (tooltip) {
97
+ grouped[row[groupXAxisBy]][`${tooltip} ${row[series]}`] = row[tooltip]
98
+ }
99
+ }
100
+ // console.timeEnd(`create objects for each thing they're grouping by`)
101
+
102
+ //The mysql field types need to map to our output data
103
+ let seriesFields = { [groupXAxisBy]: fields[groupXAxisBy] }
104
+ let seriesFormatting = {}
105
+ for (const key of allOfTheKeysInTheSeries) {
106
+ seriesFields[key] = fields[value]
107
+ //add tooltips here, so a tooltip column is created for every series column
108
+ if (tooltip) {
109
+ seriesFields[`${tooltip} ${key}`] = fields[tooltip]
110
+ }
111
+ //If there is any formatting applied to the value, we need to apply it to all of the other keys
112
+ if (formatting && value in formatting) {
113
+ seriesFormatting[key] = formatting[value]
114
+ }
115
+ }
116
+
117
+ //turn it back to an array and make sure the grouping column is first as required by a pie chart(at least)
118
+ // console.time(`group it back together`)
119
+ let multiSeriesData = []
120
+ for (const key in grouped) {
121
+ multiSeriesData.push({
122
+ [groupXAxisBy]: key,
123
+ ...grouped[key],
124
+ })
125
+ }
126
+ // console.timeEnd(`group it back together`)
127
+ // console.log(`${multiSeriesData.length} rows with ${Object.keys(multiSeriesData[0]).length} props each.`)
128
+ return {
129
+ data: multiSeriesData,
130
+ formatting: {
131
+ ...formatting,
132
+ ...seriesFormatting,
133
+ },
134
+ fields: seriesFields,
135
+ }
136
+ },
137
+ makeNegativeNumbericValuesZero: resultSet => {
138
+ const numberCols = Object.keys(resultSet[0]).filter(key => typeof resultSet[0][key] === 'number')
139
+
140
+ return resultSet.map(row => {
141
+ numberCols.forEach(col => {
142
+ if (row[col] < 0) {
143
+ row[col] = 0
144
+ }
145
+ })
146
+
147
+ return row
148
+ })
149
+ },
150
+ coerceNullColumn: (columnProp, data, destinationType) => {
151
+ const hasNullValue = data.find(row => row[columnProp] === null)
152
+
153
+ if (hasNullValue) {
154
+ const rowWithValidValue = data.find(row => row[columnProp] !== null)
155
+ let nullReplacement = ''
156
+
157
+ if ((destinationType && destinationType === 'number') || (typeof rowWithValidValue[columnProp] === 'number')) {
158
+ nullReplacement = 0
159
+ }
160
+
161
+ return data.map(row => {
162
+ return {
163
+ ...row,
164
+ [columnProp]: row[columnProp] ?? nullReplacement,
165
+ }
166
+ })
167
+ } else {
168
+ return data
169
+ }
170
+ },
171
+ handleFormattingTemplates: formatting => {
172
+ if (formatting && typeof formatting === 'object') {
173
+ for (const key in formatting) {
174
+ if (typeof formatting[key].format === 'string') {
175
+ if (formatting[key].format in formatterTemplates) {
176
+ formatting[key].format = formatterTemplates[formatting[key].format]
177
+ } else {
178
+ formatting[key].format = {}
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ return formatting
185
+ },
186
+ getDataTableFormat: (data, fields, multiSeries) => {
187
+ let cols = []
188
+ // domain column for multiseries charts
189
+ if (multiSeries) {
190
+ cols = [ makeDataTableCol(multiSeries.groupXAxisBy, fields) ]
191
+ }
192
+ // All other columns (including domain for non multiseries charts)
193
+ for (const key in fields) {
194
+ if (!multiSeries || key !== multiSeries.groupXAxisBy) {
195
+ if (multiSeries && multiSeries.tooltip && key.includes(multiSeries.tooltip)) {
196
+ // If it's a tooltip, set the column's role exlicitly
197
+ cols.push(makeDataTableCol(key, fields, 'tooltip'))
198
+ } else {
199
+ // Otherwise, role is implicitly assigned
200
+ cols.push(makeDataTableCol(key, fields))
201
+ }
202
+ }
203
+ }
204
+
205
+ const rows = data.map(row => {
206
+ let items = []
207
+
208
+ for (const column in row) {
209
+ items.push({ v: row[column] })
210
+ }
211
+
212
+ return {
213
+ c: items,
214
+ }
215
+ })
216
+
217
+ return { cols, rows }
218
+ },
219
+ makeDataCumulative: dataTable => {
220
+ dataTable.rows.forEach((row, rowIndex) => {
221
+ if (rowIndex > 0) {
222
+ row.c.forEach((col, colIndex) => {
223
+ if (colIndex > 0 && col.v == null) {
224
+ col.v = dataTable.rows[rowIndex - 1].c[colIndex].v
225
+ } else if (colIndex > 0) {
226
+ col.v = col.v + dataTable.rows[rowIndex - 1].c[colIndex].v
227
+ }
228
+ })
229
+ }
230
+ })
231
+ return dataTable
232
+ },
233
+ getRactiveTableDataFormat: (data, fields, formatting) => {
234
+ const columns = Object.keys(fields).map(field => {
235
+ // right align currency values
236
+ if (formatting[field]?.type === 'NumberFormat' && formatting?.[field].format.prefix === '$') {
237
+ return {
238
+ property: field,
239
+ name: field,
240
+ class: 'text-right',
241
+ }
242
+ }
243
+ return {
244
+ property: field,
245
+ name: field,
246
+ }
247
+ })
248
+ return {
249
+ columns,
250
+ rows: data,
251
+ sortedRows: [],
252
+ }
253
+ },
254
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "formatterTemplates": {
3
+ "CURRENCY": {
4
+ "prefix": "$",
5
+ "fractionDigits": 2,
6
+ "groupingSymbol": ","
7
+ }
8
+ }
9
+ }
package/index.js ADDED
@@ -0,0 +1,181 @@
1
+ const db = require('@isoftdata/utility-db')
2
+ const {
3
+ getMultiSeriesData,
4
+ makeNegativeNumbericValuesZero,
5
+ coerceNullColumn,
6
+ handleFormattingTemplates,
7
+ getDataTableFormat,
8
+ queryWithFields,
9
+ makeDataCumulative,
10
+ getRactiveTableDataFormat: getRactiveTableDataFormat } = require('./chart-helper.js')
11
+ const { datesFromRange } = require('@isoftdata/utility-date-time')
12
+ const formatDate = require('date-fns/format')
13
+
14
+ const handleChartTypeSpecificQuirks = (chartType, resultSet) => {
15
+ if (resultSet.length) {
16
+ switch (chartType) {
17
+ case 'PieChart':
18
+ //PieCharts error if you have negative numbers, so we'll just set those to 0
19
+ resultSet = makeNegativeNumbericValuesZero(resultSet)
20
+ break
21
+ case 'TreeMap':
22
+ //tree charts need the 2nd columns to be not null
23
+ resultSet = coerceNullColumn(Object.keys(resultSet[0])[1], resultSet)
24
+ break
25
+ default:
26
+ //all charts need the first column to be not null
27
+ resultSet = coerceNullColumn(Object.keys(resultSet[0])[0], resultSet)
28
+ }
29
+ }
30
+
31
+ return resultSet
32
+ }
33
+
34
+ const loadChartData = async(mysqlConnection, { name, query: queryObject, formatting, chartWrapper, multiSeries, parameters, table, ...theRest }) => { // needs database connection
35
+ let { results: queryResultSet, fields } = (queryObject && queryObject.sql) ? await queryWithFields(mysqlConnection, queryObject) : { fields: [], results: [] }
36
+ let data = handleChartTypeSpecificQuirks(chartWrapper.chartType, queryResultSet)
37
+
38
+ if (multiSeries) {
39
+ ({ data, formatting, fields } = getMultiSeriesData({
40
+ data,
41
+ seriesOptions: multiSeries,
42
+ formatting,
43
+ fields,
44
+ }))
45
+ }
46
+
47
+ const proccessedFormatting = handleFormattingTemplates(formatting)
48
+
49
+ let dataTable = getDataTableFormat(data, fields, multiSeries) //per the Google Charts docs, the DataTable object literal format is the most performant, so we'll turn it into that format before returning
50
+
51
+ // Stacked area charts will have holes / jagged lines if there are nulls in the middle of a column
52
+ // Presumably we'll also want to be able to have non-cumulative area charts at some point
53
+ if (chartWrapper.chartType == 'AreaChart') {
54
+ dataTable = makeDataCumulative(dataTable)
55
+ }
56
+
57
+ if (chartWrapper.chartType === 'Table') {
58
+ return {
59
+ ...theRest,
60
+ chartWrapper,
61
+ table: { ...table, ...getRactiveTableDataFormat(data, fields, formatting) },
62
+ formatting: proccessedFormatting,
63
+ }
64
+ }
65
+ return {
66
+ ...theRest,
67
+ chartWrapper: {
68
+ ...chartWrapper,
69
+ dataTable,
70
+ },
71
+ formatting: proccessedFormatting,
72
+ }
73
+ }
74
+
75
+ const loadOptionList = async(mysqlConnection, optionListQuery, optionList = []) => {
76
+ return [
77
+ ...optionList,
78
+ // ...(optionListQuery && await query(mysqlConnection, optionListQuery)) || [],
79
+ ...(optionListQuery && await db.query(mysqlConnection, optionListQuery)) || [],
80
+
81
+ ]
82
+ }
83
+
84
+ const loadOutputParameterValues = async(mysqlConnection, definitionList, selectionList = []) => {
85
+ let parameterValues = []
86
+
87
+ for (const definition of definitionList) {
88
+ const matchingParameterSelection = selectionList.find(param => param.name === definition.name)
89
+
90
+ let parameter = {
91
+ name: definition.name,
92
+ type: definition.type,
93
+ title: definition.title || definition.name,
94
+ default: definition.default,
95
+ value: (matchingParameterSelection && matchingParameterSelection.value) || definition.default,
96
+ queryValues: {},
97
+ }
98
+
99
+ if (definition.type === 'dateRange') {
100
+ parameter.dates = matchingParameterSelection?.dates || { to: null, from: null }
101
+ parameter.value = parameter.value || 'Last 30 Days'
102
+ if (parameter.value !== 'Custom') {
103
+ parameter.dates = datesFromRange(parameter.value)
104
+ }
105
+ parameter.queryValues = {
106
+ [`${parameter.name}_start_date`]: parameter.dates.from,
107
+ [`${parameter.name}_end_date`]: parameter.dates.to,
108
+ }
109
+ } else if (definition.type === 'selection') {
110
+ parameter.optionList = await loadOptionList(mysqlConnection, definition.optionListQuery, definition.optionList) // queries db
111
+ parameter.queryValues = {
112
+ [parameter.name]: parameter.value,
113
+ }
114
+ } else if (definition.type === 'date') {
115
+ parameter.value = parameter.value || formatDate(new Date(), 'yyyy-MM-dd')
116
+
117
+ parameter.queryValues = {
118
+ [parameter.name]: parameter.value,
119
+ }
120
+ }
121
+
122
+ parameterValues.push(parameter)
123
+ }
124
+
125
+ return parameterValues
126
+ }
127
+
128
+ const parameterizeQuery = (query, parameterValues) => {
129
+ const queryParameterValues = parameterValues.reduce((acc, val) => {
130
+ return val.queryValues ? {
131
+ ...acc,
132
+ ...val.queryValues,
133
+ } : acc
134
+ }, {})
135
+
136
+ //match on a token that starts with ${ and ends with a closing }.
137
+ //Then include a capture group to get the inner part of that to use as a key
138
+ const regexp = /\${([^}]+)}/g
139
+
140
+ const queryParameterKeys = [ ...query.matchAll(regexp) ].map(([ fullToken, insideToken ]) => insideToken.trim())
141
+ //if they put a token in the query that isn't one of the defined parameters of the report, just return null?
142
+ const queryParameters = queryParameterKeys.map(key => queryParameterValues[key] || null)
143
+
144
+ return {
145
+ sql: query.replace(regexp, '?'),
146
+ values: queryParameters,
147
+ }
148
+ }
149
+
150
+ module.exports = {
151
+ formatReportMetadata: async(mysqlConnection, { report, chartsInReport }) => {
152
+ const parameterValues = await loadOutputParameterValues(mysqlConnection, report.parameters)
153
+ const parameterValuesForClient = parameterValues.map(param => {
154
+ const { queryValues, ...parameterValuesForClient } = param
155
+ return parameterValuesForClient
156
+ })
157
+
158
+ return {
159
+ dashboardReportId: report.dashboardReportId,
160
+ name: report.reportName,
161
+ title: report.reportTitle,
162
+ charts: chartsInReport,
163
+ parameterValues: parameterValuesForClient,
164
+ ownerId: report.ownerId,
165
+ }
166
+ },
167
+ formatChartDataForReport: async(mysqlConnection, { reportParameters, chart, reportChart, parameterSelectionList }) => {
168
+ try {
169
+ const parameterValues = await loadOutputParameterValues(mysqlConnection, reportParameters, parameterSelectionList)
170
+ return await loadChartData(mysqlConnection, {
171
+ ...chart,
172
+ reportChartId: reportChart.reportChartId,
173
+ ...reportChart.jsonOverride, // overwrites whole chart wrapper :\
174
+ query: parameterizeQuery(chart.query, parameterValues),
175
+ })
176
+ } catch (err) {
177
+ console.error(err)
178
+ return { isErrored: true, err }
179
+ }
180
+ },
181
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@isoftdata/utility-dashboard-backend",
3
+ "version": "1.0.0",
4
+ "description": "A utility for formatting chart and report data to be usable by the frontend dashboard component.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "Charles Kaup",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@isoftdata/utility-date-time": "^2.0.3",
13
+ "@isoftdata/utility-db": "^2.2.0",
14
+ "date-fns": "^2.23.0",
15
+ "klona": "^2.0.4",
16
+ "mysql": "^2.18.1",
17
+ "to-snake-case": "^1.0.0"
18
+ }
19
+ }
package/schema.md ADDED
@@ -0,0 +1,120 @@
1
+ ## Reporting/Charting
2
+ The dashboard component has a set of three database tables where reports, charts, and their associations are defined: `dashboard_report`, `dashboard_chart`, and `dashboard_report_chart`. Reports are a collection of various charts. Reports have parameters and charts can use said parameters. Currently Charts are rendered using [Google Charts](https://developers.google.com/chart/).
3
+
4
+ ### Defining a Report
5
+ Reports can be created either with the frontend dashboard component or manually in the database. The frontend component supports setting the report's name, the charts contained within it, who it is shared with, and the default value of any parameters. Currently it is not possible to add or remove parameters for reports within the frontend component, as charts' queries also have to support any additional parameters.
6
+
7
+ ### Report Database Row - `dashboard_report`
8
+ Property | Type | Description
9
+ ---- | ---- | -----------
10
+ dashboard_report_id | `Unsigned integer` | **Required** - Primary key. Auto incremented.
11
+ report_name | `String` | **Required** - a unique name for the report.
12
+ report_title | `String` | When rendered, this title will be displayed at the top of the card. If omitted, `name` will be used.
13
+ json | `JSON` | Should be parsed into a JS object. This field contains JSON for an array of parameter objects, the format of which is detailed below.
14
+ share_type | `Enum` | Controls access to reports within the dashboard. Possible values include: `everyone`, `group`, `store`, and `user`. Defaults to `everyone`.
15
+ share_id | `Unsigned integer` | Used in conjunction with `share_type` to control access to reports. When `share_type` is 'everyone', this field is null. Otherwise, it contains the id of the user, group, or store to share the report with. Defaults to `NULL`.
16
+ owner_id | `Unsigned integer` | The `user_account_id` of the user who created the report in the dashboard. This user will always have access to the given report, regardless of its sharing settings, and will be shown as the report owner on the configuration screen. Defaults to `NULL`, which designates it as a default report.
17
+
18
+ #### Parameter Object
19
+ Property | Type | Description
20
+ ---- | ---- | -----------
21
+ name | `String` | **Required** - a unique name for the parameter. This name is also used to reference param in a query.
22
+ title | `String` | This is used to label the parameter for the user.
23
+ type | `String` | Can be one of `dateRange`, `date`, or `selection`. See the tables below for parameter-type-specific properties
24
+
25
+ ##### `dateRange` Type Properties
26
+ > In the UI, the user will see a dropdown with the possible date range values(see `default` description below).
27
+
28
+ Property | Type | Description
29
+ ---- | ---- | -----------
30
+ default | `String` | One of the following values: `'Today'`, `'Yesterday'`, `'Last 7 Days'`, `'Last 30 Days'`, `'Last 90 Days'`, `'Last 365 Days'`, `'This Week'`, `'This Month'`, `'This Quarter'`, `'This Year'`, `'Previous Week'`, `'Previous Month'`, `'Previous Quarter'`, or `'Previous Year'`. If the `default` property is omitted or [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), `'Last 30 Days'` will be used.
31
+
32
+ ##### `date` Type Properties
33
+ > In the UI, the user will see a dropdown with a date picker.
34
+
35
+ Property | Type | Description
36
+ ---- | ---- | -----------
37
+ default | `String` | Can be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date string(eg. `'2021-01-13'`). If the `default` property is omitted or [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), the today's date will be used.
38
+
39
+ ##### `selection` Type properties
40
+ > In the UI, the user will see a dropdown with the `optionList` values(if any) and the values returned from the `optionListQuery`(if any).
41
+
42
+ Property | Type | Description
43
+ ---- | ---- | -----------
44
+ default | `String` | The `id` of the option you'd like to be selected by default
45
+ optionList | `Object` | Each object should have an `id`, and `name`. `id` is the value that will be given to you at query time and `name` is what will be displayed in the dropdown to the user.
46
+ optionListQuery | `String` | A query that selects 2 columns `id` and `name`. The results of this query will be merged with `optionList` and put in the dropdown for the user.
47
+
48
+ #### Example
49
+ 'dashboard_report_id' | 'report_name' | 'report_title' | 'json' | 'share_type' | 'share_id' | 'owner_id'
50
+ -- | ---- | ---- | ------- | -- | -- | --
51
+ 1 | report_overview | Overview | `{"parameters": [{"name": "date_range", "type": "dateRange", "title": "Date Range", "default": "This Year"}]}` | everyone | `NULL` | `NULL`
52
+ 2 | shared_report | Shared Report | `{"parameters":[{"name":"date_range","type":"dateRange","title":"Date Range","default":"This Month"}]}` | user | 1 | 2
53
+
54
+ ### Defining a Chart
55
+ Currently, charts cannot be defined within Pro web, and must be created in the database table `dashboard_chart`. Currently, 19 charts are defined, but some are set to not show as options in the user interface.
56
+
57
+ ### Chart Database Row - `dashboard_chart`
58
+ Property | Type | Description
59
+ ---- | ---- | -----------
60
+ chart_id | `Unsigned integer` | **Required** - primary key. Auto incremented.
61
+ chart_name | `String` | **Required** - a unique name for the chart
62
+ chart_title | `String` | When rendered, this title will be displayed at the top of the card. If omitted, `name` will be used.
63
+ supertype | `Enum` | **Required** - designates how the chart should be loaded. Options include `google`, `table`, and `embed`. Defaults to `google`.
64
+ json | `json` | Should be parsed to a JS object on the server. Contains several properties: `query` (string, **required**), [`chartWrapper`](https://developers.google.com/chart/interactive/docs/reference#chartwrapper-class) (object, **required** for Google charts), `multiSeries` (object, **required** for multi-series charts), `formatting` (object).
65
+ show_chart | `Bit` | **Required** - Should be cast as a boolean on the server. Whether or not to show a chart in the configuration UI. Defaults to `1`.
66
+
67
+ #### MultiSeries Object
68
+ Property | Type | Description
69
+ ---- | ---- | -----------
70
+ groupXAxisBy | `String` | **Required** - The name of the query column that determines the X axis of the chart.
71
+ series | `String` | **Required** - The name of the query column that determines which series a query row corresponds to.
72
+ value | `String` | **Required** - The namem of the query column that contains the value for that data point.
73
+ tooltip | `String` | The name of the (optional) query column that contains a custom plaintext tooltip, which replaces the default Google Charts tooltip. Does not work for Tree Map type charts, which have special tooltip requirements.
74
+
75
+ #### Formatting Object
76
+ An object whose property names are the columns in the query to be formatted. Currently only currency formatting is supported.
77
+
78
+ Property | Type | Description
79
+ ---- | ---- | ---------
80
+ type | `String` | The name of a [Google Charts Formatter](https://developers.google.com/chart/interactive/docs/reference?hl=en#formatters).
81
+ format | `String` | The name of a formatter template, defined in `formatter-templates.json`.
82
+ #### JSON Example
83
+
84
+ ```json
85
+ {
86
+ "chartWrapper": {
87
+ "chartType": "AreaChart",
88
+ "options": {
89
+ "isStacked": true,
90
+ "width": 1000,
91
+ "height": 1000
92
+ }
93
+ },
94
+ "multiSeries": {
95
+ "groupXAxisBy": "Date",
96
+ "series": "Part Type",
97
+ "value": "Sales",
98
+ "tooltip": "Tooltip"
99
+ },
100
+ "formatting": {
101
+ "Sales": {
102
+ "type": "NumberFormat",
103
+ "format": "CURRENCY"
104
+ }
105
+ },
106
+ "query": "SELECT t.date AS `Date`,\r\n IFNULL (t.part_type, 'Misc. Lineitems') AS `Part Type`,\r\n ROUND(@running_total:=@running_total + t.sales, 2) AS `Sales`\r\nFROM\r\n(SELECT \r\n`wa_sale`.`document_date` AS `Date`,\r\nIF(wa_sale_line.part_type = '', 'Misc. Lineitems', wa_sale_line.part_type) AS `part_type`,\r\nFORMAT(SUM(wa_sale_line.quantity * `wa_sale_line`.`price`), 2) AS `sales`\r\nFROM \r\n`wa_sale_line`\r\nINNER JOIN `wa_sale` USING(`sale_id`)\r\nWHERE\r\n`wa_sale`.`status` = 'Invoice' AND wa_sale.document_date IS NOT NULL\r\n GROUP BY \r\n `wa_sale`.`document_date`, `wa_sale_line`.`part_type`) t\r\n JOIN (SELECT @running_total:=0) r\r\n ORDER BY t.Date;"
107
+ }
108
+ ```
109
+
110
+ ### Adding charts to reports
111
+ Adding charts to reports can be handled entirely within the dashboard component. Each row in the database table `dashboard_report_chart` defines the association of one chart to one report, the rank of the chart within the report, and any additional object properties to apply to the chart when displayed in that report.
112
+
113
+ ### Report Chart Database Row - `dashboard_report_chart`
114
+ Property | Type | Description
115
+ ---- | ---- | --------
116
+ report_chart_id | `Unsigned integer` | **Required** - Primary key. Auto incremented.
117
+ dashboard_report_id | `Unsigned integer` | **Required** - Foreign key constraint with `dashboard_report.dashboard_report_id`.
118
+ chart_id | `Unsigned integer` | **Required** - Foreign key constraint with `dashboard_chart.dashboad_chart_id`. `chart_id` and `dashboard_report_id` must be a unique pair.
119
+ rank | `Unsigned integer` | The order to display the chart in on the report. Defaults to 1.
120
+ json_override | `Text` | Parsed as JSON on the server. Contains any JSON properties to be applied to the chart for the given report, overriding any of the same property on the base chart. Primarily used for chartWarpper options. Default `NULL`.