Skip to content

Commit

Permalink
Factor out some scale- and rect-related functions (#5)
Browse files Browse the repository at this point in the history
Goal: Make it easy to allow fixed endpoints in ReferenceLine

Create a `ScaleHelper` to provide two useful helper functions on top of what d3-scale provides, and a `LabeledScaleHelper` which makes it convenient to work with multiple scales at once. Includes unit tests of those helpers.

Refactor the reference areas to use the new helpers.
  • Loading branch information
paulmelnikow committed Jul 4, 2018
1 parent db0f3f2 commit bda6bd1
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 115 deletions.
49 changes: 12 additions & 37 deletions src/cartesian/ReferenceArea.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Layer from '../container/Layer';
import Label from '../component/Label';
import { PRESENTATION_ATTRIBUTES } from '../util/ReactUtils';
import { isNumOrStr } from '../util/DataUtils';
import { validateCoordinateInRange } from '../util/ChartUtils';
import { LabeledScaleHelper, rectWithPoints } from '../util/CartesianUtils';
import Rectangle from '../shape/Rectangle';

@pureRender
Expand Down Expand Up @@ -58,46 +58,21 @@ class ReferenceArea extends Component {
getRect(hasX1, hasX2, hasY1, hasY2) {
const { x1: xValue1, x2: xValue2, y1: yValue1, y2: yValue2, xAxis,
yAxis } = this.props;
const xScale = xAxis.scale;
const yScale = yAxis.scale;
const xOffset = xScale.bandwidth ? xScale.bandwidth() / 2 : 0;
const yOffset = yScale.bandwidth ? yScale.bandwidth() / 2 : 0;
const xRange = xScale.range();
const yRange = yScale.range();
let x1, x2, y1, y2;

if (hasX1) {
x1 = xScale(xValue1) + xOffset;
} else {
x1 = xRange[0];
}

if (hasX2) {
x2 = xScale(xValue2) + xOffset;
} else {
x2 = xRange[1];
}
const scale = LabeledScaleHelper.create({ x: xAxis.scale, y: yAxis.scale });

if (hasY1) {
y1 = yScale(yValue1) + yOffset;
} else {
y1 = yRange[0];
}
const p1 = {
x: hasX1 ? scale.x.apply(xValue1) : scale.x.rangeMin,
y: hasY1 ? scale.y.apply(yValue1) : scale.y.rangeMin,
};

if (hasY2) {
y2 = yScale(yValue2) + yOffset;
} else {
y2 = yRange[1];
}
const p2 = {
x: hasX2 ? scale.x.apply(xValue2) : scale.x.rangeMax,
y: hasY2 ? scale.y.apply(yValue2) : scale.y.rangeMax,
};

if (validateCoordinateInRange(x1, xScale) && validateCoordinateInRange(x2, xScale) &&
validateCoordinateInRange(y1, yScale) && validateCoordinateInRange(y2, yScale)) {
return {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
};
if (scale.isInRange(p1) && scale.isInRange(p2)) {
return rectWithPoints(p1, p2);
}

return null;
Expand Down
25 changes: 10 additions & 15 deletions src/cartesian/ReferenceDot.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PRESENTATION_ATTRIBUTES, EVENT_ATTRIBUTES,
getPresentationAttributes, filterEventAttributes } from '../util/ReactUtils';
import Label from '../component/Label';
import { isNumOrStr } from '../util/DataUtils';
import { validateCoordinateInRange } from '../util/ChartUtils';
import { LabeledScaleHelper } from '../util/CartesianUtils';

@pureRender
class ReferenceDot extends Component {
Expand Down Expand Up @@ -52,19 +52,11 @@ class ReferenceDot extends Component {

getCoordinate() {
const { x, y, xAxis, yAxis } = this.props;
const xScale = xAxis.scale;
const yScale = yAxis.scale;
const result = {
cx: xScale(x) + (xScale.bandwidth ? xScale.bandwidth() / 2 : 0),
cy: yScale(y) + (yScale.bandwidth ? yScale.bandwidth() / 2 : 0),
};
const scales = LabeledScaleHelper.create({ x: xAxis.scale, y: yAxis.scale });

if (validateCoordinateInRange(result.cx, xScale) &&
validateCoordinateInRange(result.cy, yScale)) {
return result;
}
const result = scales.apply({ x, y }, { bandAware: true });

return null;
return scales.isInRange(result) ? result : null;
}

renderDot(option, props) {
Expand Down Expand Up @@ -99,20 +91,23 @@ class ReferenceDot extends Component {

if (!coordinate) { return null; }

const { x: cx, y: cy } = coordinate;

const { shape, className } = this.props;

const dotProps = {
...getPresentationAttributes(this.props),
...filterEventAttributes(this.props),
...coordinate,
cx,
cy,
};

return (
<Layer className={classNames('recharts-reference-dot', className)}>
{this.renderDot(shape, dotProps)}
{Label.renderCallByParent(this.props, {
x: coordinate.cx - r,
y: coordinate.cy - r,
x: cx - r,
y: cy - r,
width: 2 * r,
height: 2 * r,
})}
Expand Down
79 changes: 40 additions & 39 deletions src/cartesian/ReferenceLine.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PRESENTATION_ATTRIBUTES, getPresentationAttributes,
filterEventAttributes } from '../util/ReactUtils';
import Label from '../component/Label';
import { isNumOrStr } from '../util/DataUtils';
import { validateCoordinateInRange } from '../util/ChartUtils';
import { LabeledScaleHelper, rectWithCoords } from '../util/CartesianUtils';

const renderLine = (option, props) => {
let line;
Expand Down Expand Up @@ -72,67 +72,68 @@ class ReferenceLine extends Component {
strokeWidth: 1,
};

getEndPoints(isX, isY) {
const { xAxis, yAxis, viewBox } = this.props;
getEndPoints(scales, isFixedX, isFixedY) {
const { viewBox } = this.props;
const { x, y, width, height } = viewBox;

if (isY) {
const value = this.props.y;
const { scale } = yAxis;
const offset = scale.bandwidth ? scale.bandwidth() / 2 : 0;
const coord = scale(value) + offset;

if (validateCoordinateInRange(coord, scale)) {
return yAxis.orientation === 'left' ?
[{ x, y: coord }, { x: x + width, y: coord }] :
[{ x: x + width, y: coord }, { x, y: coord }];
if (isFixedY) {
const { y: yCoord, yAxis: { orientation } } = this.props;
const coord = scales.y.apply(yCoord);
if (scales.y.isInRange(coord)) {
const points = [
{ x: x + width, y: coord },
{ x, y: coord },
];
return orientation === 'left' ? points.reverse() : points;
}
} else if (isX) {
const value = this.props.x;
const { scale } = xAxis;
const offset = scale.bandwidth ? scale.bandwidth() / 2 : 0;
const coord = scale(value) + offset;

if (validateCoordinateInRange(coord, scale)) {
return xAxis.orientation === 'top' ?
[{ x: coord, y }, { x: coord, y: y + height }] :
[{ x: coord, y: y + height }, { x: coord, y }];
} else if (isFixedX) {
const { x: xCoord, xAxis: { orientation } } = this.props;
const coord = scales.x.apply(xCoord);
if (scales.x.isInRange(coord)) {
const points = [
{ x: coord, y: y + height },
{ x: coord, y },
];
return orientation === 'top' ? points.reverse() : points;
}
}

return null;
}

render() {
const { x, y, shape, className } = this.props;
const isX = isNumOrStr(x);
const isY = isNumOrStr(y);
const {
x: fixedX,
y: fixedY,
xAxis,
yAxis,
shape,
className,
} = this.props;

if (!isX && !isY) { return null; }
const scales = LabeledScaleHelper.create({ x: xAxis.scale, y: yAxis.scale });

const endPoints = this.getEndPoints(isX, isY);
const isX = isNumOrStr(fixedX);
const isY = isNumOrStr(fixedY);

const endPoints = this.getEndPoints(scales, isX, isY);
if (!endPoints) { return null; }

const [start, end] = endPoints;
const [{ x: x1, y: y1 }, { x: x2, y: y2 }] = endPoints;

const props = {
...getPresentationAttributes(this.props),
...filterEventAttributes(this.props),
x1: start.x,
y1: start.y,
x2: end.x,
y2: end.y,
x1,
y1,
x2,
y2,
};

return (
<Layer className={classNames('recharts-reference-line', className)}>
{renderLine(shape, props)}
{Label.renderCallByParent(this.props, {
x: Math.min(props.x1, props.x2),
y: Math.min(props.y1, props.y2),
width: Math.abs(props.x2 - props.x1),
height: Math.abs(props.y2 - props.y1),
})}
{Label.renderCallByParent(this.props, rectWithCoords({ x1, y1, x2, y2 }))}
</Layer>
);
}
Expand Down
92 changes: 92 additions & 0 deletions src/util/CartesianUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from 'lodash';
import { getTicksOfScale, parseScale, checkDomainOfScale, getBandSizeOfAxis } from './ChartUtils';

/**
Expand Down Expand Up @@ -85,3 +86,94 @@ export const formatAxisMap = (props, axisMap, offset, axisType, chartName) => {
return { ...result, [id]: finalAxis };
}, {});
};

export const rectWithPoints = ({ x: x1, y: y1 }, { x: x2, y: y2 }) => ({
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});

/**
* Compute the x, y, width, and height of a box from two reference points.
* @param {Object} coords x1, x2, y1, and y2
* @return {Object} object
*/
export const rectWithCoords = ({ x1, y1, x2, y2 }) =>
rectWithPoints({ x: x1, y: y1 }, { x: x2, y: y2 });

export class ScaleHelper {
static EPS = 1e-4;

static create(obj) {
return new ScaleHelper(obj);
}

constructor(scale) {
this.scale = scale;
}

get domain() {
return this.scale.domain;
}

get range() {
return this.scale.range;
}

get rangeMin() {
return this.range()[0];
}

get rangeMax() {
return this.range()[1];
}

get bandwidth() {
return this.scale.bandwidth;
}

apply(value, { bandAware } = {}) {
if (value === undefined) {
return undefined;
} else if (bandAware) {
const offset = this.bandwidth ? this.bandwidth() / 2 : 0;
return this.scale(value) + offset;
}
return this.scale(value);
}

isInRange(value) {
const range = this.range();

const first = range[0];
const last = range[range.length - 1];

return first <= last ?
(value >= first && value <= last) :
(value >= last && value <= first);
}
}

export class LabeledScaleHelper {
static create(obj) {
return new this(obj);
}

constructor(scales) {
this.scales = _.mapValues(scales, ScaleHelper.create);
Object.assign(this, this.scales);
}

apply(coords, { bandAware } = {}) {
const { scales } = this;
return _.mapValues(
coords,
(value, label) => scales[label].apply(value, { bandAware }));
}

isInRange(coords) {
const { scales } = this;
return _.every(coords, (value, label) => scales[label].isInRange(value));
}
}
13 changes: 0 additions & 13 deletions src/util/ChartUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -961,19 +961,6 @@ export const parseSpecifiedDomain = (specifiedDomain, dataDomain, allowDataOverf
return domain;
};

export const validateCoordinateInRange = (coordinate, scale) => {
if (!scale) { return false; }

const range = scale.range();
const first = range[0];
const last = range[range.length - 1];
const isValidate = first <= last ?
(coordinate >= first && coordinate <= last) :
(coordinate >= last && coordinate <= first);

return isValidate;
};

/**
* Calculate the size between two category
* @param {Object} axis The options of axis
Expand Down

0 comments on commit bda6bd1

Please sign in to comment.