blob: 66c129d97363b5dfe5b0c2faea75a27235a7fa00 [file]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
import * as d3 from "d3";
import * as d3path from "d3-path";
const halfPI = Math.PI / 2.0;
const twoPI = Math.PI * 2.0;
// These are scales to interpolate how the bezier control point should be adjusted.
// These numbers were determined emperically by adjusting a chord and discovering
// the relationship between the width of the inner bezier and the lengths of the arcs.
// If we were just drawing the chord diagram once, we wouldn't need to use scales.
// But since we are animating chords, we need to smoothly chnage the control point from
// [0, 0] to 1/2 way to the center of the bezier curve.
const dom = [0.06, 0.98, Math.PI];
const ys = d3.scale
.linear()
.domain(dom)
.range([0.18, 0, 0]);
const x0s = d3.scale
.linear()
.domain(dom)
.range([0.03, 0.24, 0.24]);
const x1s = d3.scale
.linear()
.domain(dom)
.range([0.24, 0.6, 0.6]);
const x2s = d3.scale
.linear()
.domain(dom)
.range([1.32, 0.8, 0.8]);
const x3s = d3.scale
.linear()
.domain(dom)
.range([3, 2, 2]);
function qdrRibbon() {
// eslint-disable-line no-unused-vars
var r = 200; // random default. this will be set later
// This is the function that gets called to produce a path for a chord.
// The path should end up looking like
// M[start point]A[arc options][arc end point]Q[control point][end points]A[arc options][arc end point]Q[control point][end points]Z
var ribbon = function(d) {
let sa0 = d.source.startAngle - halfPI,
sa1 = d.source.endAngle - halfPI,
ta0 = d.target.startAngle - halfPI,
ta1 = d.target.endAngle - halfPI;
// The control points for the bezier curves
let cp1 = [0, 0];
let cp2 = [0, 0];
// the span of the two arcs
let arc1 = Math.abs(sa0 - sa1);
let arc2 = Math.abs(ta0 - ta1);
let largeArc = Math.max(arc1, arc2);
let smallArc = Math.min(arc1, arc2);
// the gaps between the arcs
let gap1 = Math.abs(sa1 - ta0);
if (gap1 > Math.PI) gap1 = twoPI - gap1;
let gap2 = Math.abs(sa0 - ta1);
if (gap2 > Math.PI) gap2 = twoPI - gap2;
let sgap = Math.min(gap1, gap2);
// if the bezier curves intersect, ratiocp will be > 0
let ratiocp = cpRatio(sgap, largeArc, smallArc);
// x, y points for the start and end of the arcs
let s0x = r * Math.cos(sa0),
s0y = r * Math.sin(sa0),
t0x = r * Math.cos(ta0),
t0y = r * Math.sin(ta0);
if (ratiocp > 0) {
// determine which control point to calculate
if (Math.abs(gap1 - gap2) < 1e-2 || gap1 < gap2) {
let s1x = r * Math.cos(sa1),
s1y = r * Math.sin(sa1);
cp1 = [(ratiocp * (s1x + t0x)) / 2, (ratiocp * (s1y + t0y)) / 2];
} else {
let t1x = r * Math.cos(ta1),
t1y = r * Math.sin(ta1);
cp2 = [(ratiocp * (t1x + s0x)) / 2, (ratiocp * (t1y + s0y)) / 2];
}
}
// construct the path using the control points
let path = d3path.path();
path.moveTo(s0x, s0y);
path.arc(0, 0, r, sa0, sa1);
if (sa0 !== ta0 || sa1 !== ta1) {
path.quadraticCurveTo(cp1[0], cp1[1], t0x, t0y);
path.arc(0, 0, r, ta0, ta1);
}
path.quadraticCurveTo(cp2[0], cp2[1], s0x, s0y);
path.closePath();
return path + "";
};
ribbon.radius = function(radius) {
if (!arguments.length) return r;
r = radius;
return ribbon;
};
return ribbon;
}
let sqr = function(n) {
return n * n;
};
let dist = function(p1x, p1y, p2x, p2y) {
return sqr(p1x - p2x) + sqr(p1y - p2y);
};
// distance from a point to a line segment
let distToLine = function(vx, vy, wx, wy, px, py) {
let vlen = dist(vx, vy, wx, wy);
if (vlen === 0) return dist(px, py, vx, vy);
var t = ((px - vx) * (wx - vx) + (py - vy) * (wy - vy)) / vlen;
t = Math.max(0, Math.min(1, t)); // clamp t to between 0 and 1
return Math.sqrt(dist(px, py, vx + t * (wx - vx), vy + t * (wy - vy)));
};
// See if x, y is contained in trapezoid.
// gap is the smallest gap in the chord
// x is the size of the longest arc
// y is the size of the smallest arc
// the trapezoid is defined by [x0, 0] [x1, top] [x2, top] [x3, 0]
// these points are determined by the gap
let cpRatio = function(gap, x, y) {
let top = ys(gap);
if (y >= top) return 0;
// get the xpoints of the trapezoid
let x0 = x0s(gap);
if (x <= x0) return 0;
let x3 = x3s(gap);
if (x > x3) return 0;
let x1 = x1s(gap);
let x2 = x2s(gap);
// see if the point is to the right of (inside) the leftmost diagonal
// compute the outer product of the left diagonal and the point
let op = (x - x0) * top - y * (x1 - x0);
if (op <= 0) return 0;
// see if the point is to the left of the right diagonal
op = (x - x3) * top - y * (x2 - x3);
if (op >= 0) return 0;
// the point is in the trapezoid. see how far in
let dist = 0;
if (x < x1) {
// left side. get distance to left diagonal
dist = distToLine(x0, 0, x1, top, x, y);
} else if (x > x2) {
// right side. get distance to right diagonal
dist = distToLine(x3, 0, x2, top, x, y);
} else {
// middle. get distance to top
dist = top - y;
}
let distScale = d3.scale
.linear()
.domain([0, top / 8, top / 2, top])
.range([0, 0.3, 0.4, 0.5]);
return distScale(dist);
};
export { qdrRibbon };