Skip to content

Commit

Permalink
feat(axis): Enhance padding to accept px value
Browse files Browse the repository at this point in the history
- Implement axis.x.padding.unit='px'
- Removed axis.getXAxisPadding() substituting by .getXDomainPadding()
- Refactor convert pixel to scale

Fix #2246
  • Loading branch information
netil authored Aug 13, 2021
1 parent 36c4550 commit 769ec8f
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 104 deletions.
58 changes: 18 additions & 40 deletions src/ChartInternal/Axis/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,9 @@ class Axis {
if (svg) {
const isYAxis = /^y2?$/.test(id);
const targetsToShow = $$.filterTargetsToShow($$.data.targets);
const scale = $$.scale[id].copy().domain($$[`get${isYAxis ? "Y" : "X"}Domain`](targetsToShow, id));
const scale = $$.scale[id].copy().domain(
$$[`get${isYAxis ? "Y" : "X"}Domain`](targetsToShow, id)
);
const domain = scale.domain();

const isDomainSame = domain[0] === domain[1] && domain.every(v => v > 0);
Expand Down Expand Up @@ -729,35 +731,6 @@ class Axis {
return maxOverflow + tickOffset;
}

/**
* Get x Axis padding
* @param {number} tickCount Tick count
* @returns {object} Padding object values with 'left' & 'right' key
* @private
*/
getXAxisPadding(tickCount: number): {left: number, right: number} {
const $$ = this.owner;
const padding = $$.config.axis_x_padding;
let {left = 0, right = 0} = isNumber(padding) ?
{left: padding, right: padding} : padding;

if ($$.axis.isTimeSeries()) {
const firstX = +$$.getXDomainMin($$.data.targets);
const lastX = +$$.getXDomainMax($$.data.targets);
const timeDiff = lastX - firstX;
const range = timeDiff + left + right;

if (tickCount && range) {
const relativeTickWidth = (timeDiff / tickCount) / range;

left = left / range / relativeTickWidth;
right = right / range / relativeTickWidth;
}
}

return {left, right};
}

updateLabels(withTransition) {
const $$ = this.owner;
const {$el: {main}, $T} = $$;
Expand All @@ -781,22 +754,27 @@ class Axis {
});
}

getPadding(padding, key, defaultValue, domainLength) {
/**
* Get axis padding value
* @param {number|object} padding Padding object
* @param {string} key Key string of padding
* @param {Date|number} defaultValue Default value
* @param {number} domainLength Domain length
* @returns {number} Padding value in scale
* @private
*/
getPadding(padding: number | {[key: string]: number},
key: string, defaultValue: number, domainLength: number): number {
const p = isNumber(padding) ? padding : padding[key];

if (!isValue(p)) {
return defaultValue;
}

return this.convertPixelsToAxisPadding(p, domainLength);
}

convertPixelsToAxisPadding(pixels, domainLength) {
const $$ = this.owner;
const {config, state: {width, height}} = $$;
const length = config.axis_rotated ? width : height;

return domainLength * (pixels / length);
return this.owner.convertPixelToScale(
/(bottom|top)/.test(key) ? "y" : "x",
p, domainLength
);
}

generateTickValues(values, tickCount, forTimeSeries) {
Expand Down
145 changes: 88 additions & 57 deletions src/ChartInternal/internals/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* billboard.js project is licensed under the MIT license
*/
import {TYPE, TYPE_BY_CATEGORY} from "../../config/const";
import {IData} from "../data/IData";
import {brushEmpty, getBrushSelection, getMinMax, isDefined, notEmpty, isValue, isObject, isNumber, diffDomain, parseDate, sortValue} from "../../module/util";

export default {
Expand Down Expand Up @@ -58,14 +59,6 @@ export default {
return getMinMax(type, Object.keys(ys).map(key => getMinMax(type, ys[key])));
},

getYDomainMin(targets): number {
return this.getYDomainMinMax(targets, "min");
},

getYDomainMax(targets): number {
return this.getYDomainMinMax(targets, "max");
},

/**
* Check if hidden targets bound to the given axis id
* @param {string} id ID to be checked
Expand Down Expand Up @@ -106,20 +99,21 @@ export default {

const yMin = config[`${pfx}_min`];
const yMax = config[`${pfx}_max`];
let yDomainMin = $$.getYDomainMin(yTargets);
let yDomainMax = $$.getYDomainMax(yTargets);
const center = config[`${pfx}_center`];
const isInverted = config[`${pfx}_inverted`];
const showHorizontalDataLabel = $$.hasDataLabel() && config.axis_rotated;
const showVerticalDataLabel = $$.hasDataLabel() && !config.axis_rotated;

let yDomainMin = $$.getYDomainMinMax(yTargets, "min");
let yDomainMax = $$.getYDomainMinMax(yTargets, "max");

let isZeroBased = [TYPE.BAR, TYPE.BUBBLE, TYPE.SCATTER, ...TYPE_BY_CATEGORY.Line]
.some(v => {
const type = v.indexOf("area") > -1 ? "area" : v;

return $$.hasType(v, yTargets) && config[`${type}_zerobased`];
});

const isInverted = config[`${pfx}_inverted`];
const showHorizontalDataLabel = $$.hasDataLabel() && config.axis_rotated;
const showVerticalDataLabel = $$.hasDataLabel() && !config.axis_rotated;

// MEMO: avoid inverting domain unexpectedly
yDomainMin = isValue(yMin) ? yMin :
(isValue(yMax) ? (yDomainMin < yMax ? yDomainMin : yMax - 10) : yDomainMin);
Expand All @@ -138,7 +132,6 @@ export default {
yDomainMin < 0 ? yDomainMax = 0 : yDomainMin = 0;
}


const isAllPositive = yDomainMin >= 0 && yDomainMax >= 0;
const isAllNegative = yDomainMin <= 0 && yDomainMax <= 0;

Expand Down Expand Up @@ -176,7 +169,7 @@ export default {
const lengths = $$.getDataLabelLength(yDomainMin, yDomainMax, "height");

["bottom", "top"].forEach((v, i) => {
padding[v] += axis.convertPixelsToAxisPadding(lengths[i], domainLength);
padding[v] += $$.convertPixelToScale("y", lengths[i], domainLength);
});
}

Expand Down Expand Up @@ -219,60 +212,75 @@ export default {
return isDefined(value) ? value : dataValue;
},

getXDomainMin(targets) {
return this.getXDomainMinMax(targets, "min");
},

getXDomainMax(targets) {
return this.getXDomainMinMax(targets, "max");
},

getXDomainPadding(domain) {
/**
* Get x Axis padding
* @param {Array} domain x Axis domain
* @param {number} tickCount Tick count
* @returns {object} Padding object values with 'left' & 'right' key
* @private
*/
getXDomainPadding(domain, tickCount: number): {left: number, right: number} {
const $$ = this;
const {axis, config} = $$;
const diff = domain[1] - domain[0];
const xPadding = config.axis_x_padding;
let maxDataCount;
let padding;

if (axis.isCategorized()) {
padding = 0;
const padding = config.axis_x_padding;
const isTimeSeriesTickCount = axis.isTimeSeries() && tickCount;
const diff = diffDomain(domain);
let defaultValue;

// determine default padding value
if (axis.isCategorized() || isTimeSeriesTickCount) {
defaultValue = 0;
} else if ($$.hasType("bar")) {
maxDataCount = $$.getMaxDataCount();
padding = maxDataCount > 1 ? (diff / (maxDataCount - 1)) / 2 : 0.5;
const maxDataCount = $$.getMaxDataCount();

defaultValue = maxDataCount > 1 ? (diff / (maxDataCount - 1)) / 2 : 0.5;
} else {
padding = diff * 0.01;
defaultValue = diff * 0.01;
}

let left = padding;
let right = padding;
let {left = defaultValue, right = defaultValue} = isNumber(padding) ?
{left: padding, right: padding} : padding;

// when the unit is pixel, convert pixels to axis scale value
if (padding.unit === "px") {
const domainLength = Math.abs(diff + (diff * 0.2));

left = axis.getPadding(padding, "left", defaultValue, domainLength);
right = axis.getPadding(padding, "right", defaultValue, domainLength);
} else {
const range = diff + left + right;

if (isTimeSeriesTickCount && range) {
const relativeTickWidth = (diff / tickCount) / range;

if (isObject(xPadding) && notEmpty(xPadding)) {
left = isValue(xPadding.left) ? xPadding.left : padding;
right = isValue(xPadding.right) ? xPadding.right : padding;
} else if (isNumber(config.axis_x_padding)) {
left = xPadding;
right = xPadding;
left = left / range / relativeTickWidth;
right = right / range / relativeTickWidth;
}
}

return {left, right};
},

getXDomain(targets) {
/**
* Get x Axis domain
* @param {Array} targets targets
* @returns {Array} x Axis domain
* @private
*/
getXDomain(targets?: IData[]): (Date|number)[] {
const $$ = this;
const isLog = $$.scale.x.type === "log";
const xDomain = [$$.getXDomainMin(targets), $$.getXDomainMax(targets)];
let min: Date | number = 0;
let max: Date | number = 0;

if (isLog) {
min = xDomain[0];
max = xDomain[1];
} else {
const isCategorized = $$.axis.isCategorized();
const isTimeSeries = $$.axis.isTimeSeries();
const padding = $$.getXDomainPadding(xDomain);
let [firstX, lastX] = xDomain;
const {axis, scale: {x}} = $$;
const domain = [
$$.getXDomainMinMax(targets, "min"),
$$.getXDomainMinMax(targets, "max")
];
let [min = 0, max = 0] = domain;

if (x.type !== "log") {
const isCategorized = axis.isCategorized();
const isTimeSeries = axis.isTimeSeries();
const padding = $$.getXDomainPadding(domain);
let [firstX, lastX] = domain;

// show center of x domain if min and max are the same
if ((firstX - lastX) === 0 && !isCategorized) {
Expand Down Expand Up @@ -362,5 +370,28 @@ export default {
}

return [min, max];
},

/**
* Converts pixels to axis' scale values
* @param {string} type Axis type
* @param {number} pixels Pixels
* @param {number} domainLength Domain length
* @returns {number}
* @private
*/
convertPixelToScale(type: "x"|"y", pixels: number, domainLength: number): number {
const $$ = this;
const {config, state} = $$;
const isRotated = config.axis_rotated;
let length;

if (type === "x") {
length = isRotated ? "height" : "width";
} else {
length = isRotated ? "width" : "height";
}

return domainLength * (pixels / state[length]);
}
};
7 changes: 6 additions & 1 deletion src/ChartInternal/internals/size.axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ export default {
}

if (tickCount !== state.axis.x.tickCount) {
state.axis.x.padding = $$.axis.getXAxisPadding(tickCount);
const {targets} = $$.data;

state.axis.x.padding = $$.getXDomainPadding([
$$.getXDomainMinMax(targets, "min"),
$$.getXDomainMinMax(targets, "max")
], tickCount);
}

state.axis.x.tickCount = tickCount;
Expand Down
22 changes: 16 additions & 6 deletions src/config/Options/axis/x.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,12 +494,15 @@ export default {
* Set padding for x axis.<br><br>
* If this option is set, the range of x axis will increase/decrease according to the values.
* If no padding is needed in the rage of x axis, 0 should be set.
* By default, left/right padding are set depending on x axis type or chart types.
* - **NOTE:**
* The padding values aren't based on pixels. It differs according axis types<br>
* - **category:** The unit of tick value
* ex. the given value `1`, is same as the width of 1 tick width
* - **timeseries:** Numeric time value
* ex. the given value `1000*60*60*24`, which is numeric time equivalent of a day, is same as the width of 1 tick width
* - The meaning of padding values, differs according axis types:<br>
* - **category/indexed:** The unit of tick value
* ex. the given value `1`, is same as the width of 1 tick width
* - **timeseries:** Numeric time value
* ex. the given value `1000*60*60*24`, which is numeric time equivalent of a day, is same as the width of 1 tick width
* - If want values to be treated as pixels, specify `unit:"px"`.
* - The pixel value will be convered based on the scale values. Hence can not reflect accurate padding result.
* @name axis․x․padding
* @memberof Options
* @type {object|number}
Expand All @@ -518,7 +521,14 @@ export default {
* },
*
* // or set both values at once.
* padding: 10
* padding: 10,
*
* // or set padding values as pixel unit.
* padding: {
* left: 100,
* right: 50,
* unit: "px"
* },
* }
* }
*/
Expand Down
2 changes: 2 additions & 0 deletions src/config/Store/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
export default class State {
constructor() {
return {
// chart drawn area dimension, excluding axes
width: 0,
width2: 0,
height: 0,
Expand Down Expand Up @@ -41,6 +42,7 @@ export default class State {
hasRadar: false,

current: {
// chart whole dimension
width: 0,
height: 0,
dataMax: 0,
Expand Down
Loading

0 comments on commit 769ec8f

Please sign in to comment.