Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(regions): Enhance regions rendering #3855

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/ChartInternal/data/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,10 +683,21 @@ export default {
(isObjectType(dataLabels) && notEmpty(dataLabels));
},

/**
* Determine if has null value
* @param {Array} targets Data array to be evaluated
* @returns {boolean}
* @private
*/
hasNullDataValue(targets: IDataRow[]): boolean {
return targets.some(({value}) => value === null);
},

/**
* Get data index from the event coodinates
* @param {Event} event Event object
* @returns {number}
* @private
*/
getDataIndexFromEvent(event): number {
const $$ = this;
Expand Down
8 changes: 4 additions & 4 deletions src/ChartInternal/internals/scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import type {IDataRow, IGridData} from "../data/IData";
/**
* Get scale
* @param {string} [type='linear'] Scale type
* @param {number} [min] Min range
* @param {number} [max] Max range
* @param {number|Date} [min] Min range
* @param {number|Date} [max] Max range
* @returns {d3.scaleLinear|d3.scaleTime} scale
* @private
*/
export function getScale(type = "linear", min = 0, max = 1): any {
export function getScale(type = "linear", min, max): any {
const scale = ({
linear: d3ScaleLinear,
log: d3ScaleSymlog,
Expand All @@ -32,7 +32,7 @@ export function getScale(type = "linear", min = 0, max = 1): any {
scale.type = type;
/_?log/.test(type) && scale.clamp(true);

return scale.range([min, max]);
return scale.range([min ?? 0, max ?? 1]);
}

export default {
Expand Down
185 changes: 149 additions & 36 deletions src/ChartInternal/shape/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,72 @@ import {
isValue,
parseDate
} from "../../module/util";
import type {IDataRow} from "../data/IData";
import {getScale} from "../internals/scale";

/**
* Get stroke dasharray style value
* @param {number} start Start position in path length
* @param {number} end End position in path length
* @param {Array} pattern Dash array pattern
* @param {boolean} isLastX Weather is last x tick
* @returns {object} Stroke dasharray style value and its length
* @private
*/
function getStrokeDashArray(start: number, end: number, pattern: [number, number],
isLastX = false): {dash: string, length: number} {
const dash = start ? [start, 0] : pattern;

for (let i = start ? start : pattern.reduce((a, c) => a + c); i <= end;) {
pattern.forEach(v => {
if (i + v <= end) {
dash.push(v);
}

i += v;
});
}

// make sure to have even length
dash.length % 2 !== 0 && dash.push(isLastX ? pattern[1] : 0);

return {
dash: dash.join(" "),
length: dash.reduce((a, b) => a + b, 0)
};
}

/**
* Get regions data
* @param {Array} d Data object
* @param {object} _regions regions to be set
* @param {boolean} isTimeSeries whether is time series
* @returns {object} Regions data
* @private
*/
function getRegions(d, _regions, isTimeSeries) {
const $$ = this;
const regions: {start: number | string, end: number | string, style: string}[] = [];
const dasharray = "2 2"; // default value

// Check start/end of regions
if (isDefined(_regions)) {
const getValue = (v: Date | any, def: number | Date): Date | any => (
isUndefined(v) ? def : (isTimeSeries ? parseDate.call($$, v) : v)
);

for (let i = 0, reg; (reg = _regions[i]); i++) {
const start = getValue(reg.start, d[0].x);
const end = getValue(reg.end, d[d.length - 1].x);
const style = reg.style || {dasharray};

regions[i] = {start, end, style};
}
}

return regions;
}

export default {
initLine(): void {
const {$el} = this;
Expand Down Expand Up @@ -211,34 +275,31 @@ export default {
};
},

lineWithRegions(d, x, y, _regions): string {
/**
* Set regions dasharray and get path
* @param {Array} d Data object
* @param {Function} x x scale function
* @param {Function} y y scale function
* @param {object} _regions regions to be set
* @returns {stirng} Path string
* @private
*/
lineWithRegions(d: IDataRow[], x, y, _regions): string {
const $$ = this;
const {config} = $$;
const isRotated = config.axis_rotated;
const isTimeSeries = $$.axis.isTimeSeries();
const regions: any[] = [];
const dasharray = "2 2"; // default value
const regions = getRegions.bind($$)(d, _regions, isTimeSeries);

// when contains null data, can't apply style dashed
const hasNullDataValue = $$.hasNullDataValue(d);

let xp;
let yp;
let diff;
let diffx2;

// Check start/end of regions
if (isDefined(_regions)) {
const getValue = (v: Date | any, def: number): Date | any => (
isUndefined(v) ? def : (isTimeSeries ? parseDate.call($$, v) : v)
);

for (let i = 0, reg; (reg = _regions[i]); i++) {
const start = getValue(reg.start, d[0].x);
const end = getValue(reg.end, d[d.length - 1].x);
const style = reg.style || {dasharray};

regions[i] = {start, end, style};
}
}

// Set scales
const xValue = isRotated ? dt => y(dt.value) : dt => x(dt.x);
const yValue = isRotated ? dt => x(dt.x) : dt => y(dt.value);
Expand Down Expand Up @@ -279,15 +340,12 @@ export default {
yDiff = y0;
}

const points = isRotated ?
[
[yValue, xValue],
[yDiff, xDiff]
] :
[
[xValue, yValue],
[xDiff, yDiff]
];
const points = [
[xValue, yValue],
[xDiff, yDiff]
];

isRotated && points.forEach(v => v.reverse());

return generateM(points);
};
Expand All @@ -296,6 +354,16 @@ export default {
const axisType = {x: $$.axis.getAxisType("x"), y: $$.axis.getAxisType("y")};
let path = "";

// clone the line path to be used to get length value
const target = $$.$el.line.filter(({id}) => id === d[0].id);
const tempNode = target.clone().style("display", "none");
const getLength = (node, path) => node.attr("d", path).node().getTotalLength();
const dashArray = {
dash: <string[]>[],
lastLength: 0
};
let isLastX = false;

for (let i = 0, data; (data = d[i]); i++) {
const prevData = d[i - 1];
const hasPrevData = prevData && isValue(prevData.value);
Expand All @@ -316,24 +384,69 @@ export default {
xp = getScale(axisType.x, prevData.x, data.x);
yp = getScale(axisType.y, prevData.value, data.value);

const dx = x(data.x) - x(prevData.x);
const dy = y(data.value) - y(prevData.value);
const dd = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
// when it contains null data, dash can't be applied with style
if (hasNullDataValue) {
const dx = x(data.x) - x(prevData.x);
const dy = y(data.value) - y(prevData.value);
const dd = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));

diff = style[0] / dd; // dash
diffx2 = diff * style[1]; // gap
diff = style[0] / dd; // dash
diffx2 = diff * style[1]; // gap

for (let j = diff; j <= 1; j += diffx2) {
path += sWithRegion(prevData, data, j, diff);
for (let j = diff; j <= 1; j += diffx2) {
path += sWithRegion(prevData, data, j, diff);

// to make sure correct line drawing
if (j + diffx2 >= 1) {
path += sWithRegion(prevData, data, 1, 0);
// to make sure correct line drawing
if (j + diffx2 >= 1) {
path += sWithRegion(prevData, data, 1, 0);
}
}
} else {
let points = <number[][]>[];
isLastX = data.x === d[d.length - 1].x;

if (isTimeSeries) {
const x0 = +prevData.x;
const xv0 = new Date(x0);
const xv1 = new Date(x0 + (+data.x - x0));

points = [
[x(xv0), y(yp(0))], // M
[x(xv1), y(yp(1))] // L
];
} else {
points = [
[x(xp(0)), y(yp(0))], // M
[x(xp(1)), y(yp(1))] // L
];
}

isRotated && points.forEach(v => v.reverse());

const startLength = getLength(tempNode, path);
const endLength = getLength(tempNode, path += `L${points[1].join(",")}`);

const strokeDashArray = getStrokeDashArray(
startLength - dashArray.lastLength,
endLength - dashArray.lastLength,
style,
isLastX
);

dashArray.lastLength += strokeDashArray.length;
dashArray.dash.push(strokeDashArray.dash);
}
}
}

if (dashArray.dash.length) {
// if not last x tick, then should draw rest of path that is not drawed yet
!isLastX && dashArray.dash.push(getLength(tempNode, path));

tempNode.remove();
target.attr("stroke-dasharray", dashArray.dash.join(" "));
}

return path;
},

Expand Down
8 changes: 6 additions & 2 deletions src/config/Options/data/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,12 @@ export default {
* - The object type should be as:
* - start {number}: Start data point number. If not set, the start will be the first data point.
* - [end] {number}: End data point number. If not set, the end will be the last data point.
* - [style.dasharray="2 2"] {object}: The first number specifies a distance for the filled area, and the second a distance for the unfilled area.
* - **NOTE:** Currently this option supports only line chart and dashed style. If this option specified, the line will be dashed only in the regions.
* - [style.dasharray="2 2"] {string}: The first number specifies a distance for the filled area, and the second a distance for the unfilled area.
* - **NOTE:**
* - Supports only line type.
* - `start` and `end` values should be in the exact x value range.
* - Dashes will be applied using `stroke-dasharray` css property when data doesn't contain nullish value(or nullish value with `line.connectNull=true` set).
* - Dashes will be applied via path command when data contains nullish value.
* @name data․regions
* @memberof Options
* @type {object}
Expand Down
Loading
Loading