Skip to content

Commit

Permalink
feat(data,Options): Implement data.onmin/onmax callback
Browse files Browse the repository at this point in the history
- Add min, max data extraction codes
- Refactored codes getting chart types

Fix #8
Close #125
  • Loading branch information
netil committed Aug 28, 2017
1 parent 6531692 commit 6a88a74
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 52 deletions.
52 changes: 52 additions & 0 deletions spec/data-spec.js
Expand Up @@ -1437,4 +1437,56 @@ describe("DATA", () => {
expect(newData).to.not.equal(data);
});
});

describe("data.onmin/onmax callbacks", () => {
let minData;
let maxData;

before(() => {
args = {
data: {
columns: [
["data1", 30, 10, 100, 400, 150, 250],
["data2", 10, 190, 95, 40, 15, 25]
],
onmin: d => {
minData = d;
},
onmax: d => {
maxData = d;
}
}
};
});

it("check for onmin callback", done => {
setTimeout(() => {
expect(minData.length > 0).to.be.true;

expect(minData[0].value).to.be.equal(10);
expect(minData[0].value).to.be.equal(minData[1].value);
expect(minData[0].id).to.not.be.equal(minData[1].id);

expect(minData[0].element.tagName).to.be.equal("circle");
expect(minData[1].element.tagName).to.be.equal("circle");
expect(minData[0].element.getAttribute("cy")).to.be.equal(minData[1].element.getAttribute("cy"));
expect(+minData[0].element.getAttribute("cy")).to.be.closeTo(390, 5);

done();
}, 100);
});

it("check for onmax callback", done => {
setTimeout(() => {
expect(maxData.length > 0).to.be.true;

expect(maxData[0].value).to.be.equal(400);
expect(maxData[0].id).to.be.equal("data1");

expect(maxData[0].element.tagName).to.be.equal("circle");
expect(+maxData[0].element.getAttribute("cy")).to.be.closeTo(36, 5);
done();
}, 100);
});
});
});
28 changes: 28 additions & 0 deletions src/config/Options.js
Expand Up @@ -738,6 +738,34 @@ export default class Options {
*/
data_onunselected: () => {},

/**
* Set a callback for minimum data
* @name data:onmin
* @memberof Options
* @type {Function}
* @default undefined
* @example
* onmin: function(data) {
* // data - ex) [{x: 3, value: 400, id: "data1", index: 3, element: circle}, ... ]
* ...
* }
*/
data_onmin: undefined,

/**
* Set a callback for maximum data
* @name data:onmax
* @memberof Options
* @type {Function}
* @default undefined
* @example
* onmax: function(data) {
* // data - ex) [{x: 3, value: 400, id: "data1", index: 3, element: circle}, ... ]
* ...
* }
*/
data_onmax: undefined,

/**
* Load a CSV or JSON file from a URL. NOTE that this will not work if loading via the "file://" protocol as the most browsers will block XMLHTTPRequests.
* @name data:url
Expand Down
32 changes: 15 additions & 17 deletions src/data/data.convert.js
Expand Up @@ -36,7 +36,6 @@ extend(ChartInternal.prototype, {
if (type === "json") {
d = this.convertJsonToData(JSON.parse(response), keys);
} else if (type === "tsv") {
// ref : https://github.com/d3/d3-dsv#dsv_parse
d = this.convertTsvToData(response);
} else {
d = this.convertCsvToData(response);
Expand All @@ -46,36 +45,35 @@ extend(ChartInternal.prototype, {
});
},

convertCsvToData(xsv) {
const rows = d3CsvParseRows(xsv);
_convertCsvTsvToData(parser, xsv) {
const rows = parser.rows(xsv);
let d;

if (rows.length === 1) {
d = [{}];

rows[0].forEach(id => {
d[0][id] = null;
});
} else {
d = d3CsvParse(xsv);
d = parser.parse(xsv);
}

return d;
},

convertTsvToData(xsv) {
const rows = d3TsvParseRows(xsv);
let d;

if (rows.length === 1) {
d = [{}];
rows[0].forEach(id => {
d[0][id] = null;
});
} else {
d = d3TsvParse(xsv);
}
convertCsvToData(xsv) {
return this._convertCsvTsvToData({
rows: d3CsvParseRows,
parse: d3CsvParse
}, xsv);
},

return d;
convertTsvToData(tsv) {
return this._convertCsvTsvToData({
rows: d3TsvParseRows,
parse: d3TsvParse
}, tsv);
},

convertJsonToData(json, keys) {
Expand Down
91 changes: 88 additions & 3 deletions src/data/data.js
Expand Up @@ -4,8 +4,10 @@
*/
import {
set as d3Set,
min as d3Min,
max as d3Max,
merge as d3Merge
merge as d3Merge,
select as d3Select
} from "d3";
import CLASS from "../config/classes";
import ChartInternal from "../internals/ChartInternal";
Expand Down Expand Up @@ -171,10 +173,93 @@ extend(ChartInternal.prototype, {
return typeof x !== "undefined" ? x : null;
},

getMaxDataCount() {
/**
* Get min/max value from the data
* @private
* @param {Array} data array data to be evaluated
* @return {{min: {Number}, max: {Number}}}
*/
getMinMaxValue(data) {
let min;
let max;

(data || this.data.targets.map(t => t.values))
.forEach(v => {
min = d3Min([min, d3Min(v, t => t.value)]);
max = d3Max([max, d3Max(v, t => t.value)]);
});

return {min, max};
},

/**
* Get the min/max data
* @private
* @return {{min: Array, max: Array}}
*/
getMinMaxData() {
const data = this.data.targets.map(t => t.values);
const minMax = this.getMinMaxValue(data);

let min = [];
let max = [];

data.forEach(v => {
const minData = this.getFilteredDataByValue(v, minMax.min);
const maxData = this.getFilteredDataByValue(v, minMax.max);

if (minData.length) {
min = min.concat(minData);
}

if (maxData.length) {
max = max.concat(maxData);
}
});

return {min, max};
},

/**
* Get filtered data by value
* @private
* @param {Object} data
* @param {Number} value
* @return {Array} filtered array data
*/
getFilteredDataByValue(data, value) {
const $$ = this;

return d3Max($$.data.targets, t => t.values.length);
return data.filter(t => {
if (t.value === value) {
// select element. It needs schedule by setTimeout to avoid collision with d3's transitions tween
setTimeout(() => (t.element = $$.getElementByDataIndex(t)), 0);

return true;
}

return false;
});
},

/**
* Get element by data index
* @private
* @param {Object} data
* @return {SVGElement|null}
*/
getElementByDataIndex(data) {
const isArcType = this.isTypeOf(data.id, ["pie", "gauge", "donut"]);

// ex. selector for Arc types: .bb-target-data1 .bb-shape-1 / others: .bb-target-data1 .bb-arc-data1
const selector = `.${CLASS.target}-${data.id} .${isArcType ? CLASS.arc : CLASS.shape}-${isArcType ? data.id : data.index}`;
const element = d3Select(selector);

return element.empty() ? null : element.node();
},

getMaxDataCount() {
return d3Max(this.data.targets, t => t.values.length);
},

getMaxDataCountTarget(targets) {
Expand Down
12 changes: 11 additions & 1 deletion src/internals/ChartInternal.js
Expand Up @@ -14,7 +14,7 @@ import {
} from "d3";
import Axis from "../axis/Axis";
import CLASS from "../config/classes";
import {addEvent, notEmpty, asHalfPixel, isValue, getOption} from "./util";
import {addEvent, notEmpty, asHalfPixel, isValue, getOption, isFunction} from "./util";

/**
* Internal chart class.
Expand Down Expand Up @@ -322,6 +322,8 @@ export default class ChartInternal {
// Draw with targets
if (binding) {
$$.updateDimension();

// oninit callback
$$.config.oninit.call($$);

$$.redraw({
Expand All @@ -331,6 +333,14 @@ export default class ChartInternal {
withUpdateOrgXDomain: true,
withTransitionForAxis: false
});

// data.onmin/max callback
if ($$.config.data_onmin || $$.config.data_onmax) {
const minMax = $$.getMinMaxData();

isFunction($$.config.data_onmin) && $$.config.data_onmin.call($$, minMax.min);
isFunction($$.config.data_onmax) && $$.config.data_onmax.call($$, minMax.max);
}
}

// Bind resize event
Expand Down
49 changes: 18 additions & 31 deletions src/internals/type.js
Expand Up @@ -3,7 +3,7 @@
* billboard.js project is licensed under the MIT license
*/
import ChartInternal from "./ChartInternal";
import {isString, extend} from "./util";
import {isString, isArray, extend} from "./util";

extend(ChartInternal.prototype, {
setTargetType(targetIds, type) {
Expand Down Expand Up @@ -54,63 +54,50 @@ extend(ChartInternal.prototype, {
},

isLineType(d) {
const config = this.config;
const id = isString(d) ? d : d.id;

return !config.data_types[id] ||
["line", "spline", "area", "area-spline", "step", "area-step"]
.indexOf(config.data_types[id]) >= 0;
return !this.config.data_types[id] ||
this.isTypeOf(id, ["line", "spline", "area", "area-spline", "step", "area-step"]);
},

isStepType(d) {
isTypeOf(d, type) {
const id = isString(d) ? d : d.id;
const dataType = this.config.data_types[id];

return ["step", "area-step"]
.indexOf(this.config.data_types[id]) >= 0;
return isArray(type) ?
type.indexOf(dataType) >= 0 : dataType === type;
},

isSplineType(d) {
const id = isString(d) ? d : d.id;
isStepType(d) {
return this.isTypeOf(d, ["step", "area-step"]);
},

return ["spline", "area-spline"]
.indexOf(this.config.data_types[id]) >= 0;
isSplineType(d) {
return this.isTypeOf(d, ["spline", "area-spline"]);
},

isAreaType(d) {
const id = isString(d) ? d : d.id;

return ["area", "area-spline", "area-step"]
.indexOf(this.config.data_types[id]) >= 0;
return this.isTypeOf(d, ["area", "area-spline", "area-step"]);
},

isBarType(d) {
const id = isString(d) ? d : d.id;

return this.config.data_types[id] === "bar";
return this.isTypeOf(d, "bar");
},

isScatterType(d) {
const id = isString(d) ? d : d.id;

return this.config.data_types[id] === "scatter";
return this.isTypeOf(d, "scatter");
},

isPieType(d) {
const id = isString(d) ? d : d.id;

return this.config.data_types[id] === "pie";
return this.isTypeOf(d, "pie");
},

isGaugeType(d) {
const id = isString(d) ? d : d.id;

return this.config.data_types[id] === "gauge";
return this.isTypeOf(d, "gauge");
},

isDonutType(d) {
const id = isString(d) ? d : d.id;

return this.config.data_types[id] === "donut";
return this.isTypeOf(d, "donut");
},

isArcType(d) {
Expand Down

0 comments on commit 6a88a74

Please sign in to comment.