Skip to content

Commit

Permalink
feat(shape): Intent to ship stack.normalize
Browse files Browse the repository at this point in the history
- Merged all ratio functions to be done in data
- Add internal .isStackNormalized()
- Implementation for normalize option

Fix #623
Close #643
  • Loading branch information
netil committed Nov 8, 2018
1 parent e9f1417 commit 3af15a5
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 68 deletions.
25 changes: 25 additions & 0 deletions demo/demo.js
Expand Up @@ -1480,6 +1480,31 @@ var demos = {
},
description: "For selection, click data point or drag over data points"
},
DataStackNormalized: {
options: {
data: {
x: "x",
columns: [
["x", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7"],
["data1", 30, 280, 951, 400, 150, 546, 4528],
["data2", 130, 357, 751, 400, 150, 250, 3957],
["data3", 30, 280, 320, 218, 150, 150, 5000]
],
type: "bar",
groups: [
["data1", "data2", "data3"]
],
stack: {
normalize: true
}
},
axis: {
x: {
type: "category"
}
}
}
},
OnMinMaxCallback: {
options: {
data: {
Expand Down
71 changes: 71 additions & 0 deletions spec/internals/data-spec.js
Expand Up @@ -1465,4 +1465,75 @@ describe("DATA", () => {
expect(path.split("L").length).to.be.equal(expected.L);
});
});

describe("data.stack", () => {
let chartHeight = 0;

before(() => {
args = {
data: {
columns: [
["data1", 230, 50, 300],
["data2", 198, 87, 580]
],
type: "bar",
groups: [
["data1", "data2"]
],
stack: {
normalize: true
}
}
};
});

it("check for the normalized y axis tick in percentage", () => {
const tick = chart.$.main.selectAll(`.${CLASS.axisY} .tick tspan`);

// check for the y axis to be in percentage
tick.each(function (v, i) {
expect(this.textContent).to.be.equal(`${i * 10}%`);
});
});

it("check for the normalized bar's height", () => {
chartHeight = +chart.$.main.selectAll(`.${CLASS.zoomRect}`).attr("height") - 1;
const bars = chart.$.bar.bars.nodes().concat();

bars.splice(0, 3).forEach((v, i) => {
expect(v.getBBox().height + bars[i].getBBox().height).to.be.equal(chartHeight);
});
});

it("set options data.type='area'", () => {
args.data.type = "area";
args.data.columns = [
["data1", 200, 387, 123],
["data2", 200, 387, 123]
];
});

it("check for the normalized area's height", () => {
let areaHeight = 0;

chart.$.main.selectAll(`.${CLASS.areas} path`).each(function() {
areaHeight += this.getBBox().height;
});

expect(areaHeight).to.be.equal(chartHeight);
});

it("check for the normalized default tooltip", () => {
let tooltipValue = 0;

// show tooltip
chart.tooltip.show({index:1});

chart.$.tooltip.selectAll(".value").each(function() {
tooltipValue += parseInt(this.textContent);
});

expect(tooltipValue).to.be.equal(100);
});
});
});
1 change: 1 addition & 0 deletions src/api/api.group.js
Expand Up @@ -12,6 +12,7 @@ extend(Chart.prototype, {
* @instance
* @memberOf Chart
* @param {Array} groups This argument needs to be an Array that includes one or more Array that includes target ids to be grouped.
* @return {Array} Grouped data names array
* @example
* // data1 and data2 will be a new group.
* chart.groups([
Expand Down
4 changes: 3 additions & 1 deletion src/axis/Axis.js
Expand Up @@ -109,7 +109,9 @@ export default class Axis {
const axis = bbAxis(axisParams)
.scale(scale)
.orient(orient)
.tickFormat(tickFormat);
.tickFormat(
tickFormat || ($$.isStackNormalized() && (x => `${x}%`))
);

$$.isTimeSeriesY() ?
// https://github.com/d3/d3/blob/master/CHANGES.md#time-intervals-d3-time
Expand Down
40 changes: 39 additions & 1 deletion src/config/Options.js
Expand Up @@ -671,10 +671,48 @@ export default class Options {
* }
*/
data_hide: false,

/**
* Filter values to be shown
* The data value is the same as the returned by `.data()`.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
* @name data鈥ilter
* @memberOf Options
* @type {Function}
* @default undefined
* @example
* data: {
* // filter for id value
* filter: function(v) {
* // v: [{id: "data1", id_org: "data1", values: [
* // {x: 0, value: 130, id: "data2", index: 0}, ...]
* // }, ...]
* return v.id !== "data1";
* }
*/
data_filter: undefined,

/**
* Set data selection enabled.<br><br>
* Set the stacking to be normalized
* - **NOTE:**
* - For stacking, '[data.groups](#.data%25E2%2580%25A4groups)' option should be set
* - y Axis will be set in percentage value (0 ~ 100%)
* - Must have postive values
* @name data鈥tack鈥ormalize
* @memberOf Options
* @type {Boolean}
* @default false
* @see {@link https://naver.github.io/billboard.js/demo/#Data.DataStackNormalized|Example}
* @example
* data: {
* stack: {
* normalize: true
* }
* }
*/
data_stack_normalize: false,
/**
* Set data selection enabled<br><br>
* If this option is set true, we can select the data points and get/set its state of selection by API (e.g. select, unselect, selected).
* @name data鈥election鈥nabled
* @memberOf Options
Expand Down
78 changes: 78 additions & 0 deletions src/data/data.js
Expand Up @@ -40,6 +40,12 @@ extend(ChartInternal.prototype, {
return !this.isX(key);
},

isStackNormalized() {
const config = this.config;

return config.data_stack_normalize && config.data_groups.length;
},

getXKey(id) {
const $$ = this;
const config = $$.config;
Expand Down Expand Up @@ -276,6 +282,33 @@ extend(ChartInternal.prototype, {
return minMaxData;
},

/**
* Get sum of data per index
* @private
* @return {Array}
*/
getTotalPerIndex() {
const $$ = this;
const cacheKey = "$totalPerIndex";
let sum = $$.getCache(cacheKey);

if ($$.isStackNormalized() && !sum) {
sum = [];

$$.data.targets.forEach(row => {
row.values.forEach((v, i) => {
if (!sum[i]) {
sum[i] = 0;
}

sum[i] += v.value;
});
});
}

return sum;
},

/**
* Get total data sum
* @private
Expand Down Expand Up @@ -714,5 +747,50 @@ extend(ChartInternal.prototype, {
}

return value[type];
},

/**
* Get ratio value
* @param {String} type Ratio for given type
* @param {Object} d Data value object
* @param {Boolean} asPercent Convert the return as percent or not
* @return {Number} Ratio value
* @private
*/
getRatio(type, d, asPercent) {
const $$ = this;
const config = $$.config;
let ratio = d && (d.ratio || d.value);

if (type === "arc") {
// if has padAngle set, calculate rate based on value
if ($$.pie.padAngle()()) {
let total = $$.getTotalDataSum();

if ($$.hiddenTargetIds.length) {
total -= d3Sum($$.api.data.values.call($$.api, $$.hiddenTargetIds));
}

ratio = d.value / total;

// otherwise, based on the rendered angle value
} else {
ratio = (d.endAngle - d.startAngle) / (
Math.PI * ($$.hasType("gauge") && !config.gauge_fullCircle ? 1 : 2)
);
}
} else if (type === "index" && !d.ratio) {
const totalPerIndex = this.getTotalPerIndex();

if (totalPerIndex && d.value) {
d.ratio = d.value / totalPerIndex[d.index];
}

ratio = d.ratio;
} else if (type === "radar") {
ratio = (parseFloat(Math.max(d.value, 0)) / $$.maxValue) * config.radar_size_ratio;
}

return asPercent ? ratio * 100 : ratio;
}
});
5 changes: 5 additions & 0 deletions src/internals/domain.js
Expand Up @@ -80,6 +80,11 @@ extend(ChartInternal.prototype, {
getYDomain(targets, axisId, xDomain) {
const $$ = this;
const config = $$.config;

if ($$.isStackNormalized()) {
return [0, 100];
}

const targetsByAxisId = targets.filter(t => $$.axis.getId(t.id) === axisId);
const yTargets = xDomain ? $$.filterByXDomain(targetsByAxisId, xDomain) : targetsByAxisId;
const yMin = axisId === "y2" ? config.axis_y2_min : config.axis_y_min;
Expand Down
8 changes: 8 additions & 0 deletions src/internals/scale.js
Expand Up @@ -35,6 +35,14 @@ extend(ChartInternal.prototype, {
);
},

/**
* Get y Axis scale function
* @param {Number} min
* @param {Number} max
* @param {Number} domain
* @return {Function} scale
* @private
*/
getY(min, max, domain) {
const scale = this.getScale(min, max, this.isTimeSeriesY());

Expand Down
3 changes: 1 addition & 2 deletions src/internals/tooltip.js
Expand Up @@ -66,9 +66,8 @@ extend(ChartInternal.prototype, {
const config = $$.config;
const titleFormat = config.tooltip_format_title || defaultTitleFormat;
const nameFormat = config.tooltip_format_name || (name => name);
const valueFormat = config.tooltip_format_value || defaultValueFormat;
const valueFormat = config.tooltip_format_value || ($$.isStackNormalized() ? ((v, ratio) => `${(ratio * 100).toFixed(2)}%`) : defaultValueFormat);
const order = config.tooltip_order;

const getRowValue = row => $$.getBaseValue(row);
const getBgColor = $$.levelColor ? row => $$.levelColor(row.value) : row => color(row.id);

Expand Down
32 changes: 2 additions & 30 deletions src/shape/arc.js
Expand Up @@ -10,7 +10,6 @@ import {
arc as d3Arc,
pie as d3Pie
} from "d3-shape";
import {sum as d3Sum} from "d3-array";
import {interpolate as d3Interpolate} from "d3-interpolate";
import ChartInternal from "../internals/ChartInternal";
import CLASS from "../config/classes";
Expand Down Expand Up @@ -175,38 +174,11 @@ extend(ChartInternal.prototype, {
return translate;
},

getArcRatio(d) {
const $$ = this;
const config = $$.config;
let val = null;

if (d) {
// if has padAngle set, calculate rate based on value
if ($$.pie.padAngle()()) {
let total = $$.getTotalDataSum();

if ($$.hiddenTargetIds.length) {
total -= d3Sum($$.api.data.values.call($$.api, $$.hiddenTargetIds));
}

val = d.value / total;

// otherwise, based on the rendered angle value
} else {
val = (d.endAngle - d.startAngle) / (
Math.PI * ($$.hasType("gauge") && !config.gauge_fullCircle ? 1 : 2)
);
}
}

return val;
},

convertToArcData(d) {
return this.addName({
id: d.data.id,
value: d.value,
ratio: this.getArcRatio(d),
ratio: this.getRatio("arc", d),
index: d.index,
});
},
Expand All @@ -221,7 +193,7 @@ extend(ChartInternal.prototype, {

const updated = $$.updateAngle(d);
const value = updated ? updated.value : null;
const ratio = $$.getArcRatio(updated);
const ratio = $$.getRatio("arc", updated);
const id = d.data.id;

if (!$$.hasType("gauge") && !$$.meetsArcLabelThreshold(ratio)) {
Expand Down
6 changes: 4 additions & 2 deletions src/shape/bar.js
Expand Up @@ -173,11 +173,13 @@ extend(ChartInternal.prototype, {
posY = y0;
}

posY -= (y0 - offset);

// 4 points that make a bar
return [
[posX, offset],
[posX, posY - (y0 - offset)],
[posX + barW, posY - (y0 - offset)],
[posX, posY],
[posX + barW, posY],
[posX + barW, offset]
];
};
Expand Down

0 comments on commit 3af15a5

Please sign in to comment.