lines-svg
Version:
LinesSvg is a financial chart library. Support formats are lineal, candlestick, sma & ema charts.
1,660 lines (1,432 loc) • 81 kB
JavaScript
let Lines;
(function () {
"use strict";
Lines = function (elemId) {
if (!this.getId(elemId)) {
throw new Error("Missing SVG DOM Element !");
} else if (!window.Snap) {
throw new Error("Missing Snap.svg library !");
}
this.el = this.getId(elemId);
this.snap = window.Snap("#" + elemId);
this.setupChart();
};
// Debug flag.
Lines.prototype.debug = false;
// Contstant TYPESVG
Lines.prototype.TYPESVG = {
line: "line",
text: "text",
rect: "rect",
circle: "circle",
path: "path",
input: "input"
};
// Constant TYPE of charts
Lines.prototype.TYPE = {
axis: "axis",
legend: "legend",
debug: "debug",
sinit: "sinit",
init: "init",
line: "line",
candle: "candle",
sma: "sma",
ema: "ema"
};
// Chart DOM elements
// this.elms.TYPE.elemID.elem - snapSVG element
// this.elms.TYPE.elemID.color - color for this element
Lines.prototype.elms = {
id: []
};
// Chart Live DOM elements
// lelms.TYPE.elemID >>>
// - lelems.nav["navdot1"] = element
// - lelems.line["live-line-2-3"] = element
// ID of element determine which navID belong to him
// action: {draw: true, drag: false}
Lines.prototype.lelms = {
id: [],
navid: [],
last: false,
nav: false,
line: false,
arrow: false,
action: {}
};
// Dataset
// sinit > secure init did not reset after
Lines.prototype.dset = {
sinit: {},
init: { raw: [], labels: [], min: 0, max: 0, stepX: 0, stepY: 0, zeroY: 0 },
line: { points: [], lastX: 0, lastY: 0 },
candle: { width: 0, winp: [], losep: [] },
sma: { data: [], points: [], lastX: 0, lastY: 0 },
ema: { data: [], points: [], lastX: 0, lastY: 0 }
};
// Store draw methods for tail execution during animation
// toDo - remove/combine it with cfg.DrawOrder
Lines.prototype.drawOrder = [];
// Configuration section:
// - chart = properties for the chart.
// - - padding: use to calculate drawable area for chart.
// usable width = totalWidth - 2 * padding
// - - candleFill is ratio candle width is 0.4 part from x step
// - cssClass = classes for each chart type.
Lines.prototype.cfg = {
animate: false,
zoomMove: true,
chart: {
type: ["line", "candle", "sma", "ema"],
padding: 30,
attr: { stroke: "#ddd", fill: "none", strokeWidth: 1 },
textAttr: { "stroke-width": "0.1px", "font-family": "Verdana", "font-size": "12px", fill: "#000" },
textBold: { "font-weight": "bold" },
enableGrid: true,
candleFill: 0.4,
grids: 5,
navDot: 6 // radius
},
cssClass: {
textLabel: "tlabel",
liveLabel: "llabel",
liveLine: "lline",
liveDot: "ldot",
winCandle: "wcandle",
loseCandle: "lcandle",
debugDot: "ddot",
line: "stline",
sma: "stsma",
ema: "stema",
axis: "staxis",
navDot: "stnavdot",
moveNavDot: "movedot",
rotateNavDot: "rotatedot"
},
smaLength: 5,
emaLength: 10,
magnetMode: 50,
step: {
x: 50,
xMin: 20,
xMax: 100,
yMax: 20,
arrow: 50,
zoom: 9,
offset: 9,
xLegend: 100
},
debug: {
radius: 6,
attr: { stroke: "red" }
},
timeUnit: "15m",
timeUnits: ["15m", "30m", "1h", "4h", "1d", "1w"], //supported TIME UNITS
drawOrder: ["drawLine", "drawCandle", "drawSMA", "drawEMA"]
};
// Internal method
// - define chartArea which hold main chart dimensions
Lines.prototype.setupChart = function () {
var elementStyle, width, height;
if (!window.getComputedStyle) {
return;
}
elementStyle = window.getComputedStyle(this.el);
width = parseInt(elementStyle.width);
height = parseInt(elementStyle.height);
this.chartArea = {
w: width,
h: height,
width: (width - (this.cfg.chart.padding * 2)),
height: (height - (this.cfg.chart.padding * 2)),
endX: this.cfg.chart.padding,
zeroX: this.cfg.chart.padding,
zeroY: height - this.cfg.chart.padding,
offsetLeft: this.el.offsetLeft || this.el.parentElement.offsetLeft || 0,
offsetTop: this.el.offsetTop || this.el.parentElement.offsetTop || 0
};
// invert the chart start from left to right
this.chartArea.zeroX = this.chartArea.width;
// check if snap exist. for testing purposes
this.snap && this.snap.attr(this.cfg.chart.attr);
if (this.snap && this.cfg.zoomMove) {
this.chartEvents();
}
};
Lines.prototype.events = {
wheel: false
};
// bind global Events for whole chart
// mouseWheel
// mouse Drag
// once Per interaction throught debounce
Lines.prototype.chartEvents = function () {
var _debounce, self = this;
_debounce = function (cb, wait) {
var timeout;
return function () {
var action = {},
args = arguments;
action.draw = self.gl({ type: "action", prop: "draw" });
action.drag = self.gl({ type: "action", prop: "drag" });
//check draw or drag
if (action.draw || action.drag) {
return;
}
arguments[0].preventDefault && arguments[0].preventDefault();
var later = function () {
timeout = null;
cb.apply(self, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
if (!this.cfg.zoomMove) {
this.el.removeEventListener("wheel", _debounce(this.cbEvent, 300), false);
return;
}
if (!this.events.wheel) {
this.events.wheel = true;
this.el.addEventListener("wheel", _debounce(this.cbEvent, 300), false);
}
if (!this.events.drag) {
this.events.drag = true;
this.snap.drag(_debounce(this.cbEvent, 300));
}
};
// set config parameters
// support deep config
Lines.prototype.setup = function (setupData) {
var prop1, prop2;
for (prop1 in setupData) {
if (typeof setupData[prop1] === "object") {
for (prop2 in setupData[prop1]) {
if (this.cfg[prop1][prop2]) {
this.cfg[prop1][prop2] = setupData[prop1][prop2];
}
}
} else if (this.cfg[prop1]) {
this.cfg[prop1] = setupData[prop1];
}
}
this.setupChart();
};
// for wheel -> wheelDelta >>> zoom
// for drag -> dx, dy, posx, posy >>> move
Lines.prototype.cbEvent = function () {
var param;
if (arguments[0] && (arguments[0].wheelDelta || arguments[0].deltaY)) {
param = arguments[0].wheelDelta || arguments[0].deltaY;
this.zoom(param > 0 ? "in" : "out");
} else if (arguments.length === 5) {
param = arguments[0];
this.move(param > 0 ? "prev" : "next");
}
};
// Interface method
// check dataArray, handle dataArray
Lines.prototype.data = function (dataArray) {
if (!(dataArray instanceof Array)) {
return;
}
var labels, validData = [];
if (dataArray[0].date || dataArray[0].high) {
validData = this.formatData(dataArray);
labels = validData.map(item => item[4]);
} else {
validData = dataArray;
//remove this !!!
// validData = dataArray.reverse();
labels = validData.map(item => item[4]);
}
//store whole array into this.dset.init.raw
this.s({ type: this.TYPE.init, prop: "raw" }, validData);
if (labels) {
this.s({ type: this.TYPE.init, prop: "labels" }, labels);
}
// get close values for our MAIN DATA array
// OHLC - Open High Low Close
var data = validData.map(item => this.f(item[3], 5));
this.s({ type: this.TYPE.line, prop: "data" }, data);
//recover values from sinit
this.restore();
if (this.checkLen()) {
this.dataInit();
this.calculate();
}
};
//[{open, high, low, close}]
Lines.prototype.formatData = function (dataRaw) {
var validData;
validData = dataRaw.map(row => [row.open,
row.high,
row.low,
row.close,
row.date
]);
return validData;
};
// Internal method
// - check data length
// if data is more than available slots slice it
// start again
// toDo create pagging
Lines.prototype.checkLen = function () {
var data, stepX, perPage;
data = this.gg("data");
stepX = this.gg("stepX") || this.f((this.chartArea.width / data.length), 0);
if (stepX > this.cfg.step.xMax) {
stepX = this.cfg.step.xMax;
} else if (stepX < this.cfg.step.xMin) {
stepX = this.cfg.step.xMin;
}
this.s({ type: this.TYPE.init, prop: "stepX" }, stepX);
perPage = this.f(this.chartArea.width / stepX, 0);
if (stepX < 50) perPage -= 1;
if (data.length > perPage) {
var offset, cuttedData, raw, slice = {};
raw = this.gg("raw");
this.s({ type: this.TYPE.sinit, prop: "allraw" }, raw);
this.s({ type: this.TYPE.sinit, prop: "labels" }, this.gg("labels"));
offset = this.g({ type: this.TYPE.sinit, prop: "offset" }) || 0;
slice = {
begin: (offset > 0) ? offset : 0,
end: perPage + offset
};
// slice
cuttedData = raw.slice(slice.begin, slice.end);
this.s({ type: this.TYPE.sinit, prop: "slice" }, slice);
this.reset();
this.data(cuttedData);
return false;
}
return true;
};
// initialize data set
// calculate min, max value based on input data
Lines.prototype.dataInit = function () {
var data, min, max;
data = this.gg("data");
min = max = data[0];
data.map(item => {
if (min > item)
min = item;
if (max < item)
max = item;
});
this.s({ type: this.TYPE.init, prop: "min" }, min);
this.s({ type: this.TYPE.init, prop: "max" }, max);
this.periodInit();
this.zeroInit();
};
// calc stepY
Lines.prototype.periodInit = function () {
var step = {},
amplitude;
amplitude = this.f((this.gg("max") - this.gg("min")), 5);
this.s({ type: this.TYPE.init, prop: "amplitude" }, amplitude);
step.y = this.f((this.chartArea.height / amplitude), 0);
this.s({ type: this.TYPE.init, prop: "stepY" }, step.y); //stepY - important !!!
};
//define zeroX & zeroY
Lines.prototype.zeroInit = function () {
var data, zeroY;
data = this.gg("data");
this.s({ type: this.TYPE.init, prop: "zeroX" }, this.chartArea.zeroX);
zeroY = this.chartArea.zeroY - ((data[0] - this.gg("min")) * this.gg("stepY")); //???
zeroY = this.f(zeroY, 0);
this.s({ type: this.TYPE.init, prop: "zeroY" }, zeroY);
};
// CALCULATE LINE CHART AXIS POINTS //////////
// calculate axis points and store it into lines.points
// pointOne = [axisX, axisY]
// this.TYPE.points = [[x0,y0], [x1, y1].....[x88,y88]]
Lines.prototype.calculate = function () {
var data, pointLength, pIndex = 1,
pointOne = [];
data = this.gg("data");
pointOne[0] = this.gg("zeroX");
pointOne[1] = this.gg("zeroY");
// clear points and assign pointOne [axisX, axisY]
this.s({ type: this.TYPE.line, prop: "points" }, []);
this.add({ type: this.TYPE.line, prop: "points" }, pointOne);
this.s({ type: this.TYPE.line, prop: "lastX" }, pointOne[0]);
this.s({ type: this.TYPE.line, prop: "lastY" }, pointOne[1]);
pointLength = data.length;
for (; pIndex < pointLength; pIndex++) {
(this.calcPoint)(pIndex);
}
};
/**
* calcPoint.
* @desc Calculate point Axis base on value
*
* @param {number} dataIndex - The index of the point.
* @param {string} chartType - Chart type.
* @param {string} propPostfix - String which add at the end of property.
*/
Lines.prototype.calcPoint = function (dataIndex, chartType = "line", propPostfix = "") {
var prevDataValue, dataValue, diff, params = {};
params.type = chartType;
params.prop = "data" + propPostfix;
prevDataValue = this.g(params)[dataIndex - 1];
dataValue = this.g(params)[dataIndex];
if (dataValue < prevDataValue) {
diff = prevDataValue - dataValue;
this.plus(diff, chartType, propPostfix);
} else if (dataValue > prevDataValue) {
diff = dataValue - prevDataValue;
this.minus(diff, chartType, propPostfix);
} else {
this.equal(dataValue, chartType, propPostfix);
}
};
/**
* plus Method.
* @desc increase lastX with stepX
* @desc increase lastY with (increase * stepY)
*
* @param {number} increase - The difference from previous point data.
* @param {string} chartType - Chart type from TYPE.
* @param {string} propPostfix - String which add at the end of property.
*
* calculated point add to points/pointsLength to corresponding chartType
*/
Lines.prototype.plus = function (increase, chartType = "line", propPostfix) {
var point = [],
change = {};
change.x = this.gg("stepX");
point[0] = this.action({ type: chartType, prop: "lastX" + propPostfix }, { action: "-", value: change.x });
change.y = this.f((increase * this.gg("stepY")), 5);
point[1] = this.action({ type: chartType, prop: "lastY" + propPostfix }, { action: "+", value: change.y });
this.add({ type: chartType, prop: "points" + propPostfix }, point);
};
/**
* minus Method.
* @desc increase lastX with stepX
* @desc decrease lastY with (decrease * stepY)
*
* @param {number} decrease - The difference from previous point data.
* @param {string} chartType - Chart type from TYPE.
* @param {string} propPostfix - String which add at the end of property.
*
* calculated point add to points/pointsLength to corresponding chartType
*/
Lines.prototype.minus = function (decrease, chartType = "line", propPostfix) {
var point = [],
change = {};
change.x = this.gg("stepX");
point[0] = this.action({ type: chartType, prop: "lastX" + propPostfix }, { action: "-", value: change.x });
change.y = this.f((decrease * this.gg("stepY")), 5);
point[1] = this.action({ type: chartType, prop: "lastY" + propPostfix }, { action: "-", value: change.y });
this.add({ type: chartType, prop: "points" + propPostfix }, point);
};
/**
* equal Method.
* @desc increase lastX with stepX
* @desc did not change lastY
*
* @param {number} decrease - The difference from previous point data.
* @param {string} chartType - Chart type from TYPE.
* @param {string} propPostfix - String which add at the end of property.
*
* calculated point add to points/pointsLength to corresponding chartType
*/
Lines.prototype.equal = function (value, chartType = "line", propPostfix) {
var point = {},
change = {};
change.x = this.gg("stepX");
point[0] = this.action({ type: chartType, prop: "lastX" + propPostfix }, { action: "-", value: change.x });
point[1] = this.g({ type: chartType, prop: "lastY" + propPostfix });
this.add({ type: chartType, prop: "points" + propPostfix }, point);
};
/**
* add Method.
* @desc get data property if empty create
* add new data and set
*
* @param {object} addInfo - {type: this.TYPE, prop: "points" || "data"}.
* @param {number} addData - data to push into array.
*
*/
Lines.prototype.add = function (addInfo, addData) {
var points;
points = this.g(addInfo) || [];
points.push(addData);
this.s(addInfo, points);
};
/*
_ _ _ _
_ __ ___ _ __ __| | ___ _ __ _ __ ___ ___| |_| |__ ___ __| |___
| '__/ _ \ '_ \ / _` |/ _ \ '__| | '_ ` _ \ / _ \ __| '_ \ / _ \ / _` / __|
| | | __/ | | | (_| | __/ | | | | | | | __/ |_| | | | (_) | (_| \__ \
|_| \___|_| |_|\__,_|\___|_| |_| |_| |_|\___|\__|_| |_|\___/ \__,_|___/
*/
Lines.prototype.draw = function (type = "all", chartLength) {
switch (type) {
case this.TYPE.axis:
this.drawAxis();
break;
case this.TYPE.line:
this.drawLine();
break;
case this.TYPE.candle:
this.drawCandle();
break;
case this.TYPE.sma:
// this.drawOrder = ["drawLegend"];
this.drawSMA(chartLength);
break;
case "ema":
this.drawEMA(chartLength);
break;
case "all":
if (this.cfg.animate) {
this.drawOrder = this.cfg.drawOrder.slice(0);
this.drawAxis();
} else {
this.drawAxis();
this.drawLine();
this.drawCandle();
this.drawSMA();
this.drawEMA();
this.drawLegend();
}
}
};
/**
* drawSVG Method.
* @desc draw snap svg element
*
* @param {object} svgInfo - {type: this.TYPESVG, axis: [], attr: {}, id, class}.
*
* @return {object} svgObject
*/
Lines.prototype.drawSVG = function (svgInfo = {}) {
var svgData = {};
switch (svgInfo.type) {
case this.TYPESVG.rect:
svgData.args = {
x: svgInfo.axis[0],
y: svgInfo.axis[1],
width: svgInfo.w,
height: svgInfo.h
};
break;
case this.TYPESVG.text:
svgData.args = {
x: svgInfo.axis[0],
y: svgInfo.axis[1],
text: svgInfo.text
};
break;
case this.TYPESVG.circle:
svgData.args = {
cx: svgInfo.axis[0],
cy: svgInfo.axis[1],
r: svgInfo.r
};
break;
}
// add class to attributes
if (svgInfo.class) {
svgInfo.attr = svgInfo.attr || {};
svgInfo.attr.class = svgInfo.class;
}
//draw Element
svgData.element = this.snap[svgInfo.type](svgData.args);
svgInfo.id && (svgData.element.node.id = svgInfo.id);
//add attributes to svg element
if (svgInfo.attr) {
svgData.attr = {};
for (var attrKey in svgInfo.attr) {
svgData.attr[attrKey] = svgInfo.attr[attrKey];
}
svgData.element.attr(svgData.attr);
}
return svgData.element;
};
/**
* check Method.
* @desc get data property if empty create
* add new data and set
*
* @param {object} elementInfo - {type: this.TYPE}.
*
*/
Lines.prototype.checkId = function (elementInfo) {
var elemID;
elemID = this.makeId(elementInfo);
return (this.elms.id.indexOf(elemID) !== -1);
};
Lines.prototype.redraw = function (idArray) {
var chart, key, fnName;
idArray = idArray || this.elms.id;
for (key in idArray) {
chart = this.splitId(idArray[key]);
fnName = "draw" + chart.type;
this[fnName](chart.length || 0);
}
};
Lines.prototype.getLabel = function (elemID) {
var label = elemID.split("-");
label.shift(); //remove "svg"
return label.join(" ");
};
// only for SMA & EMA with period
//dset.sma.length
//dset.ema
//toDo review this...
Lines.prototype.drawLegend = function () {
var yAxis, smaID, emaID;
yAxis = this.chartArea.zeroY - 20;
for (smaID in this.elms.sma) {
this.prLegend({
id: smaID,
y: yAxis,
label: this.getLabel(smaID),
color: this.elms.sma[smaID].color || this.getStyle(smaID).stroke
});
yAxis -= 30;
}
for (emaID in this.elms.ema) {
this.prLegend({
id: emaID,
y: yAxis,
label: this.getLabel(emaID),
color: this.elms.ema[emaID].color || this.getStyle(emaID).stroke
});
yAxis -= 30;
}
};
// rowObj {y, label, color}
Lines.prototype.prLegend = function (rowObj) {
var svg = {},
cube = {};
cube.x = this.chartArea.w - this.cfg.step.xLegend;
cube.y = rowObj.y;
cube.width = cube.height = 20;
svg.cubeInfo = {
type: this.TYPESVG.rect,
axis: [cube.x, cube.y],
w: cube.width,
h: cube.height,
attr: { fill: rowObj.color }
};
svg.cube = this.drawSVG(svg.cubeInfo);
svg.textInfo = {
type: this.TYPESVG.text,
axis: [cube.x + 30, cube.y + 15],
text: rowObj.label,
attr: { stroke: rowObj.color }
};
svg.text = this.drawSVG(svg.textInfo);
this.store({ type: this.TYPE.legend }, svg.cube);
this.store({ type: this.TYPE.legend }, svg.text);
svg.text.click(() => {
alert("click Legend" + rowObj.id);
console.log("QUICK ANIMATION OF THE CHART WITH SOME BLANK COLOR >>>>", this);
});
};
//draw empty Grid behind chart
Lines.prototype.drawAxis = function () {
var lineAxis = [],
yAxis, _linePath = "",
gridStep, lk = 0;
if (this.checkId({ type: this.TYPE.axis })) {
this.pr("Axis chart already exist");
return;
}
if (!this.cfg.chart.enableGrid) {
this.drawLabelsX();
this.drawLabelsY();
return;
}
gridStep = this.chartArea.height / this.cfg.chart.grids;
this.s({ type: this.TYPE.init, prop: "gridstep" }, this.f(gridStep, 0));
lineAxis[0] = [this.chartArea.endX, this.chartArea.zeroY];
lineAxis[1] = [this.chartArea.zeroX, this.chartArea.zeroY];
yAxis = lineAxis[0][1] = lineAxis[1][1];
for (; lk <= this.cfg.chart.grids; lk++) {
_linePath += "M" + lineAxis[0][0] + " " + yAxis + "L" + lineAxis[1][0] + " " + yAxis;
yAxis = Math.floor(yAxis - gridStep);
}
this.printPath({ type: this.TYPE.axis, path: _linePath });
this.drawLabelsX();
this.drawLabelsY();
};
Lines.prototype.drawLabelsX = function () {
var xAxis, lineAxis = [
[]
],
plen, _linePath = "",
svg, lk = 0,
diff = 0,
xlabel;
plen = this.gg("points").length;
// lineAxis[0][0] = this.chartArea.endX + this.gg("stepX");
lineAxis[0][0] = this.chartArea.zeroX;
lineAxis[0][1] = Math.floor(this.chartArea.zeroY + this.cfg.chart.padding / 2);
lineAxis[1] = [lineAxis[0][0], lineAxis[0][1] + 5];
xAxis = lineAxis[0][0] = lineAxis[1][0];
if (this.cfg.timeUnit === "4h") {
diff = 1;
}
for (; lk <= plen; lk++) {
if ((lk % 3) === diff) {
xlabel = this.dateTime(lk);
_linePath += "M" + xAxis + " " + lineAxis[0][1] + "L" + xAxis + " " + lineAxis[1][1];
svg = this.snap.text(xAxis - 5, lineAxis[0][1] + 15, xlabel);
svg.attr({ class: this.cfg.cssClass.textLabel });
svg.attr(this.cfg.chart.textAttr);
switch (this.cfg.timeUnit) {
case "15m":
(xlabel.length === 2) && svg.attr(this.cfg.chart.textBold);
break;
}
// if ((xlabel.length === 1 || xlabel.length === 2 || xlabel.length === 3) && this.cfg.timeUnit !== "1w" && this.cfg.timeUnit !== "1d") {
// svg.attr(this.cfg.chart.textBold);
// } else if (xlabel.length > 2 && this.cfg.timeUnit !== "1h" && this.cfg.timeUnit !== "15m") {
// svg.attr(this.cfg.chart.textBold);
// }
this.store({ type: this.TYPE.axis }, svg);
}
xAxis -= this.gg("stepX");
}
this.printPath({ type: this.TYPE.axis, path: _linePath });
};
Lines.prototype.drawLabelsY = function () {
var svg, txtStep, label, step, point;
label = this.gg("min");
txtStep = this.f(this.gg("amplitude") * (1 / this.cfg.chart.grids), 4);
step = this.gg("gridstep");
point = [this.chartArea.zeroX, this.chartArea.zeroY];
for (var tK = 0; tK <= this.cfg.chart.grids; tK++) {
svg = this.snap.text(point[0], point[1], this.f(label, 4));
svg.attr({ class: this.cfg.cssClass.textLabel });
svg.attr(this.cfg.chart.textAttr);
this.store({ type: this.TYPE.axis }, svg);
this.drawDebug(point);
point[1] -= step;
label += txtStep;
}
};
// cuttedData = raw.slice(slice.begin, slice.end);
Lines.prototype.dateTime = function (period) {
var periodInfo = {};
periodInfo.key = period + (this.gg("offset") || 0);
periodInfo.value = this.gg("labels")[periodInfo.key];
return this.formatDate(periodInfo.value);
};
// 1. calculate whole length width/stepX
// 2. calculate one Period depend on cfg.timeUnit
// 3. calculate begining of chart
//
// timeUnits: ["15m", "30m", "1h", "4h", "1d", "1w"], //supported TIME UNITS
Lines.prototype.dateTimeOld = function (period) {
var periodLength, timeLine = {};
periodLength = this.chartArea.width / this.gg("stepX");
periodLength = this.f(periodLength, 0) - 1;
//add or subtrack offset
periodLength += this.g({ type: this.TYPE.init, prop: "offset" }) || 0;
timeLine.perPeriod = this.dateUnit(); // in milliseconds
if (period === periodLength) {
timeLine.end = (new Date).getTime();
} else {
//should round end time
// if it is 10.12 subtrack 12 min to become 10
timeLine.end = (new Date).getTime();
timeLine.diff = timeLine.end % timeLine.perPeriod;
timeLine.end -= timeLine.diff;
}
timeLine.begin = timeLine.end - (timeLine.perPeriod * periodLength); //start scale
timeLine.thisPeriod = timeLine.begin + (period * timeLine.perPeriod);
return this.formatDate(timeLine.thisPeriod);
};
Lines.prototype.formatDate = function (timeStamp) {
var base, dateFormat, months;
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
base = new Date(timeStamp);
if (["15m", "30m"].indexOf(this.cfg.timeUnit) !== -1) {
dateFormat = base.getHours();
if (dateFormat === 0 && base.getMinutes() < 31) {
dateFormat = base.getDate();
} else {
dateFormat += ":" + base.getMinutes();
}
} else if (["1h"].indexOf(this.cfg.timeUnit) !== -1) {
dateFormat = base.getHours();
if (dateFormat === 0 || dateFormat === 1 || dateFormat === 2) {
dateFormat = base.getDate();
} else {
dateFormat += ":00";
}
} else if (["4h"].indexOf(this.cfg.timeUnit) !== -1) {
dateFormat = base.getHours();
if ([0, 1, 2, 3].indexOf(dateFormat) !== -1) {
dateFormat = base.getDate();
} else {
dateFormat += ":00";
}
} else if (["1d"].indexOf(this.cfg.timeUnit) !== -1) {
if ([1, 2, 3].indexOf(base.getDate()) !== -1) {
dateFormat = months[base.getMonth()];
} else {
dateFormat = base.getDate();
}
// dateFormat += " " + base.getHours();
} else if (["1w"].indexOf(this.cfg.timeUnit) !== -1) {
if ([1, 2, 3, 4, 5].indexOf(base.getDate()) !== -1) {
dateFormat = months[base.getMonth()];
} else {
dateFormat = base.getDate();
}
}
//convert to string
return dateFormat + "";
};
// return one period unit(milisecconds) for current period
Lines.prototype.dateUnit = function () {
var oneMin, unit;
oneMin = 60000; //60
switch (this.cfg.timeUnit) {
case "5m":
unit = 5 * oneMin;
break;
case "15m":
unit = 15 * oneMin;
break;
case "30m":
unit = 30 * oneMin;
break;
case "1h":
unit = 60 * oneMin;
break;
case "4h":
unit = 4 * 60 * oneMin;
break;
case "1d":
unit = 24 * 60 * oneMin;
break;
case "1w":
unit = 7 * 24 * 60 * oneMin;
break;
}
return unit;
};
//calculate hours;
//in minute
//start date
//start hour
Lines.prototype.labelX = function (step) {
var label, res = "";
label = 840; //14 why 14 ???
switch (this.cfg.timeUnit) {
case "5m":
label += 5 * step;
break;
case "15m":
label += 15 * step;
break;
case "30m":
label += 30 * step;
break;
case "1h":
label += 60 * step;
break;
case "4h":
label += 4 * 60 * step;
break;
case "1d":
label += 24 * 60 * step;
break;
case "1w":
label += 7 * 24 * 60 * step;
break;
}
//days
if (label / 1440 > 1) {
res += Math.floor(label / 1440) + "days ";
label = label % 1440;
}
res += Math.floor(label / 60) + (label % 60) / 100;
return res;
};
//////////////////////////////////// DRAW FUNCTIONS >>>
// add snap.path to store
Lines.prototype.drawLine = function () {
var points, plen, pkey = 0,
_linePath = "",
lineAxis = [];
if (this.checkId({ type: this.TYPE.line })) {
this.pr("Line chart already exist");
return;
}
points = this.gg("points");
plen = points.length - 1;
for (; pkey < plen; pkey++) {
lineAxis.push([points[pkey][0], points[pkey][1]]); // x1, y1
lineAxis.push([points[pkey + 1][0], points[pkey + 1][1]]); // x2, y2
_linePath += this.getPath(lineAxis);
this.drawDebug(lineAxis[0]);
lineAxis = [];
}
this.drawDebug(points[plen]); //last dot
this.printPath({ type: this.TYPE.line, path: _linePath });
};
//Material 400
Lines.prototype.getHex = function () {
var colors = ["#ef5350", "#ec407a", "#ab47bc", "#7e57c2", "#5c6bc0", "#42a5f5", "#26a69a", "#66bb6a", "#9ccc65"];
colors = colors.concat(["#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#1565c0", "#2e7d32", "#558b2f"]);
return colors[Math.floor(Math.random() * colors.length)];
};
// animate or not
// https://codepen.io/JRGould/pen/dkHhw
Lines.prototype.printPath = function (lineProp) {
if (this.cfg.animate) {
this.animatePath(lineProp, () => {
if (this.drawOrder[0]) {
var callFunc = this.drawOrder.shift();
this[callFunc]();
}
});
} else {
this.directPath(lineProp);
}
};
// only sma & ema charts can be draw more than once
Lines.prototype.exist = function (chartType) {
if ([this.TYPE.sma, this.TYPE.ema].indexOf(chartType) === -1) {
return false;
}
return !!this.elms[chartType];
};
Lines.prototype.directPath = function (lineProp) {
var svg, svgAttr = {},
elemID, color, storedColor = {};
svg = this.snap.path(lineProp.path);
svgAttr.class = lineProp.class || this.cfg.cssClass[lineProp.type];
svg.attr(svgAttr);
// handle repeating sma & ema charts 5 -- 20 -- 50
if (this.exist(lineProp.type)) {
elemID = this.makeId(lineProp);
if (this.g({ type: this.TYPE.sinit, prop: "color" })) {
color = this.g({ type: this.TYPE.sinit, prop: "color" })[elemID];
} else {
color = this.getHex();
}
svg.attr({ style: "stroke: " + color });
this.store({ type: lineProp.type, length: lineProp.length, prop: "color" }, color);
storedColor[elemID] = color;
this.s({ type: this.TYPE.sinit, prop: "color" }, storedColor);
this.drawLegend();
}
// axis & candle NEED group
// if ([this.TYPE.axis, this.TYPE.candle].indexOf(lineProp.type) !== -1) {
if (lineProp.type !== this.TYPE.axis && lineProp.type !== this.TYPE.candle) {
lineProp.single = true;
}
this.store(lineProp, svg);
};
//lineProp = {type; path; color; width; strokeDasharray}
// toDo - remove animation as it is now
// replace it with draw White chart and animate after that
// imidiatly set all chart properties
Lines.prototype.animatePath = function (lineProp, cbNext) {
var svg, lineLen, svgAttr = {};
svgAttr.class = lineProp.class || this.cfg.cssClass[lineProp.type];
svg = this.snap.path(svgAttr);
lineLen = Snap.path.getTotalLength(lineProp.path);
/* beautify preserve:start */
Snap.animate(0, lineLen, step => {
svg.attr({
path: Snap.path.getSubpath(lineProp.path, 0, step),
strokeWidth: lineProp.width || 1
});
},
800, //duration
mina.backOut,
() => {
if (this.exist(lineProp.type)) {
var color = this.getHex();
svg.attr({ style: "stroke: " + color });
this.store({ type: lineProp.type, length: lineProp.length, prop: "color" }, color);
this.drawLegend();
}
//store single or group
if ([this.TYPE.axis, this.TYPE.candle].indexOf(lineProp.type) !== -1) {
this.store({ type: lineProp.type, length: lineProp.length }, svg);
} else {
this.store({ type: lineProp.type, length: lineProp.length, single: true }, svg);
}
cbNext && cbNext();
});
/* beautify preserve:end */
};
Lines.prototype.getPath = function (lineAxis) {
if (lineAxis.length !== 2) {
return;
}
var path = "M" + Math.floor(lineAxis[0][0]) + " " + Math.floor(lineAxis[0][1]);
path += "L" + Math.floor(lineAxis[1][0]) + " " + Math.floor(lineAxis[1][1]);
return path;
};
// start with
// 1. drawCandle
// 2. candle
// 3. candleShadow
Lines.prototype.drawCandle = function () {
var width, raw, rkey;
if (this.checkId({ type: this.TYPE.candle })) {
this.pr("Candle chart already exist");
return;
}
width = this.gg("stepX") * this.cfg.chart.candleFill;
this.s({ type: this.TYPE.candle, prop: "width" }, this.f(width, 0));
raw = this.gg("raw");
for (rkey in raw) {
(key => {
var cdata = {
open: raw[key][0],
high: raw[key][1],
low: raw[key][2],
close: raw[key][3],
};
(key > 0) && this.candle(cdata, key);
})(rkey);
}
};
Lines.prototype.candle = function (candleData, period) {
var axis = [],
candle = {};
axis[0] = this.gg("points")[period - 1];
axis[1] = this.gg("points")[period];
candle.data = candleData;
candle.height = this.f(axis[0][1] - axis[1][1], 0);
candle.width = this.g({ type: this.TYPE.candle, prop: "width" });
candle.x = axis[0][0] + ((1 - this.cfg.chart.candleFill) / 2 * this.gg("stepX"));
candle.x = this.f(candle.x, 0);
candle.x -= this.gg("stepX"); //invert chart fix
if (axis[0][1] > axis[1][1]) {
candle.class = this.cfg.cssClass.loseCandle;
candle.y = axis[1][1];
} else {
candle.class = this.cfg.cssClass.winCandle;
candle.y = axis[0][1];
}
candle.info = {
type: this.TYPESVG.rect,
axis: [candle.x, candle.y],
w: candle.width,
h: candle.height,
class: candle.class
};
candle.element = this.drawSVG(candle.info);
this.store({ type: this.TYPE.candle }, candle.element);
this.candleShadow(candle, period);
};
/* candleShadow - top & bottom
*
* @param {type} candle {data:{open, high, low, close}, width, height, x, y}
* @param {type} color string
*/
Lines.prototype.candleShadow = function (candle, period) {
var axisX, lineTop, lineBot, _linePath = "";
/* beautify preserve:start */
lineTop = lineBot = [[], []];
/* beautify preserve:end */
axisX = candle.x + this.f((candle.width / 2), 0);
lineTop[0] = [axisX, candle.y];
lineTop[1][0] = axisX;
lineTop[1][1] = (candle.data.high - candle.data.close) * this.gg("stepY");
lineTop[1][1] = lineTop[0][1] - lineTop[1][1];
_linePath += this.getPath(lineTop);
lineBot[0][0] = axisX;
lineBot[0][1] = lineTop[0][1] + candle.height;
lineBot[1][0] = axisX;
lineBot[1][1] = lineBot[0][1] + this.f(((candle.data.close - candle.data.low) * this.gg("stepY")), 0);
_linePath += this.getPath(lineBot);
//store linePath into array make ALL candle shadows with only TWO DOM elements
if (candle.class === this.cfg.cssClass.winCandle) {
this.add({ type: this.TYPE.candle, prop: "winp" }, _linePath);
} else {
this.add({ type: this.TYPE.candle, prop: "losep" }, _linePath);
}
if (parseInt(period) + 1 === this.gg("data").length) {
let shadowPath;
shadowPath = this.g({ type: this.TYPE.candle, prop: "winp" });
this.printPath({ type: this.TYPE.candle, path: shadowPath.join(" "), class: this.cfg.cssClass.winCandle });
shadowPath = this.g({ type: this.TYPE.candle, prop: "losep" });
this.printPath({ type: this.TYPE.candle, path: shadowPath.join(" "), class: this.cfg.cssClass.loseCandle });
}
};
// a0+a1+ ....a9 / 10 = sma9
// a1+a2+ ....a10 / 10 = sma10
Lines.prototype.calcSMA = function (smaLength) {
var data, dLen, len, total = 0,
dKey = 0;
data = this.gg("data");
//calc missing part of SMA
if (this.g({ type: this.TYPE.sinit, prop: "allraw" })) {
var bdata = this.g({ type: this.TYPE.sinit, prop: "allraw" });
var slice = this.g({ type: this.TYPE.sinit, prop: "slice" });
bdata = bdata.slice(slice.begin - smaLength, slice.begin);
bdata = bdata.map(v => v[3]);
data = bdata.concat(data); //merge bdata & data
}
dLen = data.length;
//sma period length by configuration
len = smaLength || this.cfg.smaLength - 1;
for (; dKey < dLen; dKey++) {
total += data[dKey];
if (dKey >= len) {
if (dKey > len) {
total -= data[(dKey - (len + 1))];
}
this.add({ type: this.TYPE.sma, prop: "data" + len }, this.f(total / (len + 1)));
}
}
this.initSMA(len);
};
//define lastX & lastY for default SMA
Lines.prototype.initSMA = function (smaLength) {
var data, key = 1,
len, lastAxis = [];
data = this.g({ type: this.TYPE.sma, prop: "data" + smaLength });
if (this.g({ type: this.TYPE.sinit, prop: "allraw" })) {
lastAxis[0] = this.gg("zeroX");
} else {
lastAxis[0] = this.gg("zeroX") + (smaLength * this.gg("stepX"));
lastAxis[0] = this.f(lastAxis[0], 0);
}
this.s({ type: this.TYPE.sma, prop: "lastX" + smaLength }, lastAxis[0]);
lastAxis[1] = this.chartArea.zeroY - ((data[0] - this.gg("min")) * this.gg("stepY")); //???
lastAxis[1] = this.f(lastAxis[1], 0);
this.s({ type: this.TYPE.sma, prop: "lastY" + smaLength }, lastAxis[1]);
this.s({ type: this.TYPE.sma, prop: "points" + smaLength }, []); // clear
this.add({ type: this.TYPE.sma, prop: "points" + smaLength }, lastAxis); // point 0
len = data.length;
for (; key < len; key++) {
(this.calcPoint)(key, this.TYPE.sma, smaLength);
}
};
Lines.prototype.drawSMA = function (smaLength) {
var points, plen, key = 0,
lineAxis = [],
_linePath = "";
smaLength = smaLength || this.cfg.smaLength;
points = this.g({ type: this.TYPE.sma, prop: "points" + smaLength });
if (this.gg("data").length < smaLength) {
return;
} else if (this.checkId({ type: this.TYPE.sma, length: smaLength })) {
this.pr("SMA chart already exist");
return;
}
points && (plen = points.length);
if (!plen) {
this.calcSMA(smaLength);
this.drawSMA(smaLength);
return;
} else if (plen < smaLength && 1 === 2) {
this.pr("SMA can NOT exist insufficient points");
return;
}
for (; key < (plen - 1); key++) {
lineAxis.push([points[key][0], points[key][1]]);
lineAxis.push([points[key + 1][0], points[key + 1][1]]);
_linePath += this.getPath(lineAxis);
this.drawDebug(lineAxis[0]);
lineAxis = [];
}
this.drawDebug(lineAxis[plen]);
this.printPath({ type: this.TYPE.sma, path: _linePath, length: smaLength });
};
// EMA(today) = Price(today) * K + EMA(yesterday) * (1-K)
// EMAtoday = Ptoday * k + EMAyest * k2
// K = 2 / (length + 1)
Lines.prototype.calcEMA = function (emaLength) {
var emaK, emaK2, ema = { price: 0, today: 0, yest: 0 };
var data, key = 1,
len;
emaK = 2 / (emaLength + 1);
this.s({ type: this.TYPE.ema, prop: "k" + emaLength }, this.f(emaK, 5));
emaK2 = 1 - emaK;
this.s({ type: this.TYPE.ema, prop: "lastX" + emaLength }, this.gg("zeroX"));
this.s({ type: this.TYPE.ema, prop: "lastY" + emaLength }, this.gg("zeroY"));
this.s({ type: this.TYPE.ema, prop: "data" + emaLength }, []);
this.add({ type: this.TYPE.ema, prop: "data" + emaLength }, this.gg("data")[0]);
data = this.gg("data");
len = data.length;
for (; key < len; key++) {
ema.price = data[key];
ema.yest = this.g({ type: this.TYPE.ema, prop: "data" + emaLength })[key - 1];
ema.today = ema.price * emaK + ema.yest * emaK2;
this.add({ type: this.TYPE.ema, prop: "data" + emaLength }, this.f(ema.today, 5));
}
len = key;
key = 1;
for (; key < len; key++) {
(this.calcPoint)(key, this.TYPE.ema, emaLength);
}
};
//Exponential Mobing Average
Lines.prototype.drawEMA = function (emaLength) {
var points, plen, key = 0,
lineAxis = [],
_linePath = "";
emaLength = emaLength || this.cfg.emaLength;
points = this.g({ type: this.TYPE.ema, prop: "points" + emaLength });
points && (plen = points.length);
if (!plen) {
this.calcEMA(emaLength);
this.drawEMA(emaLength);
return;
} else if (plen < this.cfg.emaLength) {
this.pr("EMA can NOT exist insufficient points");
return;
}
for (; key < (plen - 1); key++) {
lineAxis.push([points[key][0], points[key][1]]);
lineAxis.push([points[key + 1][0], points[key + 1][1]]);
_linePath += this.getPath(lineAxis);
this.drawDebug(lineAxis[0]);
lineAxis = [];
}
this.printPath({ type: this.TYPE.ema, path: _linePath, length: emaLength });
};
Lines.prototype.liveFlag = false;
Lines.prototype.dragX = 0;
//turn on live dot which follow user mouse
Lines.prototype.live = function () {
var live = {},
self = this,
dot, label, left, dragX;
if (this.cfg.animate && !this.liveFlag) {
this.liveFlag = true;
this.drawOrder.push("live");
return;
}
if (!this.findY(100)) return;
live.labelInfo = {
type: this.TYPESVG.text,
axis: [this.chartArea.zeroX, this.chartArea.zeroY],
text: "",
class: this.cfg.cssClass.liveLabel
};
label = this.drawSVG(live.labelInfo);
live.dotInfo = {
type: this.TYPESVG.circle,
axis: [100, this.findY(100).pixel],
r: this.cfg.debug.radius,
class: this.cfg.cssClass.liveDot
};
dot = this.drawSVG(live.dotInfo);
left = this.chartArea.offsetLeft;
this.snap.mousemove((e, x) => {
var cx, foundY;
x -= left;
if (x < this.chartArea.zeroX && !(x % 5)) {
cx = x + this.dragX;
foundY = this.findY((x - this.dragX)) || 0;
if (foundY.pixel) {
dot.attr({
"cx": x,
"cy": foundY.pixel
});
label.node.innerHTML = foundY.value;
label.attr({ y: foundY.pixel });
}
}
});
// if (!this.lcGroup) {
// this.lcGroup = this.snap.g(this.elms.line["svg-line"].elem);
// }
// this.snap.drag((moveX, moveY) => {
// dragX = moveX;
// self.lcGroup.attr({ transform: "t" + (dragX + self.dragX) });
// }, () => {}, () => {
// self.dragX += dragX;
// });
};
// http://jsfiddle.net/tM4L9/7/
// mobile support
// option {constx, consty, arrow, tube}
// use for live draw
// CLICK > MOUSEMOVE callback(state)
// !!! handle ONLY mouseMove mouseClick and return moveAxis
// for reference see navDot function
// cb({state: start}) first click
// cb({state: move}) before second click
// cb({state: finish}) second click
// reduse complexity into liveLine
// - one from mousemove and one mouseclick
Lines.prototype.liveMove = function (liveInfo = {}, cb) {
var self = this,
clicks = 0,
offset = {},
axis = [],
magnet = {};
offset.left = this.chartArea.offsetLeft;
offset.top = this.chartArea.offsetTop;
this.snap.unclick();
this.snap.click((e, clickX, clickY) => {
this.setEvent("draw", true);
axis[0] = [(clickX - offset.left), (clickY - offset.top)];
axis[1] = [(axis[0][0] + this.cfg.chart.padding), (axis[0][1] + this.cfg.chart.padding)];
(!clicks && cb) && cb({ state: "start", axis: axis });
clicks++;
if (clicks === 1) {
this.snap.unmousemove(); //bind once
this.snap.mousemove(function (e, dx, dy) {
if (((dx % 5) && (dy % 5)) || (axis[1][0] > self.chartArea.width)) {
return;
}
var _axis = axis.slice(0); //deep copy
_axis[1] = [(dx - offset.left), (dy - offset.top)];
magnet = self.findY(_axis[1][0]); //return pixel, value
magnet.diff = Math.abs(_axis[1][1] - magnet.pixel);
if (self.cfg.magnetMode && magnet.diff < self.cfg.magnetMode) {
_axis[1][1] = magnet.pixel;
}
if (liveInfo.constx) {
_axis[0][0] = self.chartArea.endX;
_axis[1][0] = self.chartArea.width; // axisX2
_axis[0][1] = _axis[1][1]; // axisY1
} else if (liveInfo.consty) {
_axis[0][1] = self.cfg.chart.padding;
_axis[1][1] = self.chartArea.height + self.cfg.chart.padding; // axisY
_axis[0][0] = _axis[1][0]; // axisX
}
//axis > mouse axis, axis is with manipulation
cb && cb({ state: "move", axis: _axis });
});
} else if (clicks === 2) {
this.setEvent("draw", false);
this.snap.unclick();
this.snap.unmousemove();
cb && cb({ state: "finish", axis: axis });
}
});
};
// delete from id or navid
// delete from live element object(lelms)
Lines.prototype.dl = function (deleteInfo = {}) {
var delIndex;
if (deleteInfo.type === "nav" && this.lelms.navid.indexOf(deleteInfo.prop) !== -1) {
delIndex = this.lelms.navid.indexOf(deleteInfo.prop);
this.lelms.navid.splice(delIndex, 1);
} else if (this.lelms.id.indexOf(deleteInfo.prop) !== -1) {
delIndex = this.lelms.id.indexOf(deleteInfo.prop);
this.lelms.id.splice(delIndex, 1);
}
if (deleteInfo.prop && this.lelms[deleteInfo.type][deleteInfo.prop]) {
delete this.lelms[deleteInfo.type][deleteInfo.prop];
} else {
delete this.lelms[deleteInfo.type];
}
};
//get live property
// start to use lelms
// should replace lgs
// this.lgs("navs").length;
// get live
Lines.prototype.gl = function (getInfo = {}) {
if (getInfo.id) {
var elemInfo = this.splitId(getInfo.id, true);
return this.lelms[elemInfo.type][getInfo.id];
} else if (getInfo.prop) {
return this.lelms[getInfo.type] ? (this.lelms[getInfo.type][getInfo.prop] || false) : false;
} else {
return this.lelms[getInfo.type] || false;
}
};
// set live
Lines.prototype.sl = function (setInfo = {}, setData) {
this.lelms[setInfo.type] || (this.lelms[setInfo.type] = {});
//push elemID into navid OR id
if (setInfo.type === "nav") {
this.lelms.navid.push(setInfo.prop);
} else {
this.lelms.id.push(setInfo.prop);
}
this.lelms[setInfo.type][setInfo.prop] = setData;
};
Lines.prototype.dragtmp = {
elem: "",
move: "",
rotate: ""
};
//toggle global Events
// - wheel
// - drag
Lines.prototype.setEvent = function (eventType = false, eventState) {
if (!eventType || typeof eventState !== "boolean") {
return;
} else if (["drag", "draw"].indexOf(eventType) === -1) {
return;
}
this.sl({ type: "action", prop: eventType }, eventState);
};
// set lelms.last.elem and use for further on
// callback function for drag navigation DOT...
// dragData.state = [start, drag, finish]
Lines.prototype.navDotDrag = function (dragData = {}, extraData = {}) {
//explicit set new element for EACH DRAG START !!!
if (dragData.state === "start") {
this.setEvent("drag", true);
this.dragtmp.elem = this.gl({ id: dragData.elemId });
this.dragtmp.move = this.getNavDot(dragData.elemId, "move");
this.dragtmp.rotate = this.getNavDot(dragData.elemId, "rotate");
this.dragtmp.moveAxis = this.getAxis(dragData.elemId, "move");
if (!this.dragtmp.group) {
this.dragtmp.elem.transform("");
this.dragtmp.group = this.snap.g(this.dragtmp.elem);
this.dragtmp.group.transform(this.dragtmp.transform);
if (extraData.arrow) {
this.dragtmp.group.add(this.getByType(dragData.elemId, "arrow"));
} else if (extraData.tube) {
var tubeElem = this.getByType(dragData.elemId, "tube");
this.dragtmp.group.add(tubeElem.top, tubeElem.bot);
}
}
if (dragData.action === "move") {
this.dragtmp.rotate.transform("");
// this.dragtmp.group = this.snap.g(this.dragtmp.elem, this.dragtmp.rotate);
this.dragtmp.group.add(this.dragtmp.rotate);
// this.dragtmp.group.add(this.dragtmp.arrow);
}
this.dragtmp.transform = this.dragtmp.group.transform ? this.dragtmp.group.transform().local : 0;
} else if (dragData.state === "drag") {
var transform;
if (dragData.action === "move") {
transform = this.dragtmp.transform + (this.dragtmp.transform ? "T" : "t");
transform += [dragData.dx, dragData.dy];
this.dragtmp.group.transform(transform);
} else if (dragData.action === "rotate") {
transform = this.dragtmp.transform + (this.dragtmp.transform ? "R" : "r");
transform += [dragData.angle, this.dragtmp.moveAxis.x, this.dragtmp.moveAxis.y];
this.dragtmp.group.transform(transform);
}
} else if (dragData.state === "finish" && dragData.action === "move") {
// this.mvElem(dragData.elemId);
this.mvElem(this.dragtmp.rotate.node.id);
this.setEvent("drag", false);
}
};
//toDo - change live elements object
// quite heavy method !!! BE CAREFUL
// liveMove !!!
// navDotDrag !!!
Lines.prototype.liveLine = function (extra = {}) {
var axis, elemID, navidLength, liveLine = {};
//restore last live element for multiple lines ...
this.lelms.last = 0;
this.liveMove(extra, moveData => {
axis = moveData.axis;
if (extra.text) {
this.liveText(moveData);
} else if (moveData.state === "start") {
navidLength = (this.gl({ type: "navid" }) || []).length;
elemID = thi