@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
245 lines (201 loc) • 7.54 kB
JavaScript
/* eslint-disable camelcase */
import { drawing } from '@progress/kendo-drawing';
import { SankeyElement } from './element';
import { deepExtend } from '../common';
import { defined } from '../drawing-utils';
import { ARIA_ACTIVE_DESCENDANT } from '../common/constants';
const bezierPoint = (p1, p2, p3, p4, t) => {
const t1 = 1 - t;
const t1t1 = t1 * t1;
const tt = t * t;
return (p1 * t1t1 * t1) + (3 * p2 * t * t1t1) + (3 * p3 * tt * t1) + (p4 * tt * t);
};
function calculatePerpendicularLine(x1, y1, x2, y2, L) {
// 1. Calculate the midpoint M
let xM = (x1 + x2) / 2;
let yM = (y1 + y2) / 2;
let dx, dy;
if (y1 === y2) {
// The line AB is horizontal
dx = 0;
dy = L / 2;
} else if (x1 === x2) {
// The line AB is vertical
dx = L / 2;
dy = 0;
} else {
// Common case when the line is not horizontal or vertical
// 2. Calculate the slope of the original line
let m = (y2 - y1) / (x2 - x1);
// 3. Calculate the slope of the perpendicular line
let mPerp = -1 / m;
// 4. Calculate dx and dy
dx = (L / 2) / Math.sqrt(1 + mPerp * mPerp);
dy = mPerp * dx;
}
// 5. Coordinates of the points of the perpendicular line
let P1 = { x: xM - dx, y: yM - dy };
let P2 = { x: xM + dx, y: yM + dy };
return { P1, P2 };
}
function findIntersection(a, b, L, p, q) {
// Midpoint between a and b
const midpoint = {
x: (a.x + b.x) / 2,
y: (a.y + b.y) / 2
};
// Vector of the line ab
const ab_dx = b.x - a.x;
const ab_dy = b.y - a.y;
// Vector, perpendicular to ab
let perp_dx = -ab_dy;
let perp_dy = ab_dx;
// Normalize the perpendicular vector and scale it to 2*L
const magnitude = Math.sqrt(perp_dx * perp_dx + perp_dy * perp_dy);
perp_dx = (perp_dx / magnitude) * L;
perp_dy = (perp_dy / magnitude) * L;
// The endpoints of the perpendicular, 2*L long
const c1 = {
x: midpoint.x + perp_dx,
y: midpoint.y + perp_dy
};
const c2 = {
x: midpoint.x - perp_dx,
y: midpoint.y - perp_dy
};
// Check for intersection of the lines pq and the perpendicular
const pq_dx = q.x - p.x;
const pq_dy = q.y - p.y;
const denominator = (pq_dy) * (c1.x - c2.x) - (pq_dx) * (c1.y - c2.y);
if (Math.abs(denominator) < 1e-10) {
// The lines are almost parallel, no intersection
return null;
}
const ua = (pq_dx * (c2.y - p.y) - pq_dy * (c2.x - p.x)) / denominator;
const ub = ((c1.x - c2.x) * (c2.y - p.y) - (c1.y - c2.y) * (c2.x - p.x)) / denominator;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
const intersection = {
x: c2.x + ua * (c1.x - c2.x),
// y: c2.y + ua * (c1.y - c2.y)
};
return intersection;
}
// No intersection of the segments
return null;
}
const calculateControlPointsOffsetX = (link, rtl) => {
const halfWidth = link.width / 2;
const x0 = rtl ? link.x1 : link.x0;
const x1 = rtl ? link.x0 : link.x1;
const y0 = rtl ? link.y1 : link.y0;
const y1 = rtl ? link.y0 : link.y1;
const xC = (x0 + x1) / 2;
const middlePoint = [xC, bezierPoint(y0, y0, y1, y1, 0.5)];
const tH = 0.4999;
const pointH = [
bezierPoint(x0, xC, xC, x1, tH),
bezierPoint(y0, y0, y1, y1, tH)
];
const line = calculatePerpendicularLine(middlePoint[0], middlePoint[1], pointH[0], pointH[1], link.width);
const middlePointDown = [xC, bezierPoint(y0 + halfWidth, y0 + halfWidth, y1 + halfWidth, y1 + halfWidth, 0.5)];
// const middlePointUp = [xC, bezierPoint(y0 - halfWidth, y0 - halfWidth, y1 - halfWidth, y1 - halfWidth, 0.5)];
const P = line.P1.y > line.P2.y ? line.P1 : line.P2;
const L = halfWidth;
const LDir = (y0 > y1 ? 1 : -1) * L;
const a = P;
const b = { x: middlePointDown[0], y: middlePointDown[1] };
const p = { x: middlePointDown[0], y: middlePointDown[1] };
const q = { x: Math.max(1, middlePointDown[0] + LDir), y: middlePointDown[1] };
const Pmx = findIntersection(a, b, L, p, q) || { x: (middlePointDown[0] + P.x) / 2 };
const P1 = x0;
const P4 = x1;
const P2 = (Pmx.x - (0.125 * P1) - (0.125 * P4)) / 0.75;
return xC - P2;
};
export class Link extends SankeyElement {
getElement() {
const link = this.options.link;
const { x0, x1, y0, y1 } = link;
const xC = (x0 + x1) / 2;
return new drawing.Path(this.visualOptions())
.moveTo(x0, y0).curveTo([xC, y0], [xC, y1], [x1, y1]);
}
getLabelText(options) {
let labelTemplate = options.labels.ariaTemplate;
if (labelTemplate) {
return labelTemplate({ link: options.link });
}
}
visualOptions() {
const options = this.options;
const link = this.options.link;
const ariaLabel = this.getLabelText(options);
return {
stroke: {
width: options.link.width,
color: link.color || options.color,
opacity: defined(link.opacity) ? link.opacity : options.opacity
},
role: 'graphics-symbol',
ariaRoleDescription: 'Link',
ariaLabel: ariaLabel
};
}
createFocusHighlight() {
if (!this.options.navigatable) {
return;
}
const { link } = this.options;
const { x0, x1, y0, y1 } = link;
const xC = (x0 + x1) / 2;
const halfWidth = link.width / 2;
const offset = calculateControlPointsOffsetX(link, this.options.rtl);
this._highlight = new drawing.Path({ stroke: this.options.focusHighlight.border, visible: false })
.moveTo(x0, y0 + halfWidth)
.lineTo(x0, y0 - halfWidth)
.curveTo([xC + offset, y0 - halfWidth], [xC + offset, y1 - halfWidth], [x1, y1 - halfWidth])
.lineTo(x1, y1 + halfWidth)
.curveTo([xC - offset, y1 + halfWidth], [xC - offset, y0 + halfWidth], [x0, y0 + halfWidth]);
}
focus(options) {
if (this._highlight) {
const { highlight = true } = options || {};
if (highlight) {
this._highlight.options.set('visible', true);
}
const id = `${this.options.link.sourceId}->${this.options.link.targetId}`;
this.visual.options.set('id', id);
if (this.options.root()) {
this.options.root().setAttribute(ARIA_ACTIVE_DESCENDANT, id);
}
}
}
blur() {
if (this._highlight) {
this._highlight.options.set('visible', false);
this.visual.options.set('id', '');
if (this.options.root()) {
this.options.root().removeAttribute(ARIA_ACTIVE_DESCENDANT);
}
}
}
}
export const resolveLinkOptions = (link, options, sourceNode, targetNode) => {
const linkOptions = deepExtend({},
options,
{
link,
opacity: link.opacity,
color: link.color,
colorType: link.colorType,
visual: link.visual,
highlight: link.highlight
}
);
if (linkOptions.colorType === 'source') {
linkOptions.color = sourceNode.options.fill.color;
} else if (linkOptions.colorType === 'target') {
linkOptions.color = targetNode.options.fill.color;
}
return linkOptions;
};