// o---------------------------------------------------------------------------------o // | This file is part of the RGraph package - you can learn more at: | // | | // | https://www.rgraph.net/license.html | // | | // | RGraph is dual-licensed under the Open Source GPL license. That means that it's | // | free to use and there are no restrictions on what you can use RGraph for! | // | If the GPL license does not suit you however, then there's an inexpensive | // | commercial license option available. See the URL above for more details. | // o---------------------------------------------------------------------------------o RGraph = window.RGraph || {isrgraph:true,isRGraph:true,rgraph:true}; RGraph.SVG = RGraph.SVG || {}; // Module pattern (function (win, doc, undefined) { RGraph.SVG.Scatter = function (conf) { // // A setter that the constructor uses (at the end) // to set all of the properties // // @param string name The name of the property to set // @param string value The value to set the property to // this.set = function (name, value) { if (arguments.length === 1 && typeof name === 'object') { for (i in arguments[0]) { if (typeof i === 'string') { this.set(i, arguments[0][i]); } } } else { // Go through all of the properties and make sure // that they're using the correct capitalisation name = this.properties_lowercase_map[name.toLowerCase()] || name; var ret = RGraph.SVG.commonSetter({ object: this, name: name, value: value }); name = ret.name; value = ret.value; // If setting the colors, update the originalColors // property too if (name === 'colors') { this.originalColors = RGraph.SVG.arrayClone(value, true); this.colorsParsed = false; } // BC for labelsAboveSeperator if (name === 'labelsAboveSeperator') { name = labelsAboveSeparator; } this.properties[name] = value; } return this; }; // // A getter. // // @param name string The name of the property to get // this.get = function (name) { // Go through all of the properties and make sure // that they're using the correct capitalisation name = this.properties_lowercase_map[name.toLowerCase()] || name; return this.properties[name]; }; this.type = 'scatter'; this.id = conf.id; this.uid = RGraph.SVG.createUID(); this.container = document.getElementById(this.id); this.layers = {}; // MUST be before the SVG tag is created! this.svg = RGraph.SVG.createSVG({object: this,container: this.container}); this.svgAllGroup = RGraph.SVG.createAllGroup(this); this.clipid = null; // Used to clip the canvas this.isRGraph = true; this.isrgraph = true; this.rgraph = true; this.width = Number(this.svg.getAttribute('width')); this.height = Number(this.svg.getAttribute('height')); this.data = conf.data; this.coords = []; this.coords2 = []; this.coordsBubble = []; this.coordsTrendline = []; this.colorsParsed = false; this.originalColors = {}; this.gradientCounter = 1; this.sequential = 0; this.line_groups = []; this.firstDraw = true; // After the first draw this will be false // Add this object to the ObjectRegistry RGraph.SVG.OR.add(this); this.container.style.display = 'inline-block'; this.properties = { marginLeft: 35, marginRight: 35, marginTop: 35, marginBottom: 35, backgroundColor: null, backgroundImage: null, backgroundImageAspect: 'none', backgroundImageStretch: true, backgroundImageOpacity: null, backgroundImageX: null, backgroundImageY: null, backgroundImageW: null, backgroundImageH: null, backgroundGrid: true, backgroundGridColor: '#ddd', backgroundGridLinewidth: 1, backgroundGridHlines: true, backgroundGridHlinesCount: null, backgroundGridVlines: true, backgroundGridVlinesCount: null, backgroundGridBorder: true, backgroundGridDashed: false, backgroundGridDotted: false, backgroundGridDashArray: null, tickmarksStyle: 'cross', tickmarksSize: 7, colors: null, colorsDefault: 'black', line: false, lineColors: null, lineLinewidth: 1, errorbarsColor: 'black', errorbarsLinewidth: 1, errorbarsCapwidth: 10, yaxis: true, yaxisLinewidth: 1, yaxisTickmarks: true, yaxisTickmarksLength: 3, yaxisColor: 'black', yaxisScale: true, yaxisLabels: null, yaxisLabelsOffsetx: 0, yaxisLabelsOffsety: 0, yaxisLabelsCount: 5, yaxisScaleUnitsPre: '', yaxisScaleUnitsPost: '', yaxisScaleStrict: false, yaxisScaleDecimals: 0, yaxisScalePoint: '.', yaxisScaleThousand: ',', yaxisScaleRound: false, yaxisScaleMax: null, yaxisScaleMin: 0, yaxisScaleFormatter: null, yaxisTitle: '', yaxisTitleBold: null, yaxisTitleSize: null, yaxisTitleFont: null, yaxisTitleColor: null, yaxisTitleItalic: null, yaxisTitleOffsetx: 0, yaxisTitleOffsety: 0, yaxisTitleX: null, yaxisTitleY: null, yaxisTitleHalign: null, yaxisTitleValign: null, xaxis: true, xaxisLinewidth: 1, xaxisTickmarks: true, xaxisTickmarksLength: 5, xaxisLabels: null, xaxisLabelsPosition: 'section', xaxisLabelsPositionEdgeTickmarksCount: 10, xaxisColor: 'black', xaxisLabelsOffsetx: 0, xaxisLabelsOffsety: 0, xaxisLabelsCount: 10, xaxisLabelsFont: null, xaxisLabelsSize: null, xaxisLabelsColor: null, xaxisLabelsBold: null, xaxisLabelsItalic: null, xaxisScaleUnitsPre: '', xaxisScaleUnitsPost: '', xaxisScaleMax: null, xaxisScaleMin: 0, xaxisScalePoint: '.', xaxisRound: false, xaxisScaleThousand: ',', xaxisScaleDecimals: 0, xaxisScaleFormatter: null, xaxisTitle: '', xaxisTitleBold: null, xaxisTitleSize: null, xaxisTitleFont: null, xaxisTitleColor: null, xaxisTitleItalic: null, xaxisTitleOffsetx: 0, xaxisTitleOffsety: 0, xaxisTitleX: null, xaxisTitleY: null, xaxisTitleHalign: null, xaxisTitleValign: null, textColor: 'black', textFont: 'Arial, Verdana, sans-serif', textSize: 12, textBold: false, textItalic: false, text: null, labelsAboveFont: null, labelsAboveSize: null, labelsAboveBold: null, labelsAboveItalic: null, labelsAboveColor: null, labelsAboveBackground: 'rgba(255,255,255,0.7)', labelsAboveBackgroundPadding: 2, labelsAboveXUnitsPre: null, labelsAboveXUnitsPost: null, labelsAboveXPoint: null, labelsAboveXThousand: null, labelsAboveXFormatter: null, labelsAboveXDecimals: null, labelsAboveYUnitsPre: null, labelsAboveYUnitsPost: null, labelsAboveYPoint: null, labelsAboveYThousand: null, labelsAboveYFormatter: null, labelsAboveYDecimals: null, labelsAboveOffsetx: 0, labelsAboveOffsety: -10, labelsAboveHalign: 'center', labelsAboveValign: 'bottom', labelsAboveSeparator: ',', tooltipsOverride: null, tooltipsEffect: 'fade', tooltipsCssClass: 'RGraph_tooltip', tooltipsCss: null, tooltipsEvent: 'mousemove', tooltipsPersistent: false, tooltipsFormattedThousand: ',', tooltipsFormattedPoint: '.', tooltipsFormattedDecimals: 0, tooltipsFormattedUnitsPre: '', tooltipsFormattedUnitsPost: '', tooltipsFormattedKeyColors: null, tooltipsFormattedKeyColorsShape: 'square', tooltipsFormattedKeyLabels: [], tooltipsFormattedTableHeaders: null, tooltipsFormattedTableData: null, tooltipsPointer: true, tooltipsPointerOffsetx: 0, tooltipsPointerOffsety: 0, tooltipsPositionStatic: true, highlightStroke: 'rgba(0,0,0,0)', highlightFill: 'rgba(255,255,255,0.7)', highlightLinewidth: 1, title: '', titleX: null, titleY: null, titleHalign: 'center', titleValign: null, titleSize: null, titleColor: null, titleFont: null, titleBold: null, titleItalic: null, titleSubtitle: null, titleSubtitleSize: null, titleSubtitleColor: '#aaa', titleSubtitleFont: null, titleSubtitleBold: null, titleSubtitleItalic: null, key: null, keyColors: null, keyOffsetx: 0, keyOffsety: 0, keyLabelsOffsetx: 0, keyLabelsOffsety: -1, keyLabelsFont: null, keyLabelsSize: null, keyLabelsColor: null, keyLabelsBold: null, keyLabelsItalic: null, bubble: false, bubbleMaxValue: null, bubbleMaxRadius: null, bubbleColorsSolid: false, bubbleShadow: false, bubbleShadowOffsetx: 2, bubbleShadowOffsety: 2, bubbleShadowBlur: 1, bubbleShadowColor: '#aaa', errorbars: null, errorbarsColor: 'black', errorbarsLinewidth: 1, errorbarsCapwidth: 10, trendline: false, trendlineColors: ['gray'], trendlineLinewidth: 1, trendlineMargin: 15, trendlineDashed: true, trendlineDotted: false, trendlineDashArray: null, trendlineClipping: null, outofbounds: true, clip: null, zoom: false }; // // Add the reverse look-up table for property names // so that property names can be specified in any case. // this.properties_lowercase_map = []; for (var i in this.properties) { if (typeof i === 'string') { this.properties_lowercase_map[i.toLowerCase()] = i; } } // // Copy the global object properties to this instance // RGraph.SVG.getGlobals(this); // // Set the options that the user has provided // for (i in conf.options) { if (typeof i === 'string') { this.set(i, conf.options[i]); } } // Handles the data that was supplied to the object. If only one dataset // was given, convert it into into a multiple dataset style array if (this.data[0] && !RGraph.SVG.isArray(this.data[0])) { this.data = []; this.data[0] = conf.data; } // Go through the data converting the various X and Y options // from strings to numbers if necessary // // THIS MESSES UP DATETIME CHART // //for (var i=0; i<this.data.length; ++i) { // for (var j=0; j<this.data[i].length; ++j) { // if (typeof this.data[i][j].x === 'string') this.data[i][j].x = parseFloat(this.data[i][j].x); // if (typeof this.data[i][j].y === 'string') this.data[i][j].y = parseFloat(this.data[i][j].y); // if (typeof this.data[i][j].size === 'string') this.data[i][j].size = parseFloat(this.data[i][j].size); // if (typeof this.data[i][j].opacity === 'string') this.data[i][j].opacity = parseFloat(this.data[i][j].opacity); // } //} // // "Decorate" the object with the generic effects if the effects library has been included // if (RGraph.SVG.FX && typeof RGraph.SVG.FX.decorate === 'function') { RGraph.SVG.FX.decorate(this); } // Add the responsive function to the object this.responsive = RGraph.SVG.responsive; var properties = this.properties; // // Convert string X values to timestamps // if (typeof properties.xaxisScaleMin === 'string') { properties.xaxisScaleMin = RGraph.SVG.parseDate(properties.xaxisScaleMin); } if (typeof properties.xaxisScaleMax === 'string') { properties.xaxisScaleMax = RGraph.SVG.parseDate(properties.xaxisScaleMax); } for (var i=0; i<this.data.length; ++i) { for (var j=0; j<this.data[i].length; ++j) { if (typeof this.data[i][j].x === 'string') { this.data[i][j].x = RGraph.SVG.parseDate(this.data[i][j].x); } } } // // The draw method draws the Bar chart // this.draw = function () { // Fire the beforedraw event RGraph.SVG.fireCustomEvent(this, 'onbeforedraw'); // Reset the sequential counter this.sequential = 0; // Should be the first(ish) thing that's done in the // .draw() function except for the onbeforedraw event // and the installation of clipping. this.width = Number(this.svg.getAttribute('width')); this.height = Number(this.svg.getAttribute('height')); // // If the labels option is a string then turn it // into an array. // if (properties.xaxisLabels && properties.xaxisLabels.length) { if (typeof properties.xaxisLabels === 'string') { properties.xaxisLabels = RGraph.SVG.arrayPad({ array: [], length: properties.xaxisLabelsCount, value: properties.xaxisLabels }); } // // Label substitution // for (var i=0; i<properties.xaxisLabels.length; ++i) { properties.xaxisLabels[i] = RGraph.SVG.labelSubstitution({ object: this, text: properties.xaxisLabels[i], index: i, value: this.data[0][i], decimals: properties.xaxisLabelsFormattedDecimals || 0, unitsPre: properties.xaxisLabelsFormattedUnitsPre || '', unitsPost: properties.xaxisLabelsFormattedUnitsPost || '', thousand: properties.xaxisLabelsFormattedThousand || ',', point: properties.xaxisLabelsFormattedPoint || '.' }); } } // Create the defs tag if necessary RGraph.SVG.createDefs(this); this.graphWidth = this.width - properties.marginLeft - properties.marginRight; this.graphHeight = this.height - properties.marginTop - properties.marginBottom; // Prevents these from growing this.coords = []; this.coords2 = []; this.coordsBubble = []; // Parse the colors for gradients RGraph.SVG.resetColorsToOriginalValues({object:this}); this.parseColors(); // Work out the maximum value for (var ds=0,max=0; ds<this.data.length; ++ds) { // Datasets for (var dp=0; dp<this.data[ds].length; ++dp) { // Datapoints max = Math.max( max, this.data[ds][dp].y + (this.data[ds][dp].errorbar ? (typeof this.data[ds][dp].errorbar === 'number' ? this.data[ds][dp].errorbar : this.data[ds][dp].errorbar.max) : 0) ); } } // A custom, user-specified maximum value if (typeof properties.yaxisScaleMax === 'number') { max = properties.yaxisScaleMax; } // Set the ymin to zero if it's set mirror if (properties.yaxisScaleMin === 'mirror' || properties.yaxisScaleMin === 'middle' || properties.yaxisScaleMin === 'center') { var mirrorScale = true; properties.yaxisScaleMin = 0; } // // Generate an appropiate scale // this.scale = RGraph.SVG.getScale({ object: this, numlabels: properties.yaxisLabelsCount, unitsPre: properties.yaxisScaleUnitsPre, unitsPost: properties.yaxisScaleUnitsPost, max: max, min: properties.yaxisScaleMin, point: properties.yaxisScalePoint, round: properties.yaxisScaleRound, thousand: properties.yaxisScaleThousand, decimals: properties.yaxisScaleDecimals, strict: typeof properties.yaxisScaleMax === 'number', formatter: properties.yaxisScaleFormatter }); // // Get the scale a second time if the ymin should be // mirored // // Set the ymin to zero if it's set mirror if (mirrorScale) { this.scale = RGraph.SVG.getScale({ object: this, numlabels: properties.yaxisLabelsCount, unitsPre: properties.yaxisScaleUnitsPre, unitsPost: properties.yaxisScaleUnitsPost, max: this.scale.max, min: this.scale.max * -1, point: properties.yaxisScalePoint, round: false, thousand: properties.yaxisScaleThousand, decimals: properties.yaxisScaleDecimals, strict: true, formatter: properties.yaxisScaleFormatter }); } // Now the scale has been generated adopt its max value this.max = this.scale.max; this.min = this.scale.min; properties.yaxisScaleMax = this.scale.max; properties.yaxisScaleMin = this.scale.min; // Install clipping if requested if (this.properties.clip) { this.clipid = RGraph.SVG.installClipping(this); // Add the clip ID to the all group this.svgAllGroup.setAttribute( 'clip-path', 'url(#{1})'.format(this.clipid) ); } else { // No clipping - so ensure that there's no clip-path // attribute this.clipid = null; this.svgAllGroup.removeAttribute('clip-path'); } // Draw the background first RGraph.SVG.drawBackground(this); // Draw the axes under the points RGraph.SVG.drawXAxis(this); RGraph.SVG.drawYAxis(this); // Create a group for all of the datasets var dataset_group = RGraph.SVG.create({ svg: this.svg, type: 'g', parent: this.svgAllGroup, attr: { className: 'scatter_datasets_' + this.uid } }); // Draw the points for all of the datasets for (var i=0; i<this.data.length; ++i) { var group = RGraph.SVG.create({ svg: this.svg, type: 'g', parent: this.svgAllGroup, attr: { id: 'scatter_line_' + i + this.uid } }); this.line_groups[i] = group; this.drawPoints({ index: i, data: this.data[i], group: dataset_group }); // Draw a line for this dataset if (properties.line === true || (typeof properties.line === 'object' && properties.line[i] === true)) { this.drawLine({ index: i, coords: this.coords2[i], }); } } // // Draw a trendline if requested // if (properties.trendline) { for (var i=0; i<this.data.length; ++i) { if (properties.trendline === true || (typeof properties.trendline === 'object' && properties.trendline[i] === true) ) { this.drawTrendline(i); } } } // Draw the key if (typeof properties.key !== null && RGraph.SVG.drawKey) { RGraph.SVG.drawKey(this); } else if (!RGraph.SVG.isNullish(properties.key)) { alert('The drawKey() function does not exist - have you forgotten to include the key library?'); } // Add the event listener that clears the highlight rect if // there is any. Must be MOUSEDOWN (ie before the click event) //var obj = this; //document.body.addEventListener('mousedown', function (e) //{ //RGraph.SVG.removeHighlight(obj); //}, false); // // Allow the addition of custom text via the // text: property. // RGraph.SVG.addCustomText(this); // Draw any custom lines that have been defined RGraph.SVG.drawHorizontalLines(this); // Lastly - install the zoom event listeners if // requested if (this.properties.zoom) { RGraph.SVG.addZoom(this); } // // Fire the onfirstdraw event // if (this.firstDraw) { this.firstDraw = false; RGraph.SVG.fireCustomEvent(this, 'onfirstdraw'); } // Fire the draw event RGraph.SVG.fireCustomEvent(this, 'ondraw'); // // Install any inline responsive configuration. This // should be last in the draw function - even after // the draw events. // RGraph.SVG.installInlineResponsive(this); return this; }; // // New create() shortcut function // For example: // this.create('rect,x:0,y:0,width:100,height:100'[,parent]); // // @param str string The tag definition to parse and create // @param object The (optional) parent element // @return object The new tag // this.create = function (str) { var def = RGraph.SVG.create.parseStr(this, str); def.svg = this.svg; // By default the parent is the SVG tag - but if // requested then change it to the tag that has // been given if (arguments[1]) { def.parent = arguments[1]; } return RGraph.SVG.create(def); }; // // Draws the Points // // @param opt object Options to the function which can consist of: // o index: The numerical index of the DATASET // o dataset: The dataset. // this.drawPoints = function (opt) { var index = opt.index, data = opt.data, group = opt.group; // Initialise the array for coordinates if (!this.coords2[index]) { this.coords2[index] = []; } // // Create the <g> tag that the bubbles are added to // if a bubble chart has been requested. If not - // then this group sits empty. // this.svgBubbleGroup = RGraph.SVG.create({ svg: this.svg, type: 'g', parent: this.svgAllGroup, attr: { className: 'scatter_bubble_dataset_' + index + '_' + this.uid } }); // // Create the <g> tag that the datapoints are added to // var group = RGraph.SVG.create({ svg: this.svg, type: 'g', parent: group, attr: { className: 'scatter_dataset_' + index + '_' + this.uid } }); // Loop through the data for (var i=0; i<data.length; ++i) { var point = data[i]; if (RGraph.SVG.isNumber(point.x) && RGraph.SVG.isNumber(point.y)) { var ret = this.drawSinglePoint({ dataset: data, datasetIdx: index, point: point, index: i, group: group, // The SVG <g> tag the points are added to sequential: this.sequential }); // Add the coordinates to the coords arrays this.coords.push({ x: ret.x, y: ret.y, z: ret.size, type: ret.type, element: ret.mark, object: this }); this.coords2[index][i] = { x: ret.x, y: ret.y, z: ret.size, type: ret.type, element: ret.mark, object: this }; this.sequential++ } // // Add tooltip highlight to the point // if ( (typeof data[i].tooltip === 'string' && data[i].tooltip) || (typeof data[i].tooltip === 'number') || (typeof properties.tooltips === 'string') ) { // Convert the tooltip to a string data[i].tooltip = String(data[i].tooltip); // Make the tooltipsEvent default to click if (properties.tooltipsEvent !== 'mousemove') { properties.tooltipsEvent = 'click'; } if (!group_tooltip_hotspots) { var group_tooltip_hotspots = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'g', attr: { className: 'rgraph-scatter-tooltip-hotspots' } }); } var rect = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: RGraph.SVG.isNumber(this.data[index][i].z) ? 'circle' : 'rect', parent: group_tooltip_hotspots, attr: { // If the hotspot is a circle (for a // bubble chart) then these attributes // are here to size it cx: this.coords2[index][i].x, cy: this.coords2[index][i].y, r: this.data[index][i].z ? this.coordsBubble[index][i].z : this.coords2[index][i].r, // If the hotspot is a rect then these are // the rect sizing attributes x: ret.x - (ret.size / 2), y: ret.y - (ret.size / 2), width: ret.size, height: ret.size, stroke: 'transparent', fill: 'transparent', 'stroke-width': 3, 'data-dataset': index, 'data-index': i, 'data-x': this.data[index][i].x, 'data-y': this.data[index][i].y, 'data-z': this.data[index][i].z }, style: { cursor: 'pointer' } }); // Add the hotspot to the original tickmark ret.mark.hotspot = rect; (function (dataset, index, seq, obj) { rect.addEventListener(properties.tooltipsEvent, function (e) { // TODO work out the bubble radius // so the highlight can be increased // in size var isBubble = false ; if (RGraph.SVG.isNumber(obj.data[dataset][index].z)) { var isBubble = true; var bubbleMaxRadius = obj.get('bubbleMaxRadius'); var bubbleMaxValue = obj.get('bubbleMaxValue'); var bubbleValue = obj.data[dataset][index].z; var bubbleWidth = bubbleValue / bubbleMaxValue * bubbleMaxRadius; } var tooltip = RGraph.SVG.REG.get('tooltip'); if (tooltip && tooltip.__dataset__ === dataset && tooltip.__index__ === index && tooltip.__object__.uid === obj.uid) { return; } obj.removeHighlight(); // Show the tooltip RGraph.SVG.tooltip({ object: obj, dataset: dataset, index: index, sequentialIndex: seq, text: typeof properties.tooltips === 'string' ? properties.tooltips : obj.data[dataset][index].tooltip, event: e }); // Highlight the shape that has been clicked on if (RGraph.SVG.REG.get('tooltip')) { obj.highlight(this, {width: isBubble ? bubbleWidth : null}); } }, false); // Install the event listener that changes the // cursor if necessary if (properties.tooltipsEvent === 'click') { rect.addEventListener('mousemove', function (e) { e.target.style.cursor = 'pointer'; }, false); } }(index, i, this.sequential - 1, this)); } } }; // // Draws a single point on the chart // this.drawSinglePoint = function (opt) { var dataset = opt.dataset, datasetIdx = opt.datasetIdx, seq = opt.sequential, point = opt.point, index = opt.index, valueX = opt.point.x, valueY = opt.point.y, conf = opt.point || {}, group = opt.group, coordX = opt.coordx = this.getXCoord(valueX), coordY = opt.coordy = this.getYCoord(valueY); // Get the above label if (conf.labelsAbove) { // Plural var above = true; } else if (conf.labelAbove) { // Not plural var above = true; } else if (conf.above) { var above = true; } // Allow shape to be synonym for type if (typeof conf.type === 'undefined' && typeof conf.shape !== 'undefined') { var type = conf.shape; } // set the type to the default if its not set if (typeof conf.type !== 'string') { if (typeof properties.tickmarksStyle === 'string') { var type = properties.tickmarksStyle; } else if (typeof properties.tickmarksStyle === 'object' && typeof properties.tickmarksStyle[datasetIdx] === 'string') { var type = properties.tickmarksStyle[datasetIdx]; } else { var type = 'cross'; } } else { var type = conf.type; } // set the size to the default if its not set if (typeof conf.size !== 'number' && typeof properties.tickmarksSize === 'number') { var size = properties.tickmarksSize; } else if (typeof conf.size !== 'number' && typeof properties.tickmarksSize === 'object' && typeof properties.tickmarksSize[datasetIdx] === 'number') { var size = properties.tickmarksSize[datasetIdx]; } else if (RGraph.SVG.isNumber(conf.size)) { var size = conf.size; } // Set the color to the default if its not set if (conf.color) { var color = conf.color; } else if (RGraph.SVG.isArray(properties.colors) && properties.colors[datasetIdx]) { var color = properties.colors[datasetIdx]; } else { var color = properties.colorsDefault; } // Set the opacity of this point if (typeof conf.opacity === 'undefined') { var opacity = 1; } else if (typeof conf.opacity === 'number') { var opacity = conf.opacity; } // Draw the errorbar here // // First convert the errorbar information in the data into an array in the properties // properties.errorbars = []; for (var ds=0,max=0; ds<this.data.length; ++ds) { for (var idx=0; idx<this.data[ds].length; ++idx) { properties.errorbars.push(this.data[ds][idx].errorbar); } } this.drawErrorbar({ object: this, dataset: datasetIdx, index: index, group: group, sequential: seq, x: coordX, y: coordY, valueX: valueX, valueY: valueY, parent: group }); // Bubble charts are drawn by their own function if (properties.bubble) { var bubbleRet = this.drawBubble(opt, conf); } // Handle the various shapes for tickmarks here switch (type) { case 'image:' + type.substr(6): var src = type.substr(6); var img = new Image(); img.src = src; var mark = RGraph.SVG.create({ svg: this.svg, type: 'image', parent: group, attr: { preserveAspectRatio: 'xMidYMid meet', 'xlink:href': src, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); // Once the image has loaded the x/y/width/height can be set // (both the image and it's hotspot) img.onload = function () { var x = coordX - (img.width / 2), y = coordY - (img.height / 2), w = img.width, h = img.height; mark.setAttribute('x', x); mark.setAttribute('y', y); mark.setAttribute('width', w); mark.setAttribute('height', h); if (mark && mark.hotspot) { mark.hotspot.setAttribute('x', x); mark.hotspot.setAttribute('y', y); mark.hotspot.setAttribute('width', w); mark.hotspot.setAttribute('height', h); } }; break; case 'triangle': var mark = RGraph.SVG.create({ svg: this.svg, type: 'path', parent: group, attr: { d: 'M {1} {2} L {3} {4} L {5} {6}'.format( coordX - (size / 2), coordY + (size / 2), coordX, coordY - (size / 2), coordX + (size / 2), coordY + (size / 2) ), fill: color, 'fill-opacity': opacity, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); break; case 'plus': var mark = RGraph.SVG.create({ svg: this.svg, type: 'path', parent: group, attr: { d: 'M {1} {2} L {3} {4} M {5} {6} L {7} {8}'.format( coordX - (size / 2), coordY, coordX + (size / 2), coordY, coordX, coordY - (size / 2), coordX, coordY + (size / 2) ), stroke: color, 'stroke-opacity': opacity, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); break; case 'square': case 'rect': var mark = RGraph.SVG.create({ svg: this.svg, type: 'rect', parent: group, attr: { x: coordX - (size / 2), y: coordY - (size / 2), width: size, height: size, fill: color, 'fill-opacity': opacity, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); break; case 'dot': case 'circle': var mark = RGraph.SVG.create({ svg: this.svg, type: 'circle', parent: group, attr: { cx: coordX, cy: coordY, r: size / 2, fill: color, 'fill-opacity': opacity, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); break; case 'cross': default: var mark = RGraph.SVG.create({ svg: this.svg, type: 'path', parent: group, attr: { d: 'M {1} {2} L {3} {4} M {5} {6} L {7} {8}'.format( coordX - (size / 2), coordY - (size / 2), coordX + (size / 2), coordY + (size / 2), coordX - (size / 2), coordY + (size / 2), coordX + (size / 2), coordY - (size / 2) ), stroke: color, 'stroke-opacity': opacity, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); break; } // // Draw the above label if it's present // if ( RGraph.SVG.isString(conf.above) || (!RGraph.SVG.isString(conf.above) && conf.above) ) { this.drawLabelsAbove({ point: conf, coordX: coordX, coordY: coordY }); } // Add some data attributes that save various values mark.setAttribute('data-index', index); mark.setAttribute('data-dataset', datasetIdx); mark.setAttribute('data-original-opacity', opacity); mark.setAttribute('data-original-color', color); mark.setAttribute('data-original-coordx', coordX); mark.setAttribute('data-original-coordy', coordY); mark.setAttribute('data-original-coordz', bubbleRet ? bubbleRet.z : null); mark.setAttribute('data-size', size); mark.setAttribute('data-sequential', seq); mark.setAttribute('data-type', type); return { x: coordX, y: coordY, size: type.substr(0,6) === 'image:' ? img.width : size, mark: mark, type: type }; }; // // Draw a bubble on a bubble chart // // @param opt object The internal configuration of the point // @param conf object The configuration of the point as supplied by the user // this.drawBubble = function (opt, conf) { // Check whether this point has a z: value if (typeof conf.z !== 'number') { return; } // // Check that the coords array exists // if (!RGraph.SVG.isArray(this.coordsBubble[opt.datasetIdx])) { this.coordsBubble[opt.datasetIdx] = []; } var size = (conf.z / properties.bubbleMaxValue) * properties.bubbleMaxRadius; var color = RGraph.SVG.parseColorRadial({ object: this, color: properties.bubbleColorsSolid ? conf.color : 'Gradient(white:' + conf.color + ')', cx: opt.coordx + (size / 4), cy: opt.coordy - (size / 4), fx: opt.coordx + (size / 4), fy: opt.coordy - (size / 4), r: size * 1.5 }); var circle = RGraph.SVG.create({ svg: this.svg, type: 'circle', parent: this.svgBubbleGroup, attr: { cx: opt.coordx, cy: opt.coordy, r: size, fill: color, 'fill-opacity': conf.opacity, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); // Add some data attributes that save various values circle.setAttribute('data-index', opt.index); circle.setAttribute('data-dataset', opt.datasetIdx); circle.setAttribute('data-original-opacity', conf.opacity); circle.setAttribute('data-original-color', conf.color); circle.setAttribute('data-original-coordx', opt.coordx); circle.setAttribute('data-original-coordy', opt.coordy); circle.setAttribute('data-size', size); circle.setAttribute('data-r', size); circle.setAttribute('data-sequential', opt.sequential); circle.setAttribute('data-type', 'bubble'); circle.setAttribute('data-x', conf.x); circle.setAttribute('data-y', conf.y); circle.setAttribute('data-z', conf.z); // // Set a shadow if requested // if (this.properties.bubbleShadow) { RGraph.SVG.setShadow({ object: this, offsetx: this.properties.bubbleShadowOffsetx, offsety: this.properties.bubbleShadowOffsety, blur: this.properties.bubbleShadowBlur, color: this.properties.bubbleShadowColor, id: 'bubble-chart-dropshadow' }); circle.setAttribute('filter', 'url(#bubble-chart-dropshadow)'); } this.coordsBubble[opt.datasetIdx][opt.index] = { x: opt.coordx, y: opt.coordy, z: size }; return { x: opt.coordx, y: opt.coordy, z: size }; }; // // This functions draws a line if required // this.drawLine = function (opt) { var linewidth = 1, color = 'black'; // Calculate the linewidth if (typeof properties.lineLinewidth === 'object' && typeof properties.lineLinewidth[opt.index] === 'number') { linewidth = properties.lineLinewidth[opt.index]; } else if (typeof properties.lineLinewidth === 'number') { linewidth = properties.lineLinewidth; } else { linewidth = 1; } // Determine the color if (!RGraph.SVG.isNullish(properties.lineColors) && properties.lineColors && properties.lineColors[opt.index]) { color = properties.lineColors[opt.index]; } else if (!RGraph.SVG.isNullish(properties.colors) && properties.colors.length && typeof properties.colors[opt.index] === 'string') { color = properties.colors[opt.index]; } else if (typeof properties.lineColors === 'string') { color = properties.lineColors; } else { color = 'black'; } for (var i=0,path=''; i<this.coords2[opt.index].length; ++i) { path += '{1} {2} {3} '.format( i === 0 ? 'M' : 'L', this.coords2[opt.index][i].x, this.coords2[opt.index][i].y ); } RGraph.SVG.create({ svg: this.svg, type: 'path', parent: this.line_groups[opt.index], attr: { d: path, fill: 'transparent', stroke: color, 'stroke-width': linewidth, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); }; // // This function can be used to retrieve the relevant X coordinate for a // particular value. // // @param int value The value to get the X coordinate for // this.getXCoord = function (value) { var x; if (value > properties.xaxisScaleMax) { return null; } if (value < properties.xaxisScaleMin) { return null; } x = ((value - properties.xaxisScaleMin) / (properties.xaxisScaleMax - properties.xaxisScaleMin)); x *= (this.width - properties.marginLeft - properties.marginRight); x = properties.marginLeft + x; return x; }; // // This function can be used to retrieve the relevant Y coordinate for a // particular value. // // @param int value The value to get the Y coordinate for // this.getYCoord = function (value) { if (value > this.scale.max && properties.outofbounds === false) { return null; } var y, xaxispos = properties.xaxispos; if (value < this.scale.min && properties.outofbounds === false) { return null; } y = ((value - this.scale.min) / (this.scale.max - this.scale.min)); y *= (this.height - properties.marginTop - properties.marginBottom); y = this.height - properties.marginBottom - y; return y; }; // // This function can be used to highlight a bar on the // chart // // TODO On bubble charts need to highlight the entire // bubble. // // @param object rect The rectangle to highlight // this.highlight = function (rect, opt = {}) { // Get the center point and size of the rect // on the chart and then cover it with a // circle if (rect.toString().toLowerCase().indexOf('circle') > 0) { var cx = rect.getAttribute('cx'), cy = rect.getAttribute('cy'), radius = rect.getAttribute('r'); } else { var cx = parseFloat(rect.getAttribute('x')) + (parseFloat(rect.getAttribute('width')) / 2), cy = parseFloat(rect.getAttribute('y')) + (parseFloat(rect.getAttribute('height')) / 2), radius = parseFloat(rect.getAttribute('width')) + 1; } if (RGraph.SVG.isNumber(opt.width)) { radius = opt.width + 1; } var highlight = RGraph.SVG.create({ svg: this.svg, type: 'circle', parent: this.svgAllGroup, attr: { stroke: properties.highlightStroke, fill: properties.highlightFill, cx: cx, cy: cy, r: radius, 'stroke-width': properties.highlightLinewidth }, style: { pointerEvents: 'none' } }); // Store the highlight rect in the rebistry so // it can be cleared later RGraph.SVG.REG.set('highlight', highlight); //rect.setAttribute('stroke', properties.highlightStroke); //rect.setAttribute('stroke-width', properties.highlightLinewidth); //rect.setAttribute('fill', properties.highlightFill); // Store the highlight rect in the registry so // it can be reset later //RGraph.SVG.REG.set('highlight', rect); }; // // Draws the labelsAbove // // @param opt An object that consists of various arguments to the function // this.drawLabelsAbove = function (opt) { var x = opt.point.x, y = opt.point.y, above = opt.point.above, coordX = opt.coordX, coordY = opt.coordY; // Facilitate labelsAboveSpecific if (typeof above === 'string') { var str = above; } else { x = RGraph.SVG.numberFormat({ object: this, num: x.toFixed(properties.labelsAboveXDecimals ), prepend: typeof properties.labelsAboveXUnitsPre === 'string' ? properties.labelsAboveXUnitsPre : null, append: typeof properties.labelsAboveXUnitsPost === 'string' ? properties.labelsAboveXUnitsPost : null, point: typeof properties.labelsAboveXPoint === 'string' ? properties.labelsAboveXPoint : null, thousand: typeof properties.labelsAboveXThousand === 'string' ? properties.labelsAboveXThousand : null, formatter: typeof properties.labelsAboveXFormatter === 'function' ? properties.labelsAboveXFormatter : null }); y = RGraph.SVG.numberFormat({ object: this, num: y.toFixed(properties.labelsAboveYDecimals ), prepend: typeof properties.labelsAboveYUnitsPre === 'string' ? properties.labelsAboveYUnitsPre : null, append: typeof properties.labelsAboveYUnitsPost === 'string' ? properties.labelsAboveYUnitsPost : null, point: typeof properties.labelsAboveYPoint === 'string' ? properties.labelsAboveYPoint : null, thousand: typeof properties.labelsAboveYThousand === 'string' ? properties.labelsAboveYThousand : null, formatter: typeof properties.labelsAboveYFormatter === 'function' ? properties.labelsAboveYFormatter : null }); var str = '{1}{2}{3}'.format( x, properties.labelsAboveSeparator, y ); } // Get the text configuration var textConf = RGraph.SVG.getTextConf({ object: this, prefix: 'labelsAbove' }); // Add the text to the scene var text = RGraph.SVG.text({ object: this, parent: this.svgAllGroup, tag: 'labels.above', text: str, x: parseFloat(coordX) + properties.labelsAboveOffsetx, y: parseFloat(coordY) + properties.labelsAboveOffsety, halign: properties.labelsAboveHalign, valign: properties.labelsAboveValign, font: textConf.font, size: textConf.size, bold: textConf.bold, italic: textConf.italic, color: textConf.color, background: properties.labelsAboveBackground || null, padding: properties.labelsAboveBackgroundPadding || 0 }); if (this.isTrace) { text.setAttribute( 'clip-path', 'url(#trace-effect-clip)' ); } }; // // This allows for easy specification of gradients // this.parseColors = function () { // TODO Loop thru the data parsing the color for gradients too // Save the original colors so that they can be restored when // the canvas is cleared if (!Object.keys(this.originalColors).length) { this.originalColors = { colors: RGraph.SVG.arrayClone(properties.colors, true), backgroundGridColor: RGraph.SVG.arrayClone(properties.backgroundGridColor, true), highlightFill: RGraph.SVG.arrayClone(properties.highlightFill, true), backgroundColor: RGraph.SVG.arrayClone(properties.backgroundColor, true) } } // colors var colors = properties.colors; // IMPORTANT: Bubble chart gradients are parse in the drawBubble() // function below if (colors && !properties.bubble) { for (var i=0; i<colors.length; ++i) { colors[i] = RGraph.SVG.parseColorLinear({ object: this, color: colors[i] }); } } properties.backgroundGridColor = RGraph.SVG.parseColorLinear({object: this, color: properties.backgroundGridColor}); properties.highlightFill = RGraph.SVG.parseColorLinear({object: this, color: properties.highlightFill}); properties.backgroundColor = RGraph.SVG.parseColorLinear({object: this, color: properties.backgroundColor}); }; // // Using a function to add events makes it easier to facilitate method // chaining // // @param string type The type of even to add // @param function func // this.on = function (type, func) { if (type.substr(0,2) !== 'on') { type = 'on' + type; } RGraph.SVG.addCustomEventListener(this, type, func); return this; }; // // Used in chaining. Runs a function there and then - not waiting for // the events to fire (eg the onbeforedraw event) // // @param function func The function to execute // this.exec = function (func) { func(this); return this; }; // // Remove highlight from the chart (tooltips) // this.removeHighlight = function () { RGraph.SVG.removeHighlight(); }; // // Draws a single errorbar // this.drawErrorbar = function (opt) { // Get the error bar value var max = RGraph.SVG.getErrorbarsMaxValue({ object: this, index: opt.sequential }); // Get the error bar value var min = RGraph.SVG.getErrorbarsMinValue({ object: this, index: opt.sequential }); if (!max && !min) { return; } var linewidth = RGraph.SVG.getErrorbarsLinewidth({object: this, index: opt.sequential}), color = RGraph.SVG.getErrorbarsColor({object: this, index: opt.sequential}), capwidth = RGraph.SVG.getErrorbarsCapWidth({object: this, index: opt.sequential}), halfCapWidth = capwidth / 2; if (max !== 0 || min !== 0) { var y1 = this.getYCoord(opt.valueY + max) y2 = this.getYCoord(opt.valueY - min); // Draw the UPPER vertical line var errorbarLine = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: opt.parent, attr: { x1: opt.x, y1: opt.y, x2: opt.x, y2: y1, stroke: color, 'stroke-width': linewidth, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); // Draw the cap to the UPPER line var errorbarCap = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: opt.parent, attr: { x1: opt.x - halfCapWidth, y1: y1, x2: opt.x + halfCapWidth, y2: y1, stroke: color, 'stroke-width': linewidth, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); } // Draw the minimum errorbar if necessary if (typeof min === 'number') { var errorbarLine = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: opt.parent, attr: { x1: opt.x, y1: opt.y, x2: opt.x, y2: y2, stroke: color, 'stroke-width': linewidth, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); // Draw the cap to the LOWER line var errorbarCap = RGraph.SVG.create({ svg: this.svg, type: 'line', parent: opt.parent, attr: { x1: opt.x - halfCapWidth, y1: y2, x2: opt.x + halfCapWidth, y2: y2, stroke: color, 'stroke-width': linewidth, 'clip-path': this.isTrace ? 'url(#trace-effect-clip)' : '' } }); } }; // // A worker function that handles Bar chart specific tooltip substitutions // this.tooltipSubstitutions = function (opt) { var indexes = RGraph.SVG.sequentialIndexToGrouped(opt.index, this.data), dataset = indexes[0], index = indexes[1]; return { index: index, dataset: dataset, sequentialIndex: opt.index, value: this.data[dataset][index].y, values: [this.data[dataset][index].y] }; }; // // A worker function that returns the correct color/label/value // // @param object specific The indexes that are applicable // @param number index The appropriate index // this.tooltipsFormattedCustom = function (specific, index, colors) { var color = this.data[specific.dataset][specific.index].color ? this.data[specific.dataset][specific.index].color : properties.colorsDefault; if (properties.tooltipsFormattedKeyColors && properties.tooltipsFormattedKeyColors[specific.dataset]) { color = properties.tooltipsFormattedKeyColors[specific.dataset]; } var label = properties.tooltipsFormattedKeyLabels[specific.dataset] ? properties.tooltipsFormattedKeyLabels[specific.dataset] : ''; return { label: label, color: color }; }; // // This allows for static tooltip positioning // this.positionTooltipStatic = function (args) { var obj = args.object, e = args.event, tooltip = args.tooltip, index = args.index, svgXY = RGraph.SVG.getSVGXY(obj.svg), coords = this.coords[args.index]; // Position the tooltip in the X direction args.tooltip.style.left = ( svgXY[0] // The X coordinate of the SVG tag + coords.x // The X coordinate of the bar on the chart - (tooltip.offsetWidth / 2) // Subtract half of the tooltip width ) + 'px'; args.tooltip.style.top = ( svgXY[1] // The Y coordinate of the SVG tag + coords.y // The Y coordinate of the bar on the chart - tooltip.offsetHeight // The height of the tooltip - 15 // An arbitrary amount ) + 'px'; }; // // A trace effect // // @param object Options to give to the effect // @param function A function to call when the effect has completed // this.trace = function () { var opt = arguments[0] || {}, frame = 1, frames = opt.frames || 60, obj = this; this.isTrace = true; this.draw(); // Create the clip area var clippath = RGraph.SVG.create({ svg: this.svg, parent: this.svg.defs, type: 'clipPath', attr: { id: 'trace-effect-clip' } }); var clippathrect = RGraph.SVG.create({ svg: this.svg, parent: clippath, type: 'rect', attr: { x: 0, y: 0, width: 0, height: this.height } }); var iterator = function () { var width = (frame++) / frames * obj.width; clippathrect.setAttribute("width", width); if (frame <= frames) { RGraph.SVG.FX.update(iterator); } else { // Remove the clippath clippath.parentNode.removeChild(clippath); if (opt.callback) { (opt.callback)(obj); } } }; iterator(); return this; }; // // Draws a trendline on the Scatter chart. This is also known // as a "best-fit line" // // @param dataset The index of the dataset to use // this.drawTrendline = function (dataset) { var colors = properties.trendlineColors, linewidth = properties.trendlineLinewidth, margin = properties.trendlineMargin; // Allow for trendlineColor as well (note - no "s") if (RGraph.SVG.isString(properties.trendlineColor)) { colors = [properties.trendlineColor]; } // handle the options being arrays if (typeof colors === 'object' && colors[dataset]) { color = colors[dataset]; } else if (typeof color === 'object') { color = 'gray'; } if (typeof linewidth === 'object' && typeof linewidth[dataset] === 'number') { linewidth = linewidth[dataset]; } else if (typeof linewidth === 'object') { linewidth = 1; } if (typeof margin === 'object' && typeof margin[dataset] === 'number') { margin = margin[dataset]; } else if (typeof margin === 'object'){ margin = 25; } else { margin = 0; } // Step 1: Calculate the mean values of the X coords and the Y coords for (var i=0,totalX=0,totalY=0; i<this.data[dataset].length; ++i) { totalX += Number(this.data[dataset][i].x); totalY += Number(this.data[dataset][i].y); } var averageX = totalX / this.data[dataset].length; var averageY = totalY / this.data[dataset].length; // Step 2: Calculate the slope of the line // a: The X/Y values minus the average X/Y value for (var i=0,xCoordMinusAverageX=[],yCoordMinusAverageY=[],valuesMultiplied=[],xCoordMinusAverageSquared=[]; i<this.data[dataset].length; ++i) { xCoordMinusAverageX[i] = this.data[dataset][i].x - averageX; yCoordMinusAverageY[i] = this.data[dataset][i].y - averageY; // b. Multiply the averages valuesMultiplied[i] = xCoordMinusAverageX[i] * yCoordMinusAverageY[i]; xCoordMinusAverageSquared[i] = xCoordMinusAverageX[i] * xCoordMinusAverageX[i]; } var sumOfValuesMultiplied = RGraph.SVG.arraySum(valuesMultiplied); var sumOfXCoordMinusAverageSquared = RGraph.SVG.arraySum(xCoordMinusAverageSquared); // Calculate m (???) var m = sumOfValuesMultiplied / sumOfXCoordMinusAverageSquared; var b = averageY - (m * averageX); // y = mx + b var coords = [ [properties.xaxisScaleMin, m * properties.xaxisScaleMin + b], [properties.xaxisScaleMax, m * properties.xaxisScaleMax + b] ]; // // Draw the line // // Set dotted, dash or a custom dash array var strokeDasharray = '' if (properties.trendlineDashed) { strokeDasharray = '4,4'; } if (properties.trendlineDotted) { strokeDasharray = '1, 4'; } if (!RGraph.SVG.isNullish(properties.trendlineDashArray) && typeof properties.trendlineDashArray === 'object') { strokeDasharray = String(properties.trendlineDashArray).replace(/[|]/, ''); } // Clip the canvas again so that the line doesn't look overly long // (use the minimum an maximum points for this) for (var i=0,xValues=[],yValues=[]; i<this.data[dataset].length; ++i) { if (typeof this.data[dataset][i].x === 'number') { xValues.push(this.data[dataset][i].x); } if (typeof this.data[dataset][i].y === 'number') { yValues.push(this.data[dataset][i].y); } } // These are the minimum and maximum X/Y values for this dataset var x1 = RGraph.SVG.arrayMin(xValues); var y1 = RGraph.SVG.arrayMin(yValues); var x2 = RGraph.SVG.arrayMax(xValues); var y2 = RGraph.SVG.arrayMax(yValues); // Convert the X/Y values into coordinates on the canvas x1 = this.getXCoord(x1); y1 = this.getYCoord(y1, properties.outofbounds); x2 = this.getXCoord(x2); y2 = this.getYCoord(y2, properties.outofbounds); // Create the SVG clipPath region var clippath = RGraph.SVG.create({ svg: this.svg, parent: this.svg.defs, type: 'clipPath', attr: { id: 'trendline-clippath-dataset-' + dataset } }); RGraph.SVG.create({ svg: this.svg, parent: clippath, type: 'rect', attr: { x: properties.trendlineClipping === false ? properties.marginLeft : x1 - margin, y: properties.trendlineClipping === false ? properties.marginTop : y2 - margin, width: properties.trendlineClipping === false ? (this.width - properties.marginLeft - properties.marginRight) : x2 - x1 + margin + margin, height: properties.trendlineClipping === false ? this.height - properties.marginTop - properties.marginBottom: y1 - y2 + margin + margin } }); // // The coordinates for the trendline // var x1 = this.getXCoord(coords[0][0]); var y1 = this.getYCoord(coords[0][1]); var x2 = this.getXCoord(coords[1][0]); var y2 = this.getYCoord(coords[1][1]); var line = RGraph.SVG.create({ svg: this.svg, parent: this.svgAllGroup, type: 'path', attr: { d: 'M{1} {2} L{3} {4}'.format(x1,y1,x2,y2), stroke: color, fill:'none', 'stroke-width': linewidth, 'stroke-dasharray': strokeDasharray, 'stroke-linecap': 'round', // Makes trendline clipping redundant in favour // of the trace effect //'clip-path': 'url(#trendline-clippath-dataset-' + dataset + ')' // 'clip-path': 'url(#trace-effect-clip)' } }); // Store the coordinates this.coordsTrendline[dataset] = [ [x1, y1], [x2, y2] ]; }; // // This function handles clipping to scale values. Because // each chart handles scales differently, a worker function // is needed instead of it all being done centrally. // // @param object clipPath The <clipPath> node // this.clipToScaleWorker = function (clipPath) { // The Regular expression is actually done by the // calling RGraph.clipTo.start() function in the core // library if (RegExp.$1 === 'min') from = this.min; else from = Number(RegExp.$1); if (RegExp.$2 === 'max') to = this.max; else to = Number(RegExp.$2); var width = this.width, y1 = this.getYCoord(from), y2 = this.getYCoord(to), height = Math.abs(y2 - y1), x = 0, y = Math.min(y1, y2); // Increase the height if the maximum value is "max" if (RegExp.$2 === 'max') { y = 0; height += this.properties.marginTop; } // Increase the height if the minimum value is "min" if (RegExp.$1 === 'min') { height += this.properties.marginBottom; } RGraph.SVG.create({ svg: this.svg, type: 'rect', parent: clipPath, attr: { x: x, y: y, width: width, height: height } }); // Now set the clip-path attribute on the first // Line charts all-elements group this.svgAllGroup.setAttribute( 'clip-path', 'url(#' + clipPath.id + ')' ); }; return this; }; // // This is a "wrapper" function that creaters a dual-color // trendline Line chart for you. Options to give to // the frunction are (the sole argument is an object): // // id: The id of the canvas tag // data: The data for the chart // options: The chart options that get applied to // both Line charts (the one above // and also the one below the trendline.) // optionsTop: With this option you can specify // configuration values that are specific to // the top chart (eg color) // optionsBottom: With this option you can specify // configuration values that are specific to // the bottom chart (eg color) // RGraph.SVG.Scatter.dualColorTrendline = function (args) { // Check that a trendline is enabled if(!args.options.trendline) { alert('[ALERT] A trendline is not enabled in your charts configuration'); return; } // // Draw the red part of the Scatter chart (the bottom // half) // var obj1 = new RGraph.SVG.Scatter({ id: args.id, data: RGraph.SVG.arrayClone(args.data, true), options: RGraph.SVG.arrayClone(args.options, true) }).draw(); // The coordinates of the first (and only) trendline var coords = obj1.coordsTrendline[0]; // // Calculate the coordinates for the top part of the chart // (above the trendline) // var coords_top = [ [0,coords[0][1]], ...coords, [obj1.width,coords[1][1]], [obj1.width, 0], [0,0] ]; // // Calculate the coordinates for the bottom part of the chart // (below the trendline) // var coords_bottom = [ [0,coords[0][1]], ...coords, [obj1.width,coords[1][1]], [obj1.width, obj1.height], [0,obj1.height] ]; // // Now that we have the coordinates, clipping can be // installed on the chart that's already been drawn // (the top part of the chart). // obj1.set('clip', coords_top); // Set any options that have been specified that are // specific to the top Scatter chart if (RGraph.SVG.isObject(args.optionsTop)) { for (i in args.optionsTop) { if (RGraph.SVG.isString(i)) { obj1.set(i, args.optionsTop[i]); } } } // // Create a new chart that's clipped to the bottom part // coordinates. // var obj2 = new RGraph.SVG.Scatter({ id: args.id, data: RGraph.SVG.arrayClone(args.data, true), options: { ...RGraph.SVG.arrayClone(args.options, true), clip: coords_bottom // Clip to the part of the canvas // that's below the trendline } }); // Set any options that have been specified that are // specific to the bottom Scatter chart if (RGraph.SVG.isObject(args.optionsBottom)) { for (i in args.optionsBottom) { if (RGraph.SVG.isString(i)) { obj2.set(i, args.optionsBottom[i]); } } } // // Now draw both of the charts using the RGraph.redraw // API function or the requested animation effect if ( RGraph.SVG.isString(args.animationEffect) && obj1[args.animationEffect] && obj1[args.animationEffect] ) { RGraph.SVG.clear(obj1.svg); var effect = args.animationEffect; var effectOptions = args.animationEffectOptions ? args.animationEffectOptions : {}; effectOptions.callback = function () { RGraph.SVG.runOnce('rgraph-svg-scatter-dual-color-effect-callback', function () { args.animationEffectCallback ? args.animationEffectCallback() : function () {}; }); } obj1[effect](effectOptions); obj2[effect](effectOptions); } else { RGraph.SVG.redraw(); } return [obj1, obj2]; }; // End module pattern })(window, document);