// 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.Effects = RGraph.Effects || {}; RGraph.Effects.Rose = RGraph.Effects.Rose || {}; // // The rose chart constuctor // RGraph.Rose = function (conf) { this.id = conf.id; this.canvas = document.getElementById(this.id); this.context = this.canvas.getContext ? this.canvas.getContext("2d") : null; this.data = conf.data; this.unmodified_data = RGraph.arrayClone(this.data); this.canvas.__object__ = this; this.type = 'rose'; this.isRGraph = true; this.isrgraph = true; this.rgraph = true; this.uid = RGraph.createUID(); this.canvas.uid = this.canvas.uid ? this.canvas.uid : RGraph.createUID(); this.colorsParsed = false; this.coordsText = []; this.original_colors = []; this.firstDraw = true; // After the first draw this will be false this.stopAnimationRequested = false;// Used to control the animations this.centerx = 0; this.centery = 0; this.radius = 0; this.max = 0; this.angles = []; this.angles2 = []; this.properties = { axes: false, axesColor: 'black', axesLinewidth: 1, axesTickmarks: true, backgroundGrid: true, backgroundGridColor: '#ccc', backgroundGridSize: null, backgroundGridRadialsCount: null, backgroundGridRadialsOffset: 0, backgroundGridCirclesCount: 5, // [TODO] Need linewidth setting centerx: null, centery: null, radius: null, anglesStart: 0, linewidth: 1, colors: ['rgba(255,0,0,0.5)', 'rgba(255,255,0,0.5)', 'rgba(0,255,255,0.5)', 'rgb(0,255,0)', 'gray', 'blue', 'rgb(255,128,255)','green', 'pink', 'gray', 'aqua'], colorsSequential: false, colorsAlpha: null, colorsStroke: 'rgba(0,0,0,0)', margin: 5, marginLeft: 35, marginRight: 35, marginTop: 35, marginBottom: 35, shadow: false, shadowColor: '#aaa', shadowOffsetx: 0, shadowOffsety: 0, shadowBlur: 15, title: '', titleBold: null, titleFont: null, titleSize: null, titleItalic: null, titleColor: null, titleX: null, titleY: null, titleHalign: null, titleValign: null, titleOffsetx: 0, titleOffsety: 0, titleSubtitle: '', titleSubtitleSize: null, titleSubtitleColor: '#aaa', titleSubtitleFont: null, titleSubtitleBold: null, titleSubtitleItalic: null, titleSubtitleOffsetx: 0, titleSubtitleOffsety: 0, labels: null, labelsFormattedDecimals: 0, labelsFormattedPoint: '.', labelsFormattedThousand: ',', labelsFormattedUnitsPre: '', labelsFormattedUnitsPost: '', labelsColor: null, labelsFont: null, labelsSize: null, labelsBold: null, labelsItalic: null, labelsPosition: 'center', labelsBoxed: false, labelsOffsetRadius: 0, labelsAxes: 'n', labelsAxesFont: null, labelsAxesSize: null, labelsAxesColor: null, labelsAxesBold: null, labelsAxesItalic: null, labelsAxesCount: 5, labelsAxesOffsetx: 0, labelsAxesOffsety: 0, textColor: 'black', textFont: 'Arial, Verdana, sans-serif', textSize: 12, textBold: false, textItalic: false, textAccessible: false, textAccessibleOverflow: 'visible', textAccessiblePointerevents: false, text: null, key: null, keyBackground: 'white', keyPosition: 'graph', keyHalign: 'right', keyShadow: false, keyShadowColor: '#666', keyShadowBlur: 3, keyShadowOffsetx: 2, keyShadowOffsety: 2, keyPositionGutterBoxed: false, keyPositionX: null, keyPositionY: null, keyColorShape: 'square', keyRounded: true, keyLinewidth: 1, keyColors: null, keyInteractive: false, keyinteractiveKeyHighlightChartLinewidth: 20, keyInteractiveHighlightChartStroke: 'black', keyInteractiveHighlightChartFill: 'rgba(255,255,255,0.7)', keyInteractiveHighlightLabel: 'rgba(255,0,0,0.2)', keyLabelsColor: null, keyLabelsFont: null, keyLabelsSize: null, keyLabelsBold: null, keyLabelsItalic: null, keyLabelsOffsetx: 0, keyLabelsOffsety: 0, keyFormattedDecimals: 0, keyFormattedPoint: '.', keyFormattedThousand: ',', keyFormattedUnitsPre: '', keyFormattedUnitsPost: '', keyFormattedValueSpecific: null, keyFormattedItemsCount: null, contextmenu: null, tooltips: null, tooltipsEvent: 'onclick', tooltipsEffect: 'slide', tooltipsCssClass: 'RGraph_tooltip', tooltipsCss: null, tooltipsHighlight: true, tooltipsFormattedThousand: ',', tooltipsFormattedPoint: '.', tooltipsFormattedDecimals: 0, tooltipsFormattedUnitsPre: '', tooltipsFormattedUnitsPost: '', tooltipsFormattedKeyColors: null, tooltipsFormattedKeyColorsShape: 'square', tooltipsFormattedKeyLabels: [], tooltipsFormattedListType: 'ul', tooltipsFormattedListItems: null, tooltipsFormattedTableHeaders: null, tooltipsFormattedTableData: null, tooltipsPointer: true, tooltipsPointerOffsetx: 0, tooltipsPointerOffsety: 0, tooltipsPositionStatic: true, tooltipsHotspotIgnore: null, highlightStroke: 'rgba(0,0,0,0)', highlightFill: 'rgba(255,255,255,0.7)', annotatable: false, annotatableColor: 'black', annotatableLinewidth: 1, resizable: false, resizableHandleAdjust: [0,0], resizableHandleBackground: null, adjustable: false, scaleMax: null, scaleMin: 0, scaleDecimals: null, scalePoint: '.', scaleThousand: ',', scaleUnitsPre: '', scaleUnitsPost: '', variant: 'stacked', variantThreedDepth: 10, exploded: 0, animationRoundrobinFactor: 1, animationRoundrobinRadius: true, animationGrowMultiplier: 1, segmentHighlight: false, segmentHighlightCount: null, segmentHighlightFill: 'rgba(0,255,0,0.5)', segmentHighlightStroke: 'rgba(0,0,0,0)', clearto: 'rgba(0,0,0,0)' } // // 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; } } // Go through the data converting it to numbers this.data = RGraph.stringsToNumbers(this.data); // // Create the $ objects. In the case of non-equi-angular rose charts it actually creates too many $ objects, // but it doesn't matter. // var linear_data = RGraph.arrayLinearize(this.data); this.data_seq = linear_data; // Add .data_seq this.data_arr = linear_data; // Add .data_arr for (var i=0; i 0 && properties.key.length >= 3) { this.centerx = this.centerx - this.marginRight + 5; } // User specified radius, centerx and centery if (typeof properties.centerx == 'number') this.centerx = properties.centerx; if (typeof properties.centery == 'number') this.centery = properties.centery; if (typeof properties.radius == 'number') this.radius = properties.radius; // // Allow the centerx/centery/radius to be a plus/minus // if (typeof properties.radius === 'string' && properties.radius.match(/^\+|-\d+$/) ) this.radius += parseFloat(properties.radius); if (typeof properties.centerx === 'string' && properties.centerx.match(/^\+|-\d+$/) ) this.centerx += parseFloat(properties.centerx); if (typeof properties.centery === 'string' && properties.centery.match(/^\+|-\d+$/) ) this.centery += parseFloat(properties.centery); // // Parse the colors for gradients. Its down here so that the center X/Y can be used // if (!this.colorsParsed) { this.parseColors(); // Don't want to do this again this.colorsParsed = true; } // 3D variant if (properties.variant.indexOf('3d') !== -1) { var scaleX = 1.5; this.context.setTransform( scaleX, 0, 0, 1, (this.canvas.width * scaleX - this.canvas.width) * -0.5, 0 ); } // // Work out the maximum value and the sum // if (RGraph.isNull(properties.scaleMax)) { // Work out the max var max = 0, data = this.data; for (var i=0; i0; i-=1) { this.centery -= 1; this.drawRose({storeAngles: false}); //RGraph.setShadow(this,'rgba(0,0,0,0)',0,0,0); RGraph.noShadow(this); // Make the segments darker for (var j=0,len=this.angles.length; j 0) { var num = (360 / properties.backgroundGridRadialsCount); var offset = properties.backgroundGridRadialsOffset; for (var i=0; i<=360; i+=num) { // Radius must be greater than 0 for Opera to work this.context.arc( this.centerx, this.centery, this.radius, ((i / (180 / RGraph.PI)) - RGraph.HALFPI) + this.startRadians + offset, (((i + 0.0001) / (180 / RGraph.PI)) - RGraph.HALFPI) + this.startRadians + offset, false ); this.context.lineTo(this.centerx, this.centery); } this.context.stroke(); } } if (properties.axes) { this.context.beginPath(); this.context.strokeStyle = properties.axesColor; this.context.lineWidth = properties.axesLinewidth; // Draw the X axis this.context.moveTo(this.centerx - this.radius, Math.round(this.centery) ); this.context.lineTo(this.centerx + this.radius, Math.round(this.centery) ); if (properties.axesTickmarks) { // Draw the X ends this.context.moveTo(Math.round(this.centerx - this.radius), this.centery - 5); this.context.lineTo(Math.round(this.centerx - this.radius), this.centery + 5); this.context.moveTo(Math.round(this.centerx + this.radius), this.centery - 5); this.context.lineTo(Math.round(this.centerx + this.radius), this.centery + 5); // Draw the X check marks for (var i=(this.centerx - this.radius); i<(this.centerx + this.radius); i+=(this.radius / 5)) { this.context.moveTo(Math.round(i), this.centery - 3); this.context.lineTo(Math.round(i), this.centery + 3.5); } // Draw the Y check marks for (var i=(this.centery - this.radius); i<(this.centery + this.radius); i+=(this.radius / 5)) { this.context.moveTo(this.centerx - 3, Math.round(i)); this.context.lineTo(this.centerx + 3, Math.round(i)); } } // Draw the Y axis this.context.moveTo(Math.round(this.centerx), this.centery - this.radius); this.context.lineTo(Math.round(this.centerx), this.centery + this.radius); if (properties.axesTickmarks) { // Draw the Y ends this.context.moveTo(this.centerx - 5, Math.round(this.centery - this.radius)); this.context.lineTo(this.centerx + 5, Math.round(this.centery - this.radius)); this.context.moveTo(this.centerx - 5, Math.round(this.centery + this.radius)); this.context.lineTo(this.centerx + 5, Math.round(this.centery + this.radius)); } // Stroke it this.context.closePath(); this.context.stroke(); } this.path('b c'); }; // // This method draws the data on the graph // this.drawRose = function () { var max = 0, data = this.data, margin = RGraph.toRadians(properties.margin), opt = arguments[0] || {}; this.context.lineWidth = properties.linewidth; // Move to the centre this.context.moveTo(this.centerx, this.centery); this.context.stroke(); // Stroke the background so it stays grey // Transparency if (properties.colorsAlpha) { this.context.globalAlpha = properties.colorsAlpha; } var sequentialIndex = 0; // // A non-equi-angular Rose chart // if (typeof properties.variant == 'string' && properties.variant.indexOf('non-equi-angular') !== -1) { var total=0; for (var i=0; i 1) { this.restrokeRose(); } } else { var sequentialColorIndex = 0; if (properties.shadow) { RGraph.setShadow( this, properties.shadowColor, properties.shadowOffsetx, properties.shadowOffsety, properties.shadowBlur ); } // // Draw regular segments here // for (var i=0; i 1) { this.restrokeRose(); } // // Now redraw the rose if the shadow is enabled so that // the rose appears over the shadow // if (properties.shadow) { this.redrawRose(); } } // Turn off the transparency if (properties.colorsAlpha) { this.context.globalAlpha = 1; } // Draw the title if any has been set if (properties.title) { RGraph.drawTitle(this); } }; // // This function redraws the stroke on the chart so that // the strokes appear above the fill // this.restrokeRose = function () { var angles = this.angles; for (var i=0; i -1) { // The offset for the labels if (properties.backgroundAxes) { var offset = -10; var halign = 'right'; } else { var offset = 0; var halign = 'center'; } var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsAxes' }); for (var i=0; i -1) { // The offset for the labels if (properties.backgroundAxes) { var offset = -10; var halign = 'right'; } else { var offset = 0; var halign = 'center'; } for (var i=0; i -1) { for (var i=0; i -1) { for (var i=0; i 0) { RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, 'x':centerx + properties.labelsAxesOffsetx, 'y':centery + properties.labelsAxesOffsety, 'text':typeof properties.scaleMin === 'number' ? RGraph.numberFormat({ object: this, number: Number(properties.scaleMin).toFixed(properties.scaleMin === 0 ? '0' : properties.scaleDecimals), unitspre: units_pre, unitspost: units_post }) : '0', 'valign':'center', 'halign':'center', 'bounding':true, 'bounding.fill':color, 'bounding.stroke':'rgba(0,0,0,0)', 'tag': 'scale' }); } }; // // Draws the circular labels that go around the charts // // @param labels array The labels that go around the chart // this.drawCircularLabels = function (context, labels, font, size, radius) { var variant = properties.variant, position = properties.labelsPosition, radius = radius + 5 + properties.labelsOffsetRadius, centerx = this.centerx, centery = this.centery + (properties.variant.indexOf('3d') !== -1 ? properties.variantThreedDepth : 0), labelsColor = properties.labelsColor || properties.textColor, angles = this.angles; var textConf = RGraph.getTextConf({ object: this, prefix: 'labels' }); for (var i=0; i centerx) { halign = 'left'; } else if (Math.round(x) == centerx) { halign = 'center'; } else { halign = 'right'; } RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: x, y: y, text: String(labels[i] || ''), halign: halign, valign: 'center', tag: 'labels', cssClass: RGraph.getLabelsCSSClassName({ object: this, name: 'labelsClass', index: i }) }); } }; // // This function is for use with circular graph types, eg the Pie or Rose. Pass it your event object // and it will pass you back the corresponding segment details as an array: // // [x, y, r, startAngle, endAngle] // // Angles are measured in degrees, and are measured from the "east" axis (just like the canvas). // // @param object e Your event object // @param object Options (OPTIONAL): // radius - whether to take into account // the radius of the segment // this.getShape = function (e) { var angles = this.angles; var ret = []; var opt = arguments[1] ? arguments[1] : {radius: true}; // // Go through all of the angles checking each one // for (var i=0; i 0) { this.path( 'a % % % % % true', this.angles[i][4], this.angles[i][5],this.angles[i][2],this.angles[i][1], this.angles[i][0] ); } else { this.path( 'l % %', this.angles[i][4], this.angles[i][5] ); } this.path( 'c s % f %', properties.highlightStroke, properties.highlightFill ); } } return; } // Add the new segment highlight this.path('b a % % % % % false',shape.x, shape.y, shape.radiusEnd, shape.angleStart, shape.angleEnd); if (shape.radiusStart > 0) { this.path('a % % % % % true',shape.x, shape.y, shape.radiusStart, shape.angleEnd, shape.angleStart); } else { this.path('l % %',shape.x, shape.y); } this.path('c s % f %', properties.highlightStroke, properties.highlightFill); } }; // // The getObjectByXY() worker method. Don't call this call: // // RGraph.ObjectRegistry.getObjectByXY(e) // // @param object e The event object // this.getObjectByXY = function (e) { var mouseXY = RGraph.getMouseXY(e); // Work out the radius var radius = RGraph.getHypLength(this.centerx, this.centery, mouseXY[0], mouseXY[1]); // Account for the 3D stretching effect if (properties.variant.indexOf('3d') !== -1) { radius /= -1; // Can be 100 because there's a radius check done as well var additional3D = 100; } else { var additional3D = 0; } if ( mouseXY[0] > (this.centerx - this.radius - additional3D) && mouseXY[0] < (this.centerx + this.radius + additional3D) && mouseXY[1] > (this.centery - this.radius) && mouseXY[1] < (this.centery + this.radius) && radius <= this.radius ) { return this; } }; // // This method gives you the relevant radius for a particular value // // @param number value The relevant value to get the radius for // this.getRadius = function (value) { // Range checking (the Rose minimum is always 0) if (value < 0 || value > this.max) { return null; } var r = (value / this.max) * this.radius; return r; }; // // This allows for easy specification of gradients // this.parseColors = function () { // Save the original colors so that they can be restored when the canvas is reset if (this.original_colors.length === 0) { this.original_colors.colors = RGraph.arrayClone(properties.colors); this.original_colors.keyColors = RGraph.arrayClone(properties.keyColors); this.original_colors.highlightStroke = RGraph.arrayClone(properties.highlightStroke); this.original_colors.highlightFill = RGraph.arrayClone(properties.highlightFill); } for (var i=0; i opt.startFrames[i]) { if (typeof obj.data[i] === 'number') { obj.data[i] = Math.min( Math.abs(original[i]), Math.abs(original[i] * ( (opt.counters[i]++) / framespersegment)) ); // Make the number negative if the original was if (original[i] < 0) { obj.data[i] *= -1; } } else if (!RGraph.isNull(obj.data[i])) { for (let j=0,len2=obj.data[i].length; j= opt.frames) { callback(obj); } else { RGraph.redrawCanvas(obj.canvas); RGraph.Effects.updateCanvas(iterator); } } iterator(); return this; }; // // Couple of functions that allow you to control the // animation effect // this.stopAnimation = function () { // Reset the clip this.set('animationGrowMultiplier', 1); // Reset the RoundRobin factor this.set('animationRoundrobinFactor', 1); // Set the original margin if (RGraph.isNumber(this.originalMargin)) { this.set('margin', this.originalMargin); } // Reset the exploded this.set('exploded', []); this.stopAnimationRequested = true; }; this.cancelStopAnimation = function () { this.stopAnimationRequested = false; }; // // A worker function that handles Bar chart specific tooltip substitutions // this.tooltipSubstitutions = function (opt) { var indexes = RGraph.sequentialIndexToGrouped(opt.index, this.data); var value = this.data_arr[opt.index]; var values = typeof this.data[indexes[0]] === 'number' ? [this.data[indexes[0]]] : this.data[indexes[0]]; if (properties.variant === 'non-equi-angular' && RGraph.isArray(this.data[indexes[0]])) { value = this.data[opt.index][0]; value2 = this.data[opt.index][1]; indexes = [opt.index, 0]; } return { index: indexes[1], dataset: indexes[0], sequentialIndex: opt.index, value: value, value2: typeof value2 === 'number' ? value2 : null, values: values }; }; // // 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 = properties.colors[index]; // Accommodate colorsSequential if (properties.colorsSequential) { color = colors[specific.sequential]; } // Different variations of the Rose chart // REGULAR CHART if (typeof this.data[specific.dataset] === 'number') { var label = properties.tooltipsFormattedKeyLabels[0] || ''; var color = properties.colors[0]; if (properties.tooltipsFormattedKeyColors && properties.tooltipsFormattedKeyColors[0]) { color = properties.tooltipsFormattedKeyColors[0]; } // NON-EQUI-ANGULAR CHART } else if (typeof this.data[specific.dataset] === 'object' && properties.variant === 'non-equi-angular') { // Don't show the second value on a non-equi-angular chart if (index === 0) { var color = colors[0]; var value = this.data[specific.dataset][0]; // Allow for specific tooltip key colors if (properties.tooltipsFormattedKeyColors && properties.tooltipsFormattedKeyColors[specific.index]) { color = properties.tooltipsFormattedKeyColors[specific.index]; } } else { var skip = true; } // STACKED CHART } else if (typeof this.data[specific.dataset] === 'object' && properties.variant !== 'non-equi-angular') { // Allow for specific tooltip key colors if (properties.tooltipsFormattedKeyColors && properties.tooltipsFormattedKeyColors[index]) { color = properties.tooltipsFormattedKeyColors[index]; } } //label = ( (typeof properties.tooltipsFormattedKeyLabels === 'object' && typeof properties.tooltipsFormattedKeyLabels[specific.index] === 'string') ? properties.tooltipsFormattedKeyLabels[specific.index] : ''); return { continue: skip, label: label, color: color, value: value }; }; // // This allows for static tooltip positioning // this.positionTooltipStatic = function (args) { var obj = args.object, e = args.event, tooltip = args.tooltip, index = args.index, canvasXY = RGraph.getCanvasXY(obj.canvas) segment = this.angles[args.index], shape = this.getShape(e), angle = ((shape.angleEnd - shape.angleStart) / 2) + shape.angleStart, multiplier = 0.5; var endpoint = RGraph.getRadiusEndPoint( shape.x, shape.y, angle, ((shape.radiusEnd - shape.radiusStart) / 2) + shape.radiusStart ); // Allow for the 3D stretching of the canvas if (properties.variant.indexOf('3d') !== -1) { var width = this.radius / 2; endpoint[0] = (endpoint[0] - this.centerx) * 1.5 + this.centerx; } // Position the tooltip in the X direction args.tooltip.style.left = ( canvasXY[0] // The X coordinate of the canvas + endpoint[0] // The X coordinate of the bar on the chart - (tooltip.offsetWidth / 2) // Subtract half of the tooltip width + obj.properties.tooltipsOffsetx // Add any user defined offset ) + 'px'; args.tooltip.style.top = ( canvasXY[1] // The Y coordinate of the canvas + endpoint[1] // The Y coordinate of the bar on the chart - tooltip.offsetHeight // The height of the tooltip + obj.properties.tooltipsOffsety // Add any user defined offset - 10 // Account for the pointer ) + 'px'; }; // // This returns the relevant value for the formatted key // macro %{value}. THIS VALUE SHOULD NOT BE FORMATTED. // // @param number index The index in the dataset to get // the value for // this.getKeyValue = function (index) { if ( RGraph.isArray(this.properties.keyFormattedValueSpecific) && RGraph.isNumber(this.properties.keyFormattedValueSpecific[index])) { return this.properties.keyFormattedValueSpecific[index]; // Allow for non-equi-angular Rose charts } else if (this.properties.variant === 'non-equi-angular') { var total = 0; for (let i=0; i