Compare commits

...

3 Commits

Author SHA1 Message Date
David Pineau 777b2bbf0b Fixup - Remove IRM from package.json 2016-06-20 20:32:38 +02:00
Lam Pham-Sy 49e6510b7a Plotter: functional tests for plotter for
- Simple plot
- Multiple plot
- Define parameters via .json file or an input object
2016-05-02 17:52:41 +02:00
Lam Pham-Sy d819d71049 Plotter: plot graphs from data files
It supports simple plot or multiplot. Config parameters for Plotter
must be defined either via a .json file or an input object.
2016-05-02 17:38:31 +02:00
8 changed files with 593 additions and 4 deletions

117
README.md
View File

@ -9,6 +9,7 @@ multiple components making up the whole Project.
* [Shuffle](#shuffle) to shuffle an array.
* [Errors](#errors) load an object of errors instances.
- [errors/arsenalErrors.json](errors/arsenalErrors.json)
* [Plotter] (#plotter) plot graphs from data files
## Guidelines
@ -46,3 +47,119 @@ console.log(errors.AccessDenied);
// AccessDenied: true }
```
## Plotter
It supports to plot a simple graph or multiple graphs.
- Simple graph: only one graph is plotted
- Multiplot: multiple graphs are plotted
Before plotting graph, we need define config parameters for Plotter vian
a '.json' file or an object.
### Usage
``` js
import Plotter from 'arsenal';
```
#### Use '.json' file
This file should have the following structure:
```javascript
{
"dataFolder": "./tests/functional", // REQUIRED
"dataFile": "data.txt", // REQUIRED
"output": "simple_file", // REQUIRED
"method": "simple", // or "multiplot", optional, default "simple"
"width": 12, // optional, default 29.7cm
"height": 10, // optional, default 21cm
// for simple plot, xCol must be an positive number
"xCol": 1, // optional, default 1
// for simple plot, yCol must be an array. Each element must be either a
// number or an array of length 2. In the latter case, a line with
// yerrorbars will be plotted
"yCols": [2, 3, [4, 5], 6], // optional, default [2]
"font": "CMR14", // optional, default CMR14
"fontSize": 14, // optional, default 12
"title": "Simple plot", // optional, default 'title'
"xLabel": "x-axis label", // optional, default 'xLabel'
"yLabel": "y-axis label", // optional, default 'yLabel'
"lineTitle": ["Test1", "Test2", "Test 3", "Test 4"], // optional
"message": "Functional test for plotter, use config file", // optional
"grid": "xtics ytics", // optional, default 'xtics ytics'
"styleData": "linespoints", // optional, default 'linespoints'
"lineSize": 1, // optional, default 1
"pointSize": 1, // optional, default 1
"legendPos": "top right" // optional default "top right"
}
```
Then, plot graph by
```javascript
process.env.PLOTTER_CONFIG_FILE = 'path_to_the_json_config_file';
const plotter = new Plotter();
plotter.plotData(err => {
if (err) {
// error occurs
}
// plot's done
})
```
#### Use object
Define an object containing parameters for Plotter:
```javascript
const config = {
dataFolder: './tests/functional',
dataFile: 'data.txt',
output: 'multiplot_arg',
method: 'multiplot',
nbGraphsPerRow: 3,
nbGraphsPerCol: 2,
width: 10,
height: 5,
// for multiple plot, xCol must be an array of 1 element or
// nbGraphsPerRow * nbGraphsPerCol elements. In the first case, x-axis
// is the same for all graphs
xCol: [1, 1, 1, 1, 1, 1],
// for multiple plot, yCol must be an array of
// nbGraphsPerRow * nbGraphsPerCol elements. Each element must be either a
// number or an array. In the latter case, the array can contains either
// positive number or array of length 2 (same as simple plot)
yCols: [[2, 3], 3, [[4, 5]], [4, 6], [2, 5], [6]],
graphsOrder: 'rowsfirst',
font: 'CMR14',
fontSize: 14,
title: 'Multiplot',
xLabel: ['xLabel1', 'xLabel2', 'xLabel3', 'xLabel4', 'xLabel5',
'xLabel6'],
yLabel: ['yLabel1', 'yLabel2', 'yLabel3', 'yLabel4', 'yLabel5',
'yLabel6'],
lineTitle: [['Col2', 'Col3'], ['Col3'], ['Col4-5'],
['Col4', 'Col6'], ['Col2', 'Col5'], ['Col6']],
message: 'Functional test for plotter, config via argument',
grid: 'xtics ytics',
styleData: 'linespoints',
lineSize: 1,
pointSize: 1,
legendPos: 'top left',
}
```
Then, plot graph by
```javascript
const plotter = new Plotter(config);
plotter.plotData(err => {
if (err) {
// error occurs
}
// plot's done
})
```

View File

@ -3,4 +3,5 @@ module.exports = {
errors: require('./lib/errors.js'),
shuffle: require('./lib/shuffle'),
stringHash: require('./lib/stringHash'),
plotter: require('./lib/plotter'),
};

343
lib/plotter.js Normal file
View File

@ -0,0 +1,343 @@
'use strict'; // eslint-disable-line strict
const fs = require('fs');
const spawn = require('child_process').spawn;
const stderr = process.stderr;
const simple = 'simple';
const multiplot = 'multiplot';
function getConfig() {
let config;
let configFilePath;
if (process.env.PLOTTER_CONFIG_FILE !== undefined) {
configFilePath = process.env.PLOTTER_CONFIG_FILE;
} else {
configFilePath = './config.json';
}
try {
const data = fs.readFileSync(configFilePath, { encoding: 'utf-8' });
config = JSON.parse(data);
} catch (err) {
throw new Error(`could not parse config file: ${err.message}`);
}
return config;
}
class Plotter {
/**
* @param {Object} _config: configuration for plotter
* @return {this} Plotter
*/
constructor(_config) {
const config = _config || getConfig();
if (!config) {
throw new Error('missing config for Plotter\n');
}
if (!config.dataFile) {
throw new Error('missing data file in config\n');
}
if (!config.output) {
throw new Error('missing name for output files in config\n');
}
this.gnuExt = '.gnu';
this.outputExt = '.pdf';
this.method = config.method || simple;
// folder where data, gnu, output files locate
this.dataFolder = config.dataFolder;
this.dataFile = `${this.dataFolder}/${config.dataFile}`;
this.output = `${this.dataFolder}/${config.output}${this.outputExt}`;
this.gnuFile = `${this.dataFolder}/${config.output}${this.gnuExt}`;
// data column for x-axis
this.xCol = config.xCol || 1;
// data column for y-axis
this.yCols = config.yCols || [2];
// data can be given as an array instead of froma data file
this.data = config.data;
// graph config
this.width = config.width || 29.7;
this.height = config.height || 21;
this.font = config.font || 'CMR14';
this.fontSize = config.fontSize || 12;
this.title = config.title || 'title';
this.xLabel = config.xLabel || 'xLabel';
this.yLabel = config.yLabel || 'yLabel';
this.lineTitle = config.lineTitle;
this.message = config.message.replace(/\n#/g, '\\n').replace(/#/g, '');
this.grid = config.grid || 'xtics ytics';
this.styleData = config.styleData || 'linespoints';
this.lineSize = config.lineSize || 1;
this.pointSize = config.pointSize || 1;
this.legendPos = config.legendPos || 'top left';
/* for multiplot */
this.nbGraphsPerCol = config.nbGraphsPerCol || 1;
this.nbGraphsPerRow = config.nbGraphsPerRow || 1;
this.nbGraphs = this.nbGraphsPerCol * this.nbGraphsPerRow;
this.graphsOrder = config.graphsOrder || 'rowsfirst';
this.layout = `${this.nbGraphsPerCol},${this.nbGraphsPerRow}`;
// graph size
this.size = `${this.width * this.nbGraphsPerRow}cm,` +
`${this.height * this.nbGraphsPerCol}cm`;
this.checkConsistentParams(err => {
if (err) {
throw new Error(err);
}
});
}
checkConsistentParams(cb) {
if (!this.dataFolder) {
throw new Error('missing folder storing data, .gnu and ' +
'output files\n');
}
if (this.dataFile === undefined && !this.data) {
throw new Error('missing data for Plotter\n');
} else if (this.dataFile) {
try {
fs.statSync(this.dataFile);
} catch (e) {
throw new Error(e);
}
}
if (this.method === simple && isNaN(this.xCol) || this.xCol < 1) {
return cb('\'xCol\' must be a positive number\n');
}
if (!Array.isArray(this.yCols)) {
return cb('\'yCols\' must be an array of column indices\n');
}
if (!this.lineTitle) {
this.lineTitle = new Array(this.yCols.length);
} else if (this.lineTitle &&
this.lineTitle.length !== this.yCols.length) {
return cb('\'yCols\' and \'lineTitle\' must have a same length\n');
}
if (this.styleData !== 'lines' && this.styleData !== 'points' &&
this.styleData !== 'linespoints') {
return cb('\'styleData\' must be either \'lines\', \'points\'' +
'or \'linespoints\'\n');
}
if (this.method === multiplot) {
if (!Array.isArray(this.xCol) ||
this.xCol.length !== this.nbGraphs) {
return cb(`'xCol' must be an array of ${this.nbGraphs} nbs\n`);
}
if (!Array.isArray(this.yCols) ||
this.yCols.length !== this.nbGraphs) {
return cb('\'yCols\' must be an array of ' +
`${this.nbGraphs} elements\n`);
}
this.yCols.forEach((yCol, yColIdx) => {
if (!Array.isArray(yCol)) {
if (!isNaN(yCol)) {
this.yCols[yColIdx] = [yCol];
} else {
return cb(`Element ${yCol} of \'yCols\' must be ` +
'either a number of an array\n');
}
}
return undefined;
});
if (this.graphsOrder !== 'rowsfirst' &&
this.graphsOrder !== 'columnsfirst') {
return cb('\'graphsOrder\' must be either \'rowsfirst\'' +
'or \'columnsfirst\'\n');
}
if (!Array.isArray(this.xLabel) ||
this.xLabel.length !== this.nbGraphs) {
this.xLabel = (new Array(this.nbGraphs)).fill(this.xLabel);
}
if (!Array.isArray(this.yLabel) ||
this.yLabel.length !== this.nbGraphs) {
this.yLabel = (new Array(this.nbGraphs)).fill(this.yLabel);
}
}
return undefined;
}
genLegend() {
return `${new Date()}\\n${this.message}\\n`;
}
/**
* function creates .gnu command to plot data on columns of a data file
* @param {string} file: data file name
* @param {array} cols: [col1, col2, col3]
* @param {array} every: [firstLine, step, lastLine]
* @param {string} title: curve title
* @param {string} type: 'lines' or 'points' or 'linespoints'
* @param {number} color: curve color
* @param {boolean} nextFlag: fasle -> last line
* @param {number} lt: curve line type
* @param {number} lw: curve line weight
* @param {number} ps: curve point size
* @param {array} fit: [func, title] for fit
* @return {this} this
*/
plotLine(file, cols, every, title, type, color, nextFlag, lt, lw, ps, fit) {
const _type = type || 'linespoints';
const _lt = lt || 1;
const _lw = lw || 2;
const _ps = ps || 1;
let str;
let _title;
if (title) {
_title = `title '${title}'`;
} else {
_title = 'notitle';
}
let _every = '';
if (every) {
if (every.length === 2) {
_every = `every ${every[1]}::${every[0]} `;
} else if (every.length === 3) {
_every = `every ::${every[0]}::${every[2]} `;
}
}
str = `"${file}" ${_every} u ${cols[0]}:${cols[1]} ` +
`${_title} w ${_type} lc ${color} lt ${_lt} lw ${_lw}`;
if (type === 'points' || type === 'linespoints') {
str += ` pt ${color} ps ${_ps}`;
}
if (cols[2]) {
str += `, "${file}" ${_every} u ${cols[0]}:${cols[1]}:${cols[2]} ` +
`notitle w yerrorbars lc ${color} lt ${_lt} lw ${_lw} ` +
`pt ${color} ps ${_ps}`;
}
if (fit) {
str += `,\ ${fit[0]} title ${fit[1]}`;
}
if (nextFlag) {
str += ',\\';
}
str += `\n`;
return str;
}
/**
* Function creates .gnu files that plots a graph of one or multiple lines
* If yCol is an array of two elements, it will plot with errorbars where
* the first element is average, the second one is standard-deviation
* @param {function} cb: callback function
* @return {function} callback
*/
simple(cb) {
let color = 1;
let content =
`set terminal pdfcairo size ${this.size} enhanced color ` +
`font "${this.font}, ${this.fontSize}"\n` +
`set key ${this.legendPos} Left reverse box width 3 height 1.5\n` +
`set style data ${this.styleData}\n` +
`set xlabel '${this.xLabel}'\n` +
`set ylabel '${this.yLabel}'\n` +
`set grid ${this.grid}\n` +
`set title "${this.title}\\n${this.message}"\n` +
`set output '${this.output}'\n` +
'plot ';
this.yCols.forEach((yCol, yColIdx) => {
let xyAxis = [this.xCol, yCol];
if (Array.isArray(yCol) && yCol.length === 2) {
xyAxis = [this.xCol, yCol[0], yCol[1]];
}
content += this.plotLine(this.dataFile, xyAxis, null,
`${this.lineTitle[yColIdx]}`, this.styleData, color,
yColIdx < this.yCols.length - 1, null, this.lineSize,
this.pointSize);
color++;
});
content += `\n`;
fs.writeFile(this.gnuFile, content, cb);
}
/**
* function creates .gnu files that plots a graph of one or multiple lines
* with yerrorbars
* @param {function} cb: callback function
* @return {function} callback
*/
multiplot(cb) {
let color = 1;
let content =
`set terminal pdfcairo size ${this.size} enhanced color ` +
`font "${this.font}, ${this.fontSize}"\n` +
'set key top left Left reverse box width 3 height 1.5\n' +
`set style data ${this.styleData}\n` +
`set xlabel '${this.xLabel}'\n` +
`set ylabel '${this.yLabel}'\n` +
`set grid ${this.grid}\n` +
`set output '${this.output}'\n` +
`set multiplot layout ${this.layout} ${this.graphsOrder} ` +
`title "${this.title}\\n${this.message}"\n`;
this.yCols.forEach((yCol, yColIdx) => {
content +=
`unset xlabel; set xlabel '${this.xLabel[yColIdx]}'\n` +
`unset ylabel; set ylabel '${this.yLabel[yColIdx]}'\n` +
'plot ';
yCol.forEach((_yCol, _yColIdx) => {
let xyAxis = [this.xCol[yColIdx], _yCol];
if (Array.isArray(_yCol) && _yCol.length === 2) {
xyAxis = [this.xCol[yColIdx], _yCol[0], _yCol[1]];
}
content += this.plotLine(this.dataFile, xyAxis, null,
`${this.lineTitle[yColIdx][_yColIdx]}`, this.styleData,
color, _yColIdx < yCol.length - 1, null, this.lineSize,
this.pointSize);
color++;
});
content += `\n`;
});
content += `\n`;
content += `unset multiplot; set output\n`;
fs.writeFile(this.gnuFile, content, cb);
}
createAllGnuFiles(cb) {
let createGnuFile;
if (this.method === simple) {
createGnuFile = this.simple.bind(this);
}
if (this.method === multiplot) {
createGnuFile = this.multiplot.bind(this);
}
createGnuFile(cb);
}
plotData(cb) {
stderr.write('plotting..');
this.createAllGnuFiles(err => {
if (err) {
cb(err); return;
}
const cmd = `gnuplot ${this.gnuFile}`;
const gnuplot = spawn('bash', ['-c', cmd]);
gnuplot.on('exit', () => {
stderr.write(`done\n`);
return cb();
});
gnuplot.on('error', err => {
stderr.write(`gnuplot error: ${err}\n`);
return cb(err);
});
});
}
}
module.exports = Plotter;

View File

@ -5,7 +5,7 @@
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/scality/IronMan-Arsenal.git"
"url": "git+https://github.com/scality/Arsenal.git"
},
"contributors": [
{ "name": "David Pineau", "email": "" },
@ -14,15 +14,15 @@
],
"license": "ISC",
"bugs": {
"url": "https://github.com/scality/IronMan-Arsenal/issues"
"url": "https://github.com/scality/Arsenal/issues"
},
"homepage": "https://github.com/scality/IronMan-Arsenal#readme",
"homepage": "https://github.com/scality/Arsenal#readme",
"dependencies": {
},
"devDependencies": {
"eslint": "^2.4.0",
"eslint-config-airbnb": "^6.0.0",
"eslint-config-ironman": "scality/IronMan-Guidelines#rel/1.0",
"eslint-config-ironman": "scality/Guidelines#rel/1.1",
"level": "^1.3.0",
"mocha": "^2.3.3",
"temp": "^0.8.3"

View File

@ -0,0 +1,7 @@
# Note, a line starting with '#' will be ignored
# #Ok Test1 Test2 Test3 Test4
100 1.2 1.5 0.9 0.2 2.3
200 1.5 1.6 0.3 0.1 1.1
300 1.3 1.9 0.8 0.5 0.3
400 1.7 2.0 1.2 0.6 0.9
500 1.9 2.1 1.9 0.9 1.7

View File

@ -0,0 +1,24 @@
{
"method": "multiplot",
"nbGraphsPerRow": 2,
"nbGraphsPerCol": 3,
"dataFolder": "./tests/functional",
"dataFile": "data.txt",
"output": "multiplot_file",
"xCol": [1, 1, 1, 1, 1, 1],
"yCols": [[2, 3], [3], [[4, 5]], [4, 6], [2, 5], [6]],
"graphsOrder": "rowsfirst",
"font": "CMR14",
"fontSize": 14,
"title": "Multiple plot",
"xLabel": "Number of successes",
"yLabel": "Average of latency (ms)",
"lineTitle": [["Col2", "Col3"], ["Col3"], ["Col4-5"],
["Col4", "Col6"], ["Col2", "Col5"], ["Col6"]],
"message": "Functional test for plotter, use config file",
"grid": "xtics ytics",
"styleData": "linespoints",
"lineSize": 1,
"pointSize": 1,
"legendPos": "top right"
}

75
tests/functional/plot.js Normal file
View File

@ -0,0 +1,75 @@
'use strict'; // eslint-disable-line strict
const Plotter = require('../../lib/plotter');
describe('Plot functional test', () => {
it('simple plot with .json config file', done => {
process.env.PLOTTER_CONFIG_FILE = `${__dirname}/simple.json`;
const plotter = new Plotter();
plotter.plotData(done);
});
it('simple plot with config assigned via argument', done => {
const config = {
method: 'simple',
dataFile: 'data.txt',
output: 'simple_arg',
dataFolder: './tests/functional/test',
xCol: 1,
yCols: [2, 3, [4, 5], 6],
font: 'CMR14',
fontSize: 14,
title: 'Simple plot',
xLabel: 'x-axis label',
yLabel: 'y-axis label',
lineTitle: ['Test1', 'Test2', 'Test 3', 'Test 4'],
message: 'Functional test for plotter, config via argument',
grid: 'xtics ytics',
styleData: 'linespoints',
lineSize: 1,
pointSize: 1,
legendPos: 'top left',
};
const plotter = new Plotter(config);
plotter.plotData(done);
});
it('multiplot with .json config file', done => {
process.env.PLOTTER_CONFIG_FILE = `${__dirname}/multiplot.json`;
const plotter = new Plotter();
plotter.plotData(done);
});
it('multiplot with config assigned via argument', done => {
const config = {
method: 'multiplot',
nbGraphsPerRow: 3,
nbGraphsPerCol: 2,
width: 10,
height: 5,
dataFile: 'data.txt',
output: 'multiplot_arg',
dataFolder: './tests/functional',
xCol: [1, 1, 1, 1, 1, 1],
yCols: [[2, 3], 3, [[4, 5]], [4, 6], [2, 5], [6]],
graphsOrder: 'rowsfirst',
font: 'CMR14',
fontSize: 14,
title: 'Multiplot',
xLabel: ['xLabel1', 'xLabel2', 'xLabel3', 'xLabel4', 'xLabel5',
'xLabel6'],
yLabel: ['yLabel1', 'yLabel2', 'yLabel3', 'yLabel4', 'yLabel5',
'yLabel6'],
lineTitle: [['Col2', 'Col3'], ['Col3'], ['Col4-5'],
['Col4', 'Col6'], ['Col2', 'Col5'], ['Col6']],
message: 'Functional test for plotter, config via argument',
grid: 'xtics ytics',
styleData: 'linespoints',
lineSize: 1,
pointSize: 1,
legendPos: 'top left',
};
const plotter = new Plotter(config);
plotter.plotData(done);
});
});

View File

@ -0,0 +1,22 @@
{
"method": "simple",
"width": 12,
"height": 10,
"dataFolder": "./tests/functional",
"dataFile": "data.txt",
"output": "simple_file",
"xCol": 1,
"yCols": [2, 3, [4, 5], 6],
"font": "CMR14",
"fontSize": 14,
"title": "Simple plot",
"xLabel": "Number of successes",
"yLabel": "Average of latency (ms)",
"lineTitle": ["Test1", "Test2", "Test 3", "Test 4"],
"message": "Functional test for plotter, use config file",
"grid": "xtics ytics",
"styleData": "linespoints",
"lineSize": 1,
"pointSize": 1,
"legendPos": "top right"
}