@@ -30,4 +30,4 @@ | |||
"globals": { | |||
"ENV": true | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
boundDrawFn() { | |||
this.draw(true); | |||
this.boundDrawFn = () => this.draw(true); | |||
window.addEventListener('resize', this.boundDrawFn); | |||
window.addEventListener('orientationchange', this.boundDrawFn); | |||
} | |||
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, | |||
@@ -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 | |||
} | |||
} | |||
}, | |||
@@ -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, | |||
@@ -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) { | |||
@@ -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 | |||
]; | |||
@@ -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); | |||
} |
@@ -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 | |||