Skip to content

Commit

Permalink
feat(regions): Enhance regions rendering
Browse files Browse the repository at this point in the history
Improve dashed line rendering using css instead path command.
Will render same way for nullish value.

Fix #3830
Close #3790
  • Loading branch information
netil authored Jul 25, 2024
1 parent 5071a47 commit 567b323
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 86 deletions.
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

0 comments on commit 567b323

Please sign in to comment.