Skip to content

Commit

Permalink
yaxis tick formatters (#9065)
Browse files Browse the repository at this point in the history
* yaxis tick formatters

* fixing custom format

* adding information to currency help text

* fixing based on @rashidkpc review

* updating based on rashids comments

* adding some tests

* fixing broken yaxis

* fixing broken currency mode

* adding tick formatters tests

* throw error if currency is not three letter code

* adding server side tests

* fixing bytes mode

* rebasing on master and fixing linting

* fixing custom formatter behaviour.
  • Loading branch information
ppisljar authored Feb 10, 2017
1 parent 19420f6 commit 2fce8ea
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 3 deletions.
46 changes: 46 additions & 0 deletions src/core_plugins/timelion/public/__tests__/_tick_generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
describe('Tick Generator', function () {

let generateTicks;
const axes = [
{
min: 0,
max: 5000,
delta: 100
},
{
min: 0,
max: 50000,
delta: 2000
},
{
min: 4096,
max: 6000,
delta: 250
}
];
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
generateTicks = Private(require('plugins/timelion/panels/timechart/tick_generator'));
}));

it('returns a function', function () {
expect(generateTicks).to.be.a('function');
});

axes.forEach(axis => {
it(`generates ticks from ${axis.min} to ${axis.max}`, function () {
const ticks = generateTicks(axis);
let n = 1;
while (Math.pow(2, n) < axis.delta) n++;
const expectedDelta = Math.pow(2, n);
const expectedNr = parseInt((axis.max - axis.min) / expectedDelta) + 2;
expect(ticks instanceof Array).to.be(true);
expect(ticks.length).to.be(expectedNr);
expect(ticks[0]).to.equal(axis.min);
expect(ticks[parseInt(ticks.length / 2)]).to.equal(axis.min + expectedDelta * parseInt(ticks.length / 2));
expect(ticks[ticks.length - 1]).to.equal(axis.min + expectedDelta * (ticks.length - 1));
});
});
});
3 changes: 3 additions & 0 deletions src/core_plugins/timelion/public/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './_tick_generator.js';
describe('Timelion', function () {
});
154 changes: 154 additions & 0 deletions src/core_plugins/timelion/public/__tests__/services/tick_formatters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
describe('Tick Formatters', function () {

let tickFormatters;

beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
tickFormatters = Private(require('plugins/timelion/services/tick_formatters'));
}));

describe('Bits mode', function () {
let bitFormatter;
beforeEach(function () {
bitFormatter = tickFormatters.bits;
});

it('is a function', function () {
expect(bitFormatter).to.be.a('function');
});

it('formats with b/kb/mb/gb', function () {
expect(bitFormatter(7)).to.equal('7b');
expect(bitFormatter(4 * 1000)).to.equal('4kb');
expect(bitFormatter(4.1 * 1000 * 1000)).to.equal('4.1mb');
expect(bitFormatter(3 * 1000 * 1000 * 1000)).to.equal('3gb');
});
});

describe('Bits/s mode', function () {
let bitsFormatter;
beforeEach(function () {
bitsFormatter = tickFormatters['bits/s'];
});

it('is a function', function () {
expect(bitsFormatter).to.be.a('function');
});

it('formats with b/kb/mb/gb', function () {
expect(bitsFormatter(7)).to.equal('7b/s');
expect(bitsFormatter(4 * 1000)).to.equal('4kb/s');
expect(bitsFormatter(4.1 * 1000 * 1000)).to.equal('4.1mb/s');
expect(bitsFormatter(3 * 1000 * 1000 * 1000)).to.equal('3gb/s');
});
});

describe('Bytes mode', function () {
let byteFormatter;
beforeEach(function () {
byteFormatter = tickFormatters.bytes;
});

it('is a function', function () {
expect(byteFormatter).to.be.a('function');
});

it('formats with B/KB/MB/GB', function () {
expect(byteFormatter(10)).to.equal('10B');
expect(byteFormatter(10 * 1024)).to.equal('10KB');
expect(byteFormatter(10.2 * 1024 * 1024)).to.equal('10.2MB');
expect(byteFormatter(3 * 1024 * 1024 * 1024)).to.equal('3GB');
});
});

describe('Bytes/s mode', function () {
let bytesFormatter;
beforeEach(function () {
bytesFormatter = tickFormatters['bytes/s'];
});

it('is a function', function () {
expect(bytesFormatter).to.be.a('function');
});

it('formats with B/KB/MB/GB', function () {
expect(bytesFormatter(10)).to.equal('10B/s');
expect(bytesFormatter(10 * 1024)).to.equal('10KB/s');
expect(bytesFormatter(10.2 * 1024 * 1024)).to.equal('10.2MB/s');
expect(bytesFormatter(3 * 1024 * 1024 * 1024)).to.equal('3GB/s');
});
});

describe('Currency mode', function () {
let currencyFormatter;
beforeEach(function () {
currencyFormatter = tickFormatters.currency;
});

it('is a function', function () {
expect(currencyFormatter).to.be.a('function');
});

it('formats with $ by defalt', function () {
const axis = {
options: {
units: {}
}
};
expect(currencyFormatter(10.2, axis)).to.equal('$10.20');
});

it('accepts currency in ISO 4217', function () {
const axis = {
options: {
units: {
prefix: 'CNY'
}
}
};

expect(currencyFormatter(10.2, axis)).to.equal('CN¥10.20');
});
});

