Merge pull request #240 from nniclas/spline

feat: added spline functionality
This commit is contained in:
Shivam Mishra 2019-09-02 09:53:43 +05:30 committed by GitHub
commit 6b81a0c011
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 146 additions and 63 deletions

View File

@ -30,4 +30,4 @@
"globals": {
"ENV": true
}
}
}

View File

@ -411,6 +411,50 @@ function shortenLargeNumber(label) {
return Math.round(shortened*100)/100 + ' ' + ['', 'K', 'M', 'B', 'T'][l];
}
// cubic bezier curve calculation (from example by François Romain)
function createSplineCurve(xList, yList) {
let points=[];
for(let i=0;i<xList.length;i++){
points.push([xList[i], yList[i]]);
}
let smoothing = 0.2;
let line = (pointA, pointB) => {
let lengthX = pointB[0] - pointA[0];
let lengthY = pointB[1] - pointA[1];
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
};
};
let controlPoint = (current, previous, next, reverse) => {
let p = previous || current;
let n = next || current;
let o = line(p, n);
let angle = o.angle + (reverse ? Math.PI : 0);
let length = o.length * smoothing;
let x = current[0] + Math.cos(angle) * length;
let y = current[1] + Math.sin(angle) * length;
return [x, y];
};
let bezierCommand = (point, i, a) => {
let cps = controlPoint(a[i - 1], a[i - 2], point);
let cpe = controlPoint(point, a[i - 1], a[i + 1], true);
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
};
let pointStr = (points, command) => {
return points.reduce((acc, point, i, a) => i === 0
? `${point[0]},${point[1]}`
: `${acc} ${command(point, i, a)}`, '');
};
return pointStr(points, bezierCommand);
}
const PRESET_COLOR_MAP = {
'light-blue': '#7cd6fd',
'blue': '#5e64ff',
@ -1025,6 +1069,11 @@ function datasetDot(x, y, radius, color, label='', index=0) {
function getPaths(xList, yList, color, options={}, meta={}) {
let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
let pointsStr = pointsList.join("L");
// Spline
if (options.spline)
pointsStr = createSplineCurve(xList, yList);
let path = makePath("M"+pointsStr, 'line-graph-path', color);
// HeatLine
@ -1234,13 +1283,14 @@ function animateDot(dot, x, y) {
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}
function animatePath(paths, newXList, newYList, zeroLine) {
function animatePath(paths, newXList, newYList, zeroLine, spline) {
let pathComponents = [];
let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y)).join("L");
if (spline)
pointsStr = createSplineCurve(newXList, newYList);
let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y));
let pathStr = pointsStr.join("L");
const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
const animPath = [paths.path, {d:"M" + pointsStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath);
if(paths.region) {
@ -1249,7 +1299,7 @@ function animatePath(paths, newXList, newYList, zeroLine) {
const animRegion = [
paths.region,
{d:"M" + regStartPt + pathStr + regEndPt},
{d:"M" + regStartPt + pointsStr + regEndPt},
PATH_ANIM_DUR,
STD_EASING
];
@ -1411,8 +1461,6 @@ function prepareForExport(svg) {
return container.innerHTML;
}
let BOUND_DRAW_FN;
class BaseChart {
constructor(parent, options) {
@ -1494,18 +1542,14 @@ class BaseChart {
this.height = height - getExtraHeight(this.measures);
// Bind window events
BOUND_DRAW_FN = this.boundDrawFn.bind(this);
window.addEventListener('resize', BOUND_DRAW_FN);
window.addEventListener('orientationchange', this.boundDrawFn.bind(this));
this.boundDrawFn = () => this.draw(true);
window.addEventListener('resize', this.boundDrawFn);
window.addEventListener('orientationchange', this.boundDrawFn);
}
boundDrawFn() {
this.draw(true);
}
unbindWindowEvents() {
window.removeEventListener('resize', BOUND_DRAW_FN);
window.removeEventListener('orientationchange', this.boundDrawFn.bind(this));
destroy() {
window.removeEventListener('resize', this.boundDrawFn);
window.removeEventListener('orientationchange', this.boundDrawFn);
}
// Has to be called manually
@ -2250,7 +2294,8 @@ let componentConfigs = {
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
regionFill: c.regionFill,
spline: c.spline
},
{
svgDefs: c.svgDefs,
@ -2301,7 +2346,7 @@ let componentConfigs = {
if(Object.keys(this.paths).length) {
animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline));
}
if(this.units.length) {
@ -2758,7 +2803,7 @@ function scale(val, yAxis) {
function getClosestInArray(goal, arr, index = false) {
let closest = arr.reduce(function(prev, curr) {
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
});
}, []);
return index ? arr.indexOf(closest) : closest;
}
@ -3482,6 +3527,7 @@ class AxisChart extends BaseChart {
svgDefs: this.svgDefs,
heatline: this.lineOptions.heatline,
regionFill: this.lineOptions.regionFill,
spline: this.lineOptions.spline,
hideDots: this.lineOptions.hideDots,
hideLine: this.lineOptions.hideLine,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

0
docs/assets/js/highlight.pack.js Executable file → Normal file
View File

0
docs/assets/js/index.js Executable file → Normal file
View File

File diff suppressed because one or more lines are too long

27
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "frappe-charts",
"version": "1.2.1",
"version": "1.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2973,12 +2973,6 @@
"integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
"dev": true
},
"fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3061,8 +3055,7 @@
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -3179,8 +3172,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -3222,7 +3214,6 @@
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -3241,7 +3232,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -3342,7 +3332,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -3428,8 +3417,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -3465,7 +3453,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -3529,14 +3516,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},

View File

@ -298,6 +298,7 @@ export default class AxisChart extends BaseChart {
svgDefs: this.svgDefs,
heatline: this.lineOptions.heatline,
regionFill: this.lineOptions.regionFill,
spline: this.lineOptions.spline,
hideDots: this.lineOptions.hideDots,
hideLine: this.lineOptions.hideLine,

View File

@ -368,7 +368,8 @@ let componentConfigs = {
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
regionFill: c.regionFill,
spline: c.spline
},
{
svgDefs: c.svgDefs,
@ -419,7 +420,7 @@ let componentConfigs = {
if(Object.keys(this.paths).length) {
animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline));
}
if(this.units.length) {

View File

@ -1,4 +1,4 @@
import { getBarHeightAndYAttr } from './draw-utils';
import { getBarHeightAndYAttr, getSplineCurvePointsStr } from './draw-utils';
export const UNIT_ANIM_DUR = 350;
export const PATH_ANIM_DUR = 350;
@ -74,13 +74,14 @@ export function animateDot(dot, x, y) {
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}
export function animatePath(paths, newXList, newYList, zeroLine) {
export function animatePath(paths, newXList, newYList, zeroLine, spline) {
let pathComponents = [];
let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y)).join("L");
if (spline)
pointsStr = createSplineCurve(newXList, newYList);
let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y));
let pathStr = pointsStr.join("L");
const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
const animPath = [paths.path, {d:"M" + pointsStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath);
if(paths.region) {
@ -89,7 +90,7 @@ export function animatePath(paths, newXList, newYList, zeroLine) {
const animRegion = [
paths.region,
{d:"M" + regStartPt + pathStr + regEndPt},
{d:"M" + regStartPt + pointsStr + regEndPt},
PATH_ANIM_DUR,
STD_EASING
];

View File

@ -52,4 +52,48 @@ export function shortenLargeNumber(label) {
// Correct for floating point error upto 2 decimal places
return Math.round(shortened*100)/100 + ' ' + ['', 'K', 'M', 'B', 'T'][l];
}
}
// cubic bezier curve calculation (from example by François Romain)
export function getSplineCurvePointsStr(xList, yList) {
let points=[];
for(let i=0;i<xList.length;i++){
points.push([xList[i], yList[i]]);
}
let smoothing = 0.2;
let line = (pointA, pointB) => {
let lengthX = pointB[0] - pointA[0];
let lengthY = pointB[1] - pointA[1];
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
};
};
let controlPoint = (current, previous, next, reverse) => {
let p = previous || current;
let n = next || current;
let o = line(p, n);
let angle = o.angle + (reverse ? Math.PI : 0);
let length = o.length * smoothing;
let x = current[0] + Math.cos(angle) * length;
let y = current[1] + Math.sin(angle) * length;
return [x, y];
};
let bezierCommand = (point, i, a) => {
let cps = controlPoint(a[i - 1], a[i - 2], point);
let cpe = controlPoint(point, a[i - 1], a[i + 1], true);
return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
};
let pointStr = (points, command) => {
return points.reduce((acc, point, i, a) => i === 0
? `${point[0]},${point[1]}`
: `${acc} ${command(point, i, a)}`, '');
};
return pointStr(points, bezierCommand);
}

View File

@ -1,4 +1,4 @@
import { getBarHeightAndYAttr, truncateString, shortenLargeNumber } from './draw-utils';
import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils';
import { getStringWidth } from './helpers';
import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants';
import { lightenDarkenColor } from './colors';
@ -577,6 +577,11 @@ export function datasetDot(x, y, radius, color, label='', index=0) {
export function getPaths(xList, yList, color, options={}, meta={}) {
let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
let pointsStr = pointsList.join("L");
// Spline
if (options.spline)
pointsStr = getSplineCurvePointsStr(xList, yList);
let path = makePath("M"+pointsStr, 'line-graph-path', color);
// HeatLine