UNPKG

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
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