// version: 2019-05-27 /** * o--------------------------------------------------------------------------------o * | This file is part of the RGraph package - you can learn more at: | * | | * | https://www.rgraph.net | * | | * | RGraph is licensed under the Open Source MIT license. That means that it's | * | totally free to use and there are no restrictions on what you can do with it! | * o--------------------------------------------------------------------------------o */ // modified: 2019-06-10 by WBB // changed some property defaults // added ellipse marker type and property checks // modified: 2019-11-09 by WBB // split 'rect' and 'square' marker types RGraph = window.RGraph || {isRGraph: true}; RGraph.SVG = RGraph.SVG || {}; // Module pattern (function (win, doc, undefined) { var RG = RGraph, ua = navigator.userAgent, ma = Math, win = window, doc = document; RG.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') { name = ret.name; value = ret.value; this.set(name, value); } } } else { var ret = RG.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 = RG.SVG.arrayClone(value); 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) { return this.properties[name]; }; this.id = conf.id; this.uid = RG.SVG.createUID(); this.container = document.getElementById(this.id); this.layers = {}; // MUST be before the SVG tag is created! this.svg = RG.SVG.createSVG({object: this,container: this.container}); this.isRGraph = true; this.width = Number(this.svg.getAttribute('width')); this.height = Number(this.svg.getAttribute('height')); this.data = conf.data; this.type = 'scatter'; this.coords = []; this.coords2 = []; this.colorsParsed = false; this.originalColors = {}; this.gradientCounter = 1; this.sequential = 0; this.line_groups = []; // This is a list of new property names that are used now in place of // the old names. // // *** When adding this list to a new chart library don't forget *** // *** the bit of code that also goes in the .set() function *** this.propertyNameAliases = { //colorsBackground: 'backgroundColor', //marginLeft: 'gutterLeft', //marginRight: 'gutterRight', //marginTop: 'gutterTop', //marginBottom: 'gutterBottom', //xaxisScaleFormatter: 'xaxisFormatter', //xaxisScaleThousand: 'xaxisThousand', //xaxisScalePoint: 'xaxisPoint', //xaxisScaleUnitsPre: 'xaxisUnitsPre', //xaxisScaleUnitsPost: 'xaxisUnitsPost', //xaxisScaleDecimals: 'xaxisDecimals', //xaxisScaleMax: 'xaxisMax', //xaxisScaleMin: 'xaxisMin', //yaxisScaleUnitsPre: 'yaxisUnitsPre', //yaxisScaleUnitsPost: 'yaxisUnitsPost', //yaxisScaleStrict: 'yaxisStrict', //yaxisScaleDecimals: 'yaxisDecimals', //yaxisScalePoint: 'yaxisPoint', //yaxisScaleThousand: 'yaxisThousand', //yaxisScaleRound: 'yaxisRound', //yaxisScaleMax: 'yaxisMax', //yaxisScaleMin: 'yaxisMin', //yaxisScaleFormatter: 'yaxisFormatter' /* [NEW]:[OLD] */ }; // Add this object to the ObjectRegistry RG.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, xmax: 0, tickmarksStyle: 'cross', tickmarksSize: 7, colors: ['black'], line: false, lineColors: 1, lineLinewidth: 1, errorbarsColor: 'black', errorbarsLinewidth: 1, errorbarsCapwidth: 10, yaxis: true, yaxisTickmarks: true, yaxisTickmarksLength: 3, yaxisColor: 'black', yaxisScale: true, yaxisLabels: null, yaxisLabelsOffsetx: 0, yaxisLabelsOffsety: 0, yaxisLabelsCount: 10, // WBB changed to match xaxis value yaxisScaleUnitsPre: '', yaxisScaleUnitsPost: '', yaxisScaleStrict: false, yaxisScaleDecimals: 0, yaxisScalePoint: '.', yaxisScaleThousand: ',', yaxisScaleRound: false, yaxisScaleMax: null, yaxisScaleMin: 0, yaxisScaleFormatter: null, xaxis: true, xaxisTickmarks: true, xaxisTickmarksLength: 3, // WBB changed to match yaxis value 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, textColor: 'black', textFont: 'Arial, Verdana, sans-serif', textSize: 12, textBold: false, textItalic: false, 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', tooltipsEvent: 'mousemove', 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, titleSubtitleX: null, titleSubtitleY: null, titleSubtitleHalign: 'center', titleSubtitleValign: 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, errorbars: null, errorbarsColor: 'black', errorbarsLinewidth: 1, errorbarsCapwidth: 10, }; // // Copy the global object properties to this instance // RG.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] && !RG.SVG.isArray(this.data[0])) { this.data = []; this.data[0] = conf.data; } /** * "Decorate" the object with the generic effects if the effects library has been included */ if (RG.SVG.FX && typeof RG.SVG.FX.decorate === 'function') { RG.SVG.FX.decorate(this); } var prop = this.properties; // // Convert string X values to timestamps // if (typeof prop.xaxisScaleMin === 'string') { prop.xaxisScaleMin = RG.SVG.parseDate(prop.xaxisScaleMin); } if (typeof prop.xaxisScaleMax === 'string') { prop.xaxisScaleMax = RG.SVG.parseDate(prop.xaxisScaleMax); } for (var i=0; i tag that the datapoints are added to // var group = RG.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 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') ) { // Convert the tooltip to a string data[i].tooltip = String(data[i].tooltip); // Make the tooltipsEvent default to click if (prop.tooltipsEvent !== 'mousemove') { prop.tooltipsEvent = 'click'; } if (!group_tooltip_hotspots) { var group_tooltip_hotspots = RG.SVG.create({ svg: this.svg, parent: this.svg.all, type: 'g', attr: { className: 'rgraph-scatter-tooltip-hotspots' } }); } var rect = RG.SVG.create({ svg: this.svg, parent: this.svg.all, type: 'rect', parent: group_tooltip_hotspots, attr: { x: ret.x - (ret.size / 2), y: ret.y - (ret.size / 2), width: ret.size, height: ret.size, fill: 'transparent', stroke: 'transparent', 'stroke-width': 0 }, style: { cursor: 'pointer' } }); // Add the hotspot to the original tickmark ret.mark.hotspot = rect; (function (dataset, index, seq, obj) { rect.addEventListener(prop.tooltipsEvent, function (e) { var tooltip = RG.SVG.REG.get('tooltip'); if (tooltip && tooltip.__dataset__ === dataset && tooltip.__index__ === index) { return; } obj.removeHighlight(); // Show the tooltip RG.SVG.tooltip({ object: obj, dataset: dataset, index: index, sequentialIndex: seq, text: obj.data[dataset][index].tooltip, event: e }); // Highlight the shape that has been clicked on if (RG.SVG.REG.get('tooltip')) { obj.highlight(this); } }, false); // Install the event listener that changes the // cursor if necessary if (prop.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) { var above = true; } else if (conf.labelAbove) { 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') { conf.type = conf.shape; } // set the type to the default if it's not set if (typeof conf.type === 'string') { // nada } else if (typeof prop.tickmarksStyle === 'string') { conf.type = prop.tickmarksStyle; } else if (typeof prop.tickmarksStyle === 'object' && typeof prop.tickmarksStyle[datasetIdx] === 'string') { conf.type = prop.tickmarksStyle[datasetIdx]; } // set the size to the default if it's not set if (typeof conf.size !== 'number' && typeof prop.tickmarksSize === 'number') { conf.size = prop.tickmarksSize; } else if (typeof conf.size !== 'number' && typeof prop.tickmarksSize === 'object' && typeof prop.tickmarksSize[datasetIdx] === 'number') { conf.size = prop.tickmarksSize[datasetIdx]; } // WBB adding optional 'rx' property // set the ellipse x-radius to the default, if it's not set if (typeof conf.rx !== 'number' && typeof prop.tickmarksSize === 'number') { conf.rx = prop.tickmarksSize; } else if (typeof conf.rx !== 'number' && typeof prop.tickmarksSize === 'object' && typeof prop.tickmarksSize[datasetIdx] === 'number') { conf.rx = prop.tickmarksSize[datasetIdx]; } // WBB adding optional 'ry' property // set the ellipse y-radius to the default, if it's not set if (typeof conf.ry !== 'number' && typeof prop.tickmarksSize === 'number') { conf.ry = prop.tickmarksSize; } else if (typeof conf.rx !== 'number' && typeof prop.tickmarksSize === 'object' && typeof prop.tickmarksSize[datasetIdx] === 'number') { conf.ry = prop.tickmarksSize[datasetIdx]; } // WBB adding optional 'rotate' property // set the rotation to 0, if undefined if (typeof conf.rotate === 'undefined') { conf.rotate = 0; } // Set the color to the default if it's not set and then black if that's not set either if (typeof conf.color === 'string') { // nada } else if (typeof prop.colors[datasetIdx] === 'string') { conf.color = prop.colors[datasetIdx]; } else { conf.color = 'black'; } // WBB adding optional 'stroke' property // Set the stroke to 'none', if not a string if (typeof conf.stroke !== 'string') { conf.stroke = 'none'; } // Set the opacity of this point if (typeof conf.opacity === 'undefined') { conf.opacity = 1; } else if (typeof conf.opacity === 'number') { // nada } // Draw the errorbar here // // First convert the errorbar information in the data into an array in the properties // prop.errorbars = []; for (var ds=0,max=0; ds prop.xaxisScaleMax) { return null; } if (value < prop.xaxisScaleMin) { return null; } x = ((value - prop.xaxisScaleMin) / (prop.xaxisScaleMax - prop.xaxisScaleMin)); x *= (this.width - prop.marginLeft - prop.marginRight); x = prop.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) { var prop = this.properties; if (value > this.scale.max) { return null; } var y, xaxispos = prop.xaxispos; if (value < this.scale.min) { return null; } y = ((value - this.scale.min) / (this.scale.max - this.scale.min)); y *= (this.height - prop.marginTop - prop.marginBottom); y = this.height - prop.marginBottom - y; return y; }; /** * This function can be used to highlight a bar on the chart * * @param object rect The rectangle to highlight */ this.highlight = function (rect) { rect.setAttribute('fill', prop.highlightFill); // Store the highlight rect in the registry so // it can be reset later RG.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 conf = opt.point, coordX = opt.coordX, coordY = opt.coordY; // Facilitate labelsAboveSpecific if (typeof conf.above === 'string') { var str = conf.above; } else { conf.x = RG.SVG.numberFormat({ object: this, num: conf.x.toFixed(prop.labelsAboveXDecimals ), prepend: typeof prop.labelsAboveXUnitsPre === 'string' ? prop.labelsAboveXUnitsPre : null, append: typeof prop.labelsAboveXUnitsPost === 'string' ? prop.labelsAboveXUnitsPost : null, point: typeof prop.labelsAboveXPoint === 'string' ? prop.labelsAboveXPoint : null, thousand: typeof prop.labelsAboveXThousand === 'string' ? prop.labelsAboveXThousand : null, formatter: typeof prop.labelsAboveXFormatter === 'function' ? prop.labelsAboveXFormatter : null }); conf.y = RG.SVG.numberFormat({ object: this, num: conf.y.toFixed(prop.labelsAboveYDecimals ), prepend: typeof prop.labelsAboveYUnitsPre === 'string' ? prop.labelsAboveYUnitsPre : null, append: typeof prop.labelsAboveYUnitsPost === 'string' ? prop.labelsAboveYUnitsPost : null, point: typeof prop.labelsAboveYPoint === 'string' ? prop.labelsAboveYPoint : null, thousand: typeof prop.labelsAboveYThousand === 'string' ? prop.labelsAboveYThousand : null, formatter: typeof prop.labelsAboveYFormatter === 'function' ? prop.labelsAboveYFormatter : null }); var str = '{1}{2}{3}'.format( conf.x, prop.labelsAboveSeparator, conf.y ); } // Add the text to the scene RG.SVG.text({ object: this, parent: this.svg.all, tag: 'labels.above', text: str, x: parseFloat(coordX) + prop.labelsAboveOffsetx, y: parseFloat(coordY) + prop.labelsAboveOffsety, halign: prop.labelsAboveHalign, valign: prop.labelsAboveValign, font: prop.labelsAboveFont || prop.textFont, size: typeof prop.labelsAboveSize === 'number' ? prop.labelsAboveSize : prop.textSize, bold: typeof prop.labelsAboveBold === 'boolean' ? prop.labelsAboveBold : prop.textBold, italic: typeof prop.labelsAboveItalic === 'boolean' ? prop.labelsAboveItalic : prop.textItalic, color: prop.labelsAboveColor || prop.textColor, background: prop.labelsAboveBackground || null, padding: prop.labelsAboveBackgroundPadding || 0 }); }; /** * 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: RG.SVG.arrayClone(prop.colors), backgroundGridColor: RG.SVG.arrayClone(prop.backgroundGridColor), highlightFill: RG.SVG.arrayClone(prop.highlightFill), backgroundColor: RG.SVG.arrayClone(prop.backgroundColor) } } // colors var colors = prop.colors; // IMPORTANT: Bubble chart gradients are parse in the drawBubble() // function below if (colors && !prop.bubble) { for (var i=0; i