UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

245 lines (201 loc) 7.54 kB
/* 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; };