describe('Custom mode', function () {
let customFormatter;
beforeEach(function () {
customFormatter = tickFormatters.custom;
});

it('is a function', function () {
expect(customFormatter).to.be.a('function');
});

it('accepts prefix and suffix', function () {
const axis = {
options: {
units: {
prefix: 'prefix',
suffix: 'suffix'
}
},
tickDecimals: 1
};

expect(customFormatter(10.2, axis)).to.equal('prefix10.2suffix');
});

it('correctly renders small values', function () {
const axis = {
options: {
units: {
prefix: 'prefix',
suffix: 'suffix'
}
},
tickDecimals: 3
};

expect(customFormatter(0.00499999999999999, axis)).to.equal('prefix0.005suffix');
});
});
});
20 changes: 19 additions & 1 deletion src/core_plugins/timelion/public/panels/timechart/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ module.exports = function timechartFn(Private, config, $rootScope, timefilter, $
render: function ($scope, $elem) {
const template = '<div class="chart-top-title"></div><div class="chart-canvas"></div>';
const timezone = Private(require('plugins/timelion/services/timezone'))();
const tickFormatters = require('plugins/timelion/services/tick_formatters')();
const getxAxisFormatter = Private(require('plugins/timelion/panels/timechart/xaxis_formatter'));
const generateTicks = Private(require('plugins/timelion/panels/timechart/tick_generator'));

// TODO: I wonder if we should supply our own moment that sets this every time?
// could just use angular's injection to provide a moment service?
Expand Down Expand Up @@ -147,7 +149,11 @@ module.exports = function timechartFn(Private, config, $rootScope, timefilter, $
}

if (y != null) {
legendValueNumbers.eq(i).text('(' + y.toFixed(precision) + ')');
let label = y.toFixed(precision);
if (series.yaxis.tickFormatter) {
label = series.yaxis.tickFormatter(label, series.yaxis);
}
legendValueNumbers.eq(i).text(`(${label})`);
} else {
legendValueNumbers.eq(i).empty();
}
Expand Down Expand Up @@ -224,6 +230,18 @@ module.exports = function timechartFn(Private, config, $rootScope, timefilter, $
return series;
});

if (options.yaxes) {
options.yaxes.forEach(yaxis => {
if (yaxis && yaxis.units) {
yaxis.tickFormatter = tickFormatters[yaxis.units.type];
const byteModes = ['bytes', 'bytes/s'];
if (byteModes.includes(yaxis.units.type)) {
yaxis.tickGenerator = generateTicks;
}
}
});
}

try {
$scope.plot = $.plot(canvasElem, _.compact(series), options);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module.exports = function generateTicksProvider() {

function floorInBase(n, base) {
return base * Math.floor(n / base);
}

function generateTicks(axis) {
const returnTicks = [];
let tickSize = 2;
let delta = axis.delta;
let steps = 0;
let tickVal;
let tickCount = 0;

//Count the steps
while (Math.abs(delta) >= 1024) {
steps++;
delta /= 1024;
}

//Set the tick size relative to the remaining delta
while (tickSize <= 1024) {
if (delta <= tickSize) {
break;
}
tickSize *= 2;
}
axis.tickSize = tickSize * Math.pow(1024, steps);

//Calculate the new ticks
const tickMin = floorInBase(axis.min, axis.tickSize);
do {
tickVal = tickMin + (tickCount++) * axis.tickSize;
returnTicks.push(tickVal);
} while (tickVal < axis.max);

return returnTicks;
}

return function (axis) {
return generateTicks(axis);
};
};
72 changes: 72 additions & 0 deletions src/core_plugins/timelion/public/services/tick_formatters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
define(function (require) {

function baseTickFormatter(value, axis) {
const factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
const formatted = '' + Math.round(value * factor) / factor;

// If tickDecimals was specified, ensure that we have exactly that
// much precision; otherwise default to the value's own precision.

if (axis.tickDecimals != null) {
const decimal = formatted.indexOf('.');
const precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
if (precision < axis.tickDecimals) {
return (precision ? formatted : formatted + '.') + ('' + factor).substr(1, axis.tickDecimals - precision);
}
}

return formatted;
}

return function ticketFormatters() {
const formatters = {
'bits': function (val, axis) {
const labels = ['b','kb','mb','gb','tb','pb'];
let index = 0;
while (val >= 1000 && index < labels.length) {
val /= 1000;
index++;
}
return (Math.round(val * 100) / 100) + labels[index];
},
'bits/s': function (val, axis) {
const labels = ['b/s','kb/s','mb/s','gb/s','tb/s','pb/s'];
let index = 0;
while (val >= 1000 && index < labels.length) {
val /= 1000;
index++;
}
return (Math.round(val * 100) / 100) + labels[index];
},
'bytes': function (val, axis) {
const labels = ['B','KB','MB','GB','TB','PB'];
let index = 0;
while (val >= 1024 && index < labels.length) {
val /= 1024;
index++;
}
return (Math.round(val * 100) / 100) + labels[index];
},
'bytes/s': function (val, axis) {
const labels = ['B/s','KB/s','MB/s','GB/s','TB/s','PB/s'];
let index = 0;
while (val >= 1024 && index < labels.length) {
val /= 1024;
index++;
}
return (Math.round(val * 100) / 100) + labels[index];
},
'currency': function (val, axis) {
return val.toLocaleString('en', { style: 'currency', currency: axis.options.units.prefix || 'USD' });
},
'custom': function (val, axis) {
const formattedVal = baseTickFormatter(val, axis);
const prefix = axis.options.units.prefix;
const suffix = axis.options.units.suffix;
return prefix + formattedVal + suffix;
}
};

return formatters;
};
});
Loading

0 comments on commit 2fce8ea

Please sign in to comment.