DEV Community

Cover image for Create data reports using JavaScript function
Ron Clarijs
Ron Clarijs

Posted on • Edited on

Create data reports using JavaScript function

Assume you have a sporting event or league. The results are likely stored in a database and need to be displayed on a website. You can use the Fetch API to retrieve the data from the backend, although this document assumes the data has already been retrieved and is available as an array of records. This array must be in the correct order, but the source function can filter and sort the array on the fly within the report engine.

This document explains how to easily define headers and footers and arrange record grouping using compare function. Each header function returns HTML based on static text and parameters currentRecord, objWork, and splitPosition. Each footer function returns HTML based on static text and parameters previousRecord, objWork, and splitPosition. It is very flexible, but you have to create the HTML yourself; don't expect a WYSIWYG editor.

General structure of a report

A report consists of a report header and footer, which can be text, HTML, or both. The report contains one or more section levels:

  • Section level N starts with header level N and ends with footer level N.
  • Section level N contains one or more instances of section level N+1, except for the highest section level.
  • The highest section level contains data created based on the records in the array, usually in the form of an HTML table or flex item.

Example of report structure

Example of reportDefinition object

The structure of the report definition object is as follows:

const reportDefinition = {};
reportDefinition.headers = [report_header, header_level_1, header_level_2, header_level_3, ...]; // default = []
reportDefinition.footers = [report_footer, footer_level_1, footer_level_2, footer_level_3, ...]; // default = []
reportDefinition.compare = (previousRecord, currentRecord, objWork) => {
    // default = () => -1
    // code that returns an integer (report level break number)
};
reportDefinition.display = (currentRecord, objWork) => {
    // code that returns a string, for example
    return `${currentRecord.team} - ${currentRecord.player}`;
};
// source array can be preprocessed, for example filter or sort
reportDefinition.source = (arr, objWork) => preProcessFuncCode(arr); // optional function to preprocess data array
// example to add extra field for HOME and AWAY and sort afterwards
reportDefinition.source = (arr, objWork) => arr.flatMap(val => [{ team: val.team1, ...val }, { team: val.team2, ...val }])
    .sort((a, b) => a.team.localeCompare(b.team));
// optional method 'init' which should be a function. It will be called with argument objWork
// can be used to initialize some things.
reportDefinition.init = objWork => { ... };
Enter fullscreen mode Exit fullscreen mode

Examples of headers and footers array elements

reportDefinition.headers = [];
// currentRecord=current record, objWork is extra object,
// splitPosition=0 if this is the first header shown at this place, otherwise it is 1, 2, 3 ...
reportDefinition.headers[0] = (currentRecord, objWork, splitPosition) => {
    // code that returns a string
};
reportDefinition.headers[1] = '<div>Some string</div>'; // string instead of function is allowed;
reportDefinition.footers = [];
// previousRecord=previous record, objWork is extra object,
// splitPosition=0 if this is the last footer shown at this place, otherwise it is 1, 2, 3 ...
reportDefinition.footers[0] = (previousRecord, objWork, splitPosition) => {
    // code that returns a string
};
reportDefinition.footers[1] = '<div>Some string</div>'; // string instead of function is allowed;
Enter fullscreen mode Exit fullscreen mode

Example of compare function

Here's a basic compare function:

// previousRecord=previous record, currentRecord=current record, objWork is extra object,
reportDefinition.compare = (previousRecord, currentRecord, objWork) => {
    // please never return 0! headers[0] will be displayed automagically on top of report
    // group by date return 1 (lowest number first)
    if (previousRecord.date !== currentRecord.date) return 1;
    // group by team return 2
    if (previousRecord.team !== currentRecord.team) return 2;
    // assume this function returns X (except -1) then:
    // footer X upto and include LAST footer will be displayed (in reverse order). In case of footer function the argument is previous record
    // header X upto and include LAST header will be displayed. In case of header function the argument is current record
    // current record will be displayed
    //
    // if both records belong to same group return -1
    return -1;
};
Enter fullscreen mode Exit fullscreen mode

This is very basic compare code and is the same as:

reportDefinition.compare = GroupBy(['date', 'team'], 1);
Enter fullscreen mode Exit fullscreen mode

as long as you define GroupBy function:

const GroupBy = (fields, level = 1) => (prv, cur) => {
    const index = fields.findIndex(field => cur[field] !== prv[field]);
    return (index === -1) ? -1 : index + level;
};
Enter fullscreen mode Exit fullscreen mode

Running counter

In case you want to implement a running counter, you have to initialize/reset it in the right place. It can be achieved by putting some code in the relevant header:

reportDefinition.headers[2] = (currentRecord, objWork, splitPosition) => {
    // this is a new level 2 group. Reset objWork.runningCounter to 0
    objWork.runningCounter = 0;
    // put extra code here
    return `<div>This is header number 2: ${currentRecord.team}</div>`;
};
Enter fullscreen mode Exit fullscreen mode

If you only want to initialize objWork.runningCounter at the beginning of the report you can achieve that by putting the right code in reportDefinition.headers[0]. I call it property runningCounter, but you can give it any name you want.

You have to increase the running counter somewhere in your code because... otherwise it is not running 😃 for example:

reportDefinition.display = (currentRecord, objWork) => {
    objWork.runningCounter++;
    // put extra code here
    return `<div>This is record number ${objWork.runningCounter}: ${currentRecord.team} - ${currentRecord.player}</div>`;
};
Enter fullscreen mode Exit fullscreen mode

Most of the time you don't need to manage a running counter in JavaScript! Most likely data comes from a database which can be queried by SQL. Use the SQL WINDOW function ROW_NUMBER or RANK to do the hard work.

How to create totals for multiple section levels, a running total and even a numbered header

// headers
// -----------------------------------------------------
reportDefinition.headers[3] = (record, objWork) => {
    objWork.totalSection3 = 0;
    // Section3 has multiple Section4's inside. We will number those Section4's
    objWork.runningCounterSection4 = 0;
    // any other code that is necessary and display the header
    return 'any string';
};
reportDefinition.headers[4] = (record, objWork) => {
    objWork.runningCounterSection4++;
    // any other code that is necessary and display the header
    return `${objWork.runningCounterSection4}: text header 4`;
};
reportDefinition.headers[5] = (record, objWork) => {
    objWork.totalSection5 = 0;
    // any other code that is necessary and display the header
    return 'any string';
};
reportDefinition.headers[6] = (record, objWork) => {
    objWork.totalSection6 = 0;
    // any other code that is necessary and display the header
    return 'any string';
};
// footers
// -----------------------------------------------------
reportDefinition.footers[3] = (record, objWork) => {
    // no extra code needed
    // any other code that is necessary and display the footer
    // most likely you display objWork.totalSection3 here
    return `<h4>League above total: ${objWork.totalSection3}</h4>`;
};
reportDefinition.footers[4] = (record, objWork) => {
    // no extra code needed
    // any other code that is necessary and display the footer
    // if you want you can display objWork.totalSection3 as running total
    // it will be a running total because reportDefinition.headers[4] didn't reset objWork.totalSection3
    return `<h4>League above running total: ${objWork.totalSection3}</h4>`;
};
reportDefinition.footers[5] = (record, objWork) => {
    // no extra code needed
    // any other code that is necessary and display the footer
    // most likely you display objWork.totalSection5 here
    return `<h4>State above total: ${objWork.totalSection5}</h4>`;
};
reportDefinition.footers[6] = (record, objWork) => {
    // no extra code needed
    // any other code that is necessary and display the footer
    // most likely you display objWork.totalSection6 here
    return `<h4>City above total: ${objWork.totalSection6}</h4>`;
};
// -----------------------------------------------------
reportDefinition.display = (record, objWork) => {
    // 'amount' is the field to totalize
    const { amount } = record;
    objWork.totalSection3 += amount;
    objWork.totalSection5 += amount;
    objWork.totalSection6 += amount;
    // diplay the record
    return 'string containing record details';
};
Enter fullscreen mode Exit fullscreen mode

How to preprocess the source array on the fly (for example in click event)

// <div id="playersListContainer">
//    <span class="ks_clickable">All Players</span> <span class="ks_clickable">John Doe</span> <span class="ks_clickable">Peter Pan</span> <span class="ks_clickable">Jim Baker</span> <span class="ks_clickable">Harry Potter</span>
// </div>
// <div id="mainOutput"></div>
document.getElementById('playersListContainer').addEventListener('click', e => {
    if (e.target.matches('.ks_clickable')) { e.currentTarget.querySelector('.ks_clickable.ks_clicked')?.classList.remove('ks_clicked');
        e.target.classList.add('ks_clicked');
        // define extra filtering depending on which player name you clicked
        const preProcess = {};
        if (e.target.textContent !== 'All Players') {
            preProcess.source = (arr, objWork) => arr.filter(record => [record.Player1, record.Player2].includes(e.target.textContent));
        }
        // create a shallow copy of the reportDefinition by object spreading and add properties of preProcess as well
        // dataPlayersDetails = data fetched from database
        document.getElementById('mainOutput').innerHTML = createOutput({ ...reportDefinition, ...preProcess })(dataPlayersDetails);
        // in case dataPlayersDetails is a Promise:
        // dataPlayersDetails.then(data => {document.getElementById('mainOutput').innerHTML = createOutput({ ...reportDefinition, ...preProcess })(data)});
    }
});
Enter fullscreen mode Exit fullscreen mode

How to generate the report

// reportDefinition has to be defined first
const report = createOutput(reportDefinition, objWork); // objWork isn't needed most of the time
// variable data: array of records to be processed
document.getElementById('id of html element').innerHTML = report(data);
Enter fullscreen mode Exit fullscreen mode

Source code

Below you find a link to the source code I created to make this all work. It is kind of wrapper function for all headers and footers. Feel free to copy paste it or link to the code in jsdelivr CDN. Function CreateOutput and function GroupBy is all you need.
https://cdn.jsdelivr.net/gh/kaktussoft/ksToolkit@latest/js/ksToolkit.js
or https://cdn.jsdelivr.net/gh/kaktussoft/ksToolkit@v1.0.0/js/ksToolkit.js to force using a specific version. GITHUB REPO: https://github.com/kaktussoft/ksToolkit

What is objWork

objWork is a JavaScript object that is passed as the second argument to createOutput function (optional argument, default {}). It is passed on as shallow copy to headers functions, footers functions, compare function, init function, source function and display function. All these functions share this object. You can use it for example for configuration information or color scheme. objWork is automagically extended with { rawData: inputData }. For example createOutput(reportDefinition, { font: 'Arial', font_color: 'blue' })(inputData).

Examples

The examples listed below are written in Dutch.
Reports for billiard club De Klos
Reports for billiard club 't Waeghje
Reports for billiard scores
More reports for carom billiards
Reports for petanque
and many more....

Top comments (0)