Skip to content

Commit

Permalink
feat(options): Intent to ship bar.overlap
Browse files Browse the repository at this point in the history
Implement overlapped bar option

Close #2839
  • Loading branch information
netil authored Sep 19, 2022
1 parent ab4d98d commit 046aedb
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 13 deletions.
44 changes: 44 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -3924,6 +3924,50 @@ d3.select(".chart_area")
}
},
],
BarOverlap: [
{
options: {
data: {
columns: [
["data1", 80, 150, 100, 100, 100],
["data2", 100, 120, 130, 50, 150],
["data3", 150, 80, 120, 30, 80]
],
type: "bar"
},
bar: {
width: {
data1: {
ratio: 1.2
},
data2: 40,
data3: 20
},
overlap: true
}
}
},
{
options: {
data: {
columns: [
["data1", 130, 150, 130, 100, 130],
["data2", 70, 120, 125, 50, 125],
["data3", 50, 80, 110, 30, 20]
],
type: "bar"
},
bar: {
width: {
data1: 20,
data2: 40,
data3: 60
},
overlap: true
}
}
}
],
BarPadding: {
options: {
data: {
Expand Down
3 changes: 2 additions & 1 deletion src/ChartInternal/shape/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {DataRow} from "../../../types/types";
import {$BAR, $COMMON} from "../../config/classes";
import {getRandom, isNumber} from "../../module/util";
import type {IDataRow} from "../data/IData";
import type {IOffset} from "./shape";

type BarTypeDataRow = DataRow<number | number[]>;

Expand Down Expand Up @@ -234,7 +235,7 @@ export default {
const {config} = $$;
const axis = isSub ? $$.axis.subX : $$.axis.x;
const barTargetsNum = $$.getIndicesMax(barIndices) + 1;
const barW = $$.getBarW("bar", axis, barTargetsNum);
const barW: IOffset = $$.getBarW("bar", axis, barTargetsNum);
const barX = $$.getShapeX(barW, barIndices, !!isSub);
const barY = $$.getShapeY(!!isSub);
const barOffset = $$.getShapeOffset($$.isBarType, barIndices, !!isSub);
Expand Down
7 changes: 4 additions & 3 deletions src/ChartInternal/shape/candlestick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import {select as d3Select} from "d3-selection";
import {$CANDLESTICK, $COMMON} from "../../config/classes";
import {getRandom, isArray, isNumber, isObject} from "../../module/util";
import type {IOffset} from "./shape";

type CandlestickData = {
interface ICandlestickData {
open: number;
high: number;
low: number;
Expand Down Expand Up @@ -160,7 +161,7 @@ export default {

const axis = isSub ? $$.axis.subX : $$.axis.x;
const targetsNum = $$.getIndicesMax(indices) + 1;
const barW = $$.getBarW("candlestick", axis, targetsNum);
const barW: IOffset = $$.getBarW("candlestick", axis, targetsNum);
const x = $$.getShapeX(barW, indices, !!isSub);
const y = $$.getShapeY(!!isSub);
const shapeOffset = $$.getShapeOffset($$.isBarType, indices, !!isSub);
Expand Down Expand Up @@ -244,7 +245,7 @@ export default {
* @returns {object|null} Converted data object
* @private
*/
getCandlestickData({value}): CandlestickData | null {
getCandlestickData({value}): ICandlestickData | null {
let d;

if (isArray(value)) {
Expand Down
31 changes: 22 additions & 9 deletions src/ChartInternal/shape/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,19 @@ import CLASS from "../../config/classes";
import {capitalize, getPointer, getRectSegList, getUnique, isObjectType, isNumber, isValue, isUndefined, notEmpty} from "../../module/util";
import type {IDataRow, IDataIndice, TIndices} from "../data/IData";

export interface IOffset {
_$width: number;
_$total: number[]
}

export default {
/**
* Get the shape draw function
* @returns {object}
* @private
*/
getDrawShape() {
type SHAPE = {
type TShape = {
area?: any;
bar?: any;
line?: any;
Expand All @@ -44,7 +49,7 @@ export default {
const $$ = this;
const isRotated = $$.config.axis_rotated;
const {hasRadar} = $$.state;
const shape = {type: <SHAPE> {}, indices: <SHAPE> {}, pos: {}};
const shape = {type: <TShape>{}, indices: <TShape>{}, pos: {}};

["bar", "candlestick", "line", "area"].forEach(v => {
const name = capitalize(/^(bubble|scatter)$/.test(v) ? "line" : v);
Expand Down Expand Up @@ -176,12 +181,15 @@ export default {
.reduce((acc, curr) => acc + curr) : (indices as IDataIndice).__max__;
},

getShapeX(offset, indices, isSub?: boolean): (d) => number {
getShapeX(offset: IOffset, indices, isSub?: boolean): (d) => number {
const $$ = this;
const {config, scale} = $$;
const currScale = isSub ? scale.subX : (scale.zoom || scale.x);
const barOverlap = config.bar_overlap;
const barPadding = config.bar_padding;
const sum = (p, c) => p + c;

// total shapes half width
const halfWidth = isObjectType(offset) && (
offset._$total.length ? offset._$total.reduce(sum) / 2 : 0
);
Expand All @@ -196,15 +204,20 @@ export default {
const xPos = currScale(d.x, true);

if (halfWidth) {
x = xPos - (offset[d.id] || offset._$width) +
offset._$total.slice(0, index + 1).reduce(sum) -
halfWidth;
const offsetWidth = offset[d.id] || offset._$width;

x = barOverlap ?
xPos - offsetWidth / 2 :
xPos - offsetWidth + offset._$total.slice(0, index + 1).reduce(sum) - halfWidth;
} else {
x = xPos - (isNumber(offset) ? offset : offset._$width) * (targetsNum / 2 - index);
x = xPos - (isNumber(offset) ? offset : offset._$width) *
(targetsNum / 2 - (
barOverlap ? 1 : index
));
}
}

// adjust x position for bar.padding optionq
// adjust x position for bar.padding option
if (offset && x && targetsNum > 1 && barPadding) {
if (index) {
x += barPadding * index;
Expand Down Expand Up @@ -353,7 +366,7 @@ export default {
};
},

getBarW(type, axis, targetsNum: number): number {
getBarW(type, axis, targetsNum: number): number | IOffset {
const $$ = this;
const {config, org, scale} = $$;
const maxDataCount = $$.getMaxDataCount();
Expand Down
6 changes: 6 additions & 0 deletions src/config/Options/shape/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
* @property {object} bar Bar object
* @property {number} [bar.indices.removeNull=false] Remove nullish data on bar indices positions.
* @property {number} [bar.label.threshold=0] Set threshold ratio to show/hide labels.
* @property {boolean} [bar.overlap=false] Bars will be rendered at same position, which will be overlapped each other. (for non-grouped bars only)
* @property {number} [bar.padding=0] The padding pixel value between each bar.
* @property {number} [bar.radius] Set the radius of bar edge in pixel.
* @property {number} [bar.radius.ratio] Set the radius ratio of bar edge in relative the bar's width.
Expand All @@ -29,6 +30,7 @@ export default {
* @property {number} [bar.width.dataname.max] The maximum width value for ratio.
* @property {boolean} [bar.zerobased=true] Set if min or max value will be 0 on bar chart.
* @see [Demo: bar indices](https://naver.github.io/billboard.js/demo/#BarChartOptions.BarIndices)
* @see [Demo: bar overlap](https://naver.github.io/billboard.js/demo/#BarChartOptions.BarOverlap)
* @see [Demo: bar padding](https://naver.github.io/billboard.js/demo/#BarChartOptions.BarPadding)
* @see [Demo: bar radius](https://naver.github.io/billboard.js/demo/#BarChartOptions.BarRadius)
* @see [Demo: bar width](https://naver.github.io/billboard.js/demo/#BarChartOptions.BarWidth)
Expand All @@ -40,6 +42,9 @@ export default {
* removeNull: true
* },
*
* // remove nullish da
* overlap: true,
*
* padding: 1,
*
* // bar radius
Expand Down Expand Up @@ -80,6 +85,7 @@ export default {
*/
bar_label_threshold: 0,
bar_indices_removeNull: false,
bar_overlap: false,
bar_padding: 0,
bar_radius: <number|{ratio: number}|undefined> undefined,
bar_radius_ratio: <number|undefined> undefined,
Expand Down
43 changes: 43 additions & 0 deletions test/shape/bar-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,49 @@ describe("SHAPE BAR", () => {
})
});

describe("bar overlap", () => {
before(() => {
args = {
data: {
columns: [
["data1", 80, 150, 100],
["data2", 100, 120, 130],
["data3", 150, 80, 120]
],
type: "bar"
},
bar: {
width: {
data1: 60,
data2: 40,
data3: 20
},
overlap: true
}
};
});

it("bars should positioned at the center of each x Axis ticks.", () => {
const {x} = chart.internal.scale;
const {bars} = chart.$.bar;
const dataNames = chart.data().map(v => v.id);
const {width} = args.bar;
const ticks = dataNames.map((v, i) => x(i));
const re = /^M(\d+)/;

dataNames.forEach(id => {
const bar = bars.filter(d => d.id === id).nodes();

ticks.forEach((t, i) => {
const xPos = +bar[i].getAttribute("d").match(re)?.[1] ?? 0;
const expectedX = t - width[id] / 2;

expect(xPos).to.be.closeTo(expectedX, 1);
});
});
});
});

describe("bar position", () => {
before(() => {
args = {
Expand Down
5 changes: 5 additions & 0 deletions types/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ export interface ChartOptions {
removeNull?: boolean;
}

/**
* Bars will be rendered at same position, which will be overlapped each other. (for non-grouped bars only)
*/
orverlap?: boolean;

/**
* The padding pixel value between each bar.
*/
Expand Down

0 comments on commit 046aedb

Please sign in to comment.