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": { "globals": {
"ENV": true "ENV": true
} }
} }

View File

@ -411,6 +411,50 @@ function shortenLargeNumber(label) {
return Math.round(shortened*100)/100 + ' ' + ['', 'K', 'M', 'B', 'T'][l]; 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 = { const PRESET_COLOR_MAP = {
'light-blue': '#7cd6fd', 'light-blue': '#7cd6fd',
'blue': '#5e64ff', 'blue': '#5e64ff',
@ -1025,6 +1069,11 @@ function datasetDot(x, y, radius, color, label='', index=0) {
function getPaths(xList, yList, color, options={}, meta={}) { function getPaths(xList, yList, color, options={}, meta={}) {
let pointsList = yList.map((y, i) => (xList[i] + ',' + y)); let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
let pointsStr = pointsList.join("L"); let pointsStr = pointsList.join("L");
// Spline
if (options.spline)
pointsStr = createSplineCurve(xList, yList);
let path = makePath("M"+pointsStr, 'line-graph-path', color); let path = makePath("M"+pointsStr, 'line-graph-path', color);
// HeatLine // HeatLine
@ -1234,13 +1283,14 @@ function animateDot(dot, x, y) {
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein); // 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 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)); const animPath = [paths.path, {d:"M" + pointsStr}, PATH_ANIM_DUR, STD_EASING];
let pathStr = pointsStr.join("L");
const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath); pathComponents.push(animPath);
if(paths.region) { if(paths.region) {
@ -1249,7 +1299,7 @@ function animatePath(paths, newXList, newYList, zeroLine) {
const animRegion = [ const animRegion = [
paths.region, paths.region,
{d:"M" + regStartPt + pathStr + regEndPt}, {d:"M" + regStartPt + pointsStr + regEndPt},
PATH_ANIM_DUR, PATH_ANIM_DUR,
STD_EASING STD_EASING
]; ];
@ -1411,8 +1461,6 @@ function prepareForExport(svg) {
return container.innerHTML; return container.innerHTML;
} }
let BOUND_DRAW_FN;
class BaseChart { class BaseChart {
constructor(parent, options) { constructor(parent, options) {
@ -1494,18 +1542,14 @@ class BaseChart {
this.height = height - getExtraHeight(this.measures); this.height = height - getExtraHeight(this.measures);
// Bind window events // Bind window events
BOUND_DRAW_FN = this.boundDrawFn.bind(this); this.boundDrawFn = () => this.draw(true);
window.addEventListener('resize', BOUND_DRAW_FN); window.addEventListener('resize', this.boundDrawFn);
window.addEventListener('orientationchange', this.boundDrawFn.bind(this)); window.addEventListener('orientationchange', this.boundDrawFn);
} }
boundDrawFn() { destroy() {
this.draw(true); window.removeEventListener('resize', this.boundDrawFn);
} window.removeEventListener('orientationchange', this.boundDrawFn);
unbindWindowEvents() {
window.removeEventListener('resize', BOUND_DRAW_FN);
window.removeEventListener('orientationchange', this.boundDrawFn.bind(this));
} }
// Has to be called manually // Has to be called manually
@ -2250,7 +2294,8 @@ let componentConfigs = {
c.color, c.color,
{ {
heatline: c.heatline, heatline: c.heatline,
regionFill: c.regionFill regionFill: c.regionFill,
spline: c.spline
}, },
{ {
svgDefs: c.svgDefs, svgDefs: c.svgDefs,
@ -2301,7 +2346,7 @@ let componentConfigs = {
if(Object.keys(this.paths).length) { if(Object.keys(this.paths).length) {
animateElements = animateElements.concat(animatePath( animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine)); this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline));
} }
if(this.units.length) { if(this.units.length) {
@ -2758,7 +2803,7 @@ function scale(val, yAxis) {
function getClosestInArray(goal, arr, index = false) { function getClosestInArray(goal, arr, index = false) {
let closest = arr.reduce(function(prev, curr) { let closest = arr.reduce(function(prev, curr) {
return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
}); }, []);
return index ? arr.indexOf(closest) : closest; return index ? arr.indexOf(closest) : closest;
} }
@ -3482,6 +3527,7 @@ class AxisChart extends BaseChart {
svgDefs: this.svgDefs, svgDefs: this.svgDefs,
heatline: this.lineOptions.heatline, heatline: this.lineOptions.heatline,
regionFill: this.lineOptions.regionFill, regionFill: this.lineOptions.regionFill,
spline: this.lineOptions.spline,
hideDots: this.lineOptions.hideDots, hideDots: this.lineOptions.hideDots,
hideLine: this.lineOptions.hideLine, 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", "name": "frappe-charts",
"version": "1.2.1", "version": "1.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -2973,12 +2973,6 @@
"integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
"dev": true "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": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3061,8 +3055,7 @@
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -3179,8 +3172,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -3222,7 +3214,6 @@
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -3241,7 +3232,6 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -3342,7 +3332,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -3428,8 +3417,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -3465,7 +3453,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -3529,14 +3516,12 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
} }
} }
}, },

View File

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

View File

@ -368,7 +368,8 @@ let componentConfigs = {
c.color, c.color,
{ {
heatline: c.heatline, heatline: c.heatline,
regionFill: c.regionFill regionFill: c.regionFill,
spline: c.spline
}, },
{ {
svgDefs: c.svgDefs, svgDefs: c.svgDefs,
@ -419,7 +420,7 @@ let componentConfigs = {
if(Object.keys(this.paths).length) { if(Object.keys(this.paths).length) {
animateElements = animateElements.concat(animatePath( animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine)); this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline));
} }
if(this.units.length) { 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 UNIT_ANIM_DUR = 350;
export const PATH_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); // 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 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)); const animPath = [paths.path, {d:"M" + pointsStr}, PATH_ANIM_DUR, STD_EASING];
let pathStr = pointsStr.join("L");
const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath); pathComponents.push(animPath);
if(paths.region) { if(paths.region) {
@ -89,7 +90,7 @@ export function animatePath(paths, newXList, newYList, zeroLine) {
const animRegion = [ const animRegion = [
paths.region, paths.region,
{d:"M" + regStartPt + pathStr + regEndPt}, {d:"M" + regStartPt + pointsStr + regEndPt},
PATH_ANIM_DUR, PATH_ANIM_DUR,
STD_EASING STD_EASING
]; ];

View File

@ -52,4 +52,48 @@ export function shortenLargeNumber(label) {
// Correct for floating point error upto 2 decimal places // Correct for floating point error upto 2 decimal places
return Math.round(shortened*100)/100 + ' ' + ['', 'K', 'M', 'B', 'T'][l]; 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 { getStringWidth } from './helpers';
import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants'; import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants';
import { lightenDarkenColor } from './colors'; 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={}) { export function getPaths(xList, yList, color, options={}, meta={}) {
let pointsList = yList.map((y, i) => (xList[i] + ',' + y)); let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
let pointsStr = pointsList.join("L"); let pointsStr = pointsList.join("L");
// Spline
if (options.spline)
pointsStr = getSplineCurvePointsStr(xList, yList);
let path = makePath("M"+pointsStr, 'line-graph-path', color); let path = makePath("M"+pointsStr, 'line-graph-path', color);
// HeatLine // HeatLine