Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDESK-4847] Implement CSV download for tables #113

Merged
merged 1 commit into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions client/charts/SDChart/Axis.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ export class Axis {
*/
this.excludeEmpty = false;

/**
* @ngdoc property
* @name SDChart.Axis#includeTotal
* @type {Boolean}
* @description If true, then adds the 'Total' column in table outputs
*/
this.includeTotal = true;

this._sortedCategories = undefined;
}

Expand All @@ -190,6 +198,7 @@ export class Axis {
* @param {String} [options.sortOrder] - If undefined, do not sort, otherwise sort categories in
* ascending or descending order based on series values (if series data is provided as an object)
* @param {Boolean} [options.excludeEmpty=false] If true, remove values with 0 from the categories and series
* @param {Boolean} [options.includeTotal] - If true, then adds the 'Total' column in table outputs
* @return {SDChart.Axis}
* @description Sets the options for the axis
*/
Expand Down Expand Up @@ -283,7 +292,9 @@ export class Axis {
* @description Generate the x-axis config
*/
genXAxisConfig(config) {
const axisConfig = {type: this.type};
const axisConfig = {
type: this.type === 'table' ? 'category' : this.type,
};

if (this.categories !== undefined) {
axisConfig.categories = this.getCategories();
Expand Down Expand Up @@ -386,4 +397,4 @@ export class Axis {

return config;
}
}
}
44 changes: 35 additions & 9 deletions client/charts/SDChart/Chart.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {forEach, get, sum, drop, cloneDeep} from 'lodash';

import {Axis} from './Axis';
import {convertHtmlStringToText} from '../../utils';

/**
* @ngdoc class
Expand Down Expand Up @@ -656,9 +657,30 @@ export class Chart {
rows: rows,
title: this.getTitle(),
subtitle: this.getSubtitle(),
genCSV: this.genCSVFromTable.bind(this),
};
}

genCSVFromTable() {
let csv = '';

csv += `"${this.config.headers.join('","')}"\n`;
this.config.rows.forEach((row) => {
let text = row.map((cell) => {
let cellText = convertHtmlStringToText(cell);

return typeof cellText === 'number' ?
cellText :
`"${cellText.replace(/"/g, '""')}"`;
})
.join(',');

csv += text + '\n';
});

return csv.replace(/\n/g, '\r\n');
}

/**
* @ngdoc method
* @name SDChart.Chart#genMultiTableConfig
Expand All @@ -669,8 +691,7 @@ export class Chart {
const axis = this.axis[0];

const headers = [axis.xTitle].concat(
axis.series.map((series) => series.getName()),
'Total'
axis.series.map((series) => series.getName())
);

const rows = axis.getTranslatedCategories().map((category) => ([category]));
Expand All @@ -681,13 +702,17 @@ export class Chart {
});
});

rows.forEach((row) => {
row.push(
sum(
drop(row)
)
);
});
if (axis.includeTotal) {
headers.push('Total');

rows.forEach((row) => {
row.push(
sum(
drop(row)
)
);
});
}

return {
id: this.id,
Expand All @@ -699,6 +724,7 @@ export class Chart {
rows: rows,
title: this.getTitle(),
subtitle: this.getSubtitle(),
genCSV: this.genCSVFromTable.bind(this),
};
}

Expand Down
4 changes: 4 additions & 0 deletions client/charts/SDChart/Series.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@ export class Series {
type: this.type !== undefined ? this.type : this.axis.defaultChartType || 'bar',
};

if (series.type === 'table') {
series.type = 'bar';
}

this.setDataConfig(series);
this.setPointConfig(series);
this.setStyleConfig(series);
Expand Down
62 changes: 62 additions & 0 deletions client/charts/SDChart/tests/SDChart.Chart.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,66 @@ describe('SDChart.Chart', () => {
fullHeight: true,
}));
});

it('can generate CSV from table', () => {
const chart = new SDChart.Chart({
id: 'test_table',
title: 'Testing Table to CSV',
chartType: 'table',
});

const axis = chart.addAxis()
.setOptions({
dfaultChartType: 'table',
xTitle: 'Sent',
includeTotal: false,
categories: [
'11/10/2019 15:43',
'12/10/2019 15:43',
'13/10/2019 15:43',
],
});

axis.addSeries()
.setOptions({
name: 'Slugline',
data: [
'Cri Aust',
'Test',
'Three',
],
});

axis.addSeries()
.setOptions({
name: 'Version',
data: [
1,
3,
7,
],
});

axis.addSeries()
.setOptions({
name: 'Reason',
data: [
'<div><p>Single line text</p></div>',
'<div><p>Multi line text</p><p>second line<br />third "line" test</p></div>',
'',
],
});

const config = chart.genConfig();
const csv = config.genCSV();

expect(csv).toBe('"Sent","Slugline","Version","Reason"\r\n' +
'"11/10/2019 15:43","Cri Aust",1,"Single line text"\r\n' +
'"12/10/2019 15:43","Test",3,"Multi line text\r\n' +
'\r\n' +
'second line\r\n' +
'third ""line"" test"\r\n' +
'"13/10/2019 15:43","Three",7,""\r\n'
);
});
});
2 changes: 1 addition & 1 deletion client/charts/directives/Chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function Chart(chartManager, $timeout, $sce, _) {
* @description Using the chartManager, download the chart data as a CSV file
*/
scope.downloadAsCSV = function() {
chartManager.downloadCSV(scope.config.id, 'chart.csv');
chartManager.downloadCSV(scope.config);
};

/**
Expand Down
13 changes: 9 additions & 4 deletions client/charts/services/ChartManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,18 @@ export function ChartManager(_, Highcharts, notify) {
/**
* @ngdoc method
* @name ChartManager#downloadCSV
* @param {String} id - The ID assocaited with the chart instance
* @param {String} filename - The name of the downloaded file
* @param {Object} config - The chart config object, containing the ID of the ID associated with the chart instance
* @description Converts the chart data to a CSV string, then downloads as a CSV file
*/
this.downloadCSV = function(id, filename) {
this.downloadCSV = function(config) {
const id = config.id;
const filename = `${id}.csv`;

if (_.get(this.charts, `${id}.getCSV`)) {
const csv = this.charts[id].getCSV();
// Either use a custom defined genCSV or the one from Highcharts
const csv = config.genCSV ?
config.genCSV() :
this.charts[id].getCSV();
const link = document.createElement('a');

link.setAttribute('href', 'data:text/text;charset=utf-8,' + encodeURIComponent(csv));
Expand Down
2 changes: 2 additions & 0 deletions client/charts/tests/ChartConfig.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ describe('chartConfig', () => {
chart: {type: 'column'},
title: 'Tables',
subtitle: 'For Today',
genCSV: jasmine.any(Function),
xAxis: [{
title: {text: 'Category'},
categories: ['Basketball', 'Advisories', 'Cricket'],
Expand Down Expand Up @@ -397,6 +398,7 @@ describe('chartConfig', () => {
chart: {type: 'column'},
title: 'Tables',
subtitle: 'For Today',
genCSV: jasmine.any(Function),
xAxis: [{
title: {text: 'Category'},
categories: ['Cricket', 'Basketball', 'Advisories'],
Expand Down
52 changes: 52 additions & 0 deletions client/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,55 @@ export const compileAndGetHTML = ($compile, scope, template, data = {}) => {

return html;
};

/**
* Utility to select and copy the text within a parent node
* @param {Node} node - The parent node used to select all text from
* @returns {string}
*/
export const getTextFromDOMNode = (node) => {
let innerText;

if (typeof document.selection !== 'undefined' && typeof document.body.createTextRange !== 'undefined') {
const range = document.body.createTextRange();

range.moveToElementText(node);
innerText = range.text;
} else if (typeof window.getSelection !== 'undefined' && typeof document.createRange !== 'undefined') {
const selection = window.getSelection();

selection.selectAllChildren(node);
innerText = '' + selection;
selection.removeAllRanges();
}
return innerText;
};

/**
* Utility to convert text into HTML DOM Nodes, then select and return the text shown
* This replicates the user highlighting the text within those nodes, using the Browser
* to do the grunt work for us.
* @param {String|Number} data - The html in string format, i.e. '<div><p>testing</p></div>'
* @returns {string|Number}
*/
export const convertHtmlStringToText = (data) => {
if (typeof data !== 'string' || !data.startsWith('<')) {
return data;
}

const node = document.createElement('div');
let text;

node.innerHTML = data;

// Attach the node to the document before selecting
// This is required otherwise the browser won't be able to
// select the text
document.body.appendChild(node);
text = getTextFromDOMNode(node);

// Make sure we clean up after adding the node to the document
document.body.removeChild(node);

return text;
};