// 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}; // // The gantt chart constructor // RGraph.Gantt = function (conf) { var id = conf.id var canvas = document.getElementById(id); var data = conf.data; this.id = id; this.canvas = canvas; this.context = this.canvas.getContext ? this.canvas.getContext("2d", {alpha: (typeof id === 'object' && id.alpha === false) ? false : true}) : null; this.canvas.__object__ = this; this.type = 'gantt'; 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.data = data; this.original_data = window.structuredClone(data); 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 // Set some defaults this.properties = { backgroundBarsCount: null, backgroundBarsColor1: 'rgba(0,0,0,0)', backgroundBarsColor2: 'rgba(0,0,0,0)', backgroundGrid: true, backgroundGridLinewidth: 1, backgroundGridColor: '#ddd', backgroundGridHsize: 20, backgroundGridVsize: 20, backgroundGridHlines: true, backgroundGridVlines: true, backgroundGridBorder: true, backgroundGridAlign: true, backgroundGridAutofit:true, backgroundGridAutofitAlign:true, backgroundGridHlinesCount: null, backgroundGridVlinesCount: null, backgroundVbars: [], backgroundHbars: [], backgroundBorder: false, backgroundBorderLinewidth: 1, backgroundBorderColor: '#aaa', backgroundBorderDashed: false, backgroundBorderDotted: false, backgroundBorderDashArray: null, textSize: 12, textFont: 'Arial, Verdana, sans-serif', textColor: 'black', textBold: false, textItalic: false, textAccessible: false, textAccessibleOverflow: 'visible', textAccessiblePointerevents: false, text: null, marginLeft: 75, marginRight: 35, marginTop: 35, marginBottom: 35, marginInner: 2, labelsInbar: null, labelsInbarBackground: null, labelsInbarAlign: 'left', labelsInbarSize: null, labelsInbarFont: null, labelsInbarColor: null, labelsInbarBold: null, labelsInbarItalic: null, labelsInbarAbove: false, labelsInbarOffsetx: 0, labelsInbarOffsety: 0, labelsComplete: true, labelsCompleteFont: null, labelsCompleteSize: null, labelsCompleteColor: null, labelsCompleteBold: null, labelsCompleteItalic: null, labelsCompleteOffsetx: 0, labelsCompleteOffsety: 0, title: '', titleX: null, titleY: null, titleBold: null, titleItalic: null, titleFont: null, titleSize: null, titleColor: null, titleHalign: null, titleValign: null, titleOffsetx: 0, titleOffsety: 0, titleSubtitle: '', titleSubtitleSize: null, titleSubtitleColor: '#aaa', titleSubtitleFont: null, titleSubtitleBold: null, titleSubtitleItalic: null, titleSubtitleOffsetx: 0, titleSubtitleOffsety: 0, xaxis: false, xaxisLinewidth: 1, xaxisColor: 'black', xaxisTickmarks: true, xaxisTickmarksLength: 3, xaxisTickmarksLastLeft: null, xaxisTickmarksLastRight: null, xaxisTickmarksCount: null, xaxisLabels: null, xaxisLabelsSize: null, xaxisLabelsFont: null, xaxisLabelsItalic: null, xaxisLabelsBold: null, xaxisLabelsColor: null, xaxisLabelsOffsetx: 0, xaxisLabelsOffsety: 0, xaxisLabelsHalign: null, xaxisLabelsValign: null, xaxisLabelsPosition: 'section', xaxisPosition: 'bottom', xaxisLabelsAngle: 0, xaxisTitle: null, xaxisTitleBold: null, xaxisTitleSize: null, xaxisTitleFont: null, xaxisTitleColor: null, xaxisTitleItalic: null, xaxisTitlePos: null, xaxisTitleOffsetx: 0, xaxisTitleOffsety: 0, xaxisTitleX: null, xaxisTitleY: null, xaxisTitleHalign: null, xaxisTitleValign: null, xaxisScaleMin: 0, xaxisScaleMax: 0, yaxis: false, yaxisColor: 'black', yaxisLinewidth: 1, yaxisTickmarks: false, yaxisTickmarksLength: null, yaxisTickmarksCount: null, yaxisTickmarksLastTop: null, yaxisTickmarksLastBottom: null, yaxisScale: false, yaxisLabelsPosition: 'section', yaxisLabels: null, // This is populated when the chart is first drawn yaxisLabelsFont: null, yaxisLabelsSize: null, yaxisLabelsColor: null, yaxisLabelsBold: null, yaxisLabelsItalic: null, yaxisLabelsOffsety: 0, yaxisLabelsOffsetx: 0, yaxisLabelsValign: 'center', yaxisLabelsHalign: 'right', yaxisTitle: '', yaxisTitleBold: null, yaxisTitleItalic: null, yaxisTitleFont: null, yaxisTitleSize: null, yaxisTitleColor: null, yaxisTitlePos: null, yaxisTitleOffsetx: null, yaxisTitleOffsety: null, yaxisTitleX: null, yaxisTitleY: null, yaxisTitleHalign: null, yaxisTitleValign: null, yaxisTitleAccessible: null, colorsDefault: 'white', borders: true, coords: [], tooltips: null, tooltipsEffect: 'slide', tooltipsCssClass: 'RGraph_tooltip', tooltipsCss: null, tooltipsHighlight: true, tooltipsEvent: 'click', tooltipsPersistent: false, tooltipsFormattedThousand: ',', tooltipsFormattedPoint: '.', tooltipsFormattedDecimals: 0, tooltipsFormattedUnitsPre: '', tooltipsFormattedUnitsPost: '', 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)', contextmenu: null, annotatable: false, annotatableColor: 'black', annotatableLinewidth: 1, adjustable: false, adjustableOnly: null, corners: 'square', cornersRoundRadius: 25, 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; } } // // Create the dollar objects so that functions can be added to them // if (!data) { alert('[GANTT] The Gantt chart event data is now supplied as the second argument to the constructor - please update your code'); } else { // Go through the data converting relevant args to numbers for (var i=0,idx=0; i<data.length; ++i) { if (typeof data[i].start === 'string') data[i].start = parseFloat(data[i].start); if (typeof data[i].duration === 'string') data[i].duration = parseFloat(data[i].duration); if (typeof data[i].complete === 'string') data[i].complete = parseFloat(data[i].complete); if (typeof data[i].linewidth === 'string') data[i].linewidth = parseFloat(data[i].linewidth); } } // Linearize the data (DON'T use RGraph.arrayLinearize() here) // Initialise this this.properties.yaxisLabels = []; for (var i=0,idx=0; i<data.length; ++i) { if (RGraph.isArray(this.data[i])) { for (var j=0; j<this.data[i].length; ++j) { this.data[i][j].index = j; this.data[i][j].dataset = i; this['$' + (idx++)] = this.data[i][j]; // Populate the label property if (this.data[i][j].label) { this.properties.yaxisLabels[i] = this.data[i][j].label; } } } else { this.data[i].index = 0; this.data[i].dataset = i; this['$' + (idx++)] = this.data[i]; // Populate the label property this.properties.yaxisLabels[i] = this.data[i].label; } } // Easy access to properties and the path function var properties = this.properties; this.path = RGraph.pathObjectFunction; // // "Decorate" the object with the generic effects if the effects library has been included // if (RGraph.Effects && typeof RGraph.Effects.decorate === 'function') { RGraph.Effects.decorate(this); } // Add the responsive method. This method resides in the common file. this.responsive = RGraph.responsive; // // A setter // this.set = function (name) { var value = typeof arguments[1] === 'undefined' ? null : arguments[1]; // Go through all of the properties and make sure // that they're using the correct capitalisation if (typeof name === 'string') { name = this.properties_lowercase_map[name.toLowerCase()] || name; } // Set the colorsParsed flag to false if the colors // property is being set if ( name === 'backgroundBarsColor1' || name === 'backgroundBarsColor2' || name === 'backgroundGridColor' || name === 'colorsDefault' || name === 'highlightStroke' || name === 'highlightFill' ) { this.colorsParsed = false; } // the number of arguments is only one and it's an // object - parse it for configuration data and return. if (arguments.length === 1 && typeof arguments[0] === 'object') { for (i in arguments[0]) { if (typeof i === 'string') { this.set(i, arguments[0][i]); } } return this; } // Fix labelsInBar* name name = name.replace('labelsInBar','labelsInbar'); // Property name change if (name === 'labelsInbarBgcolor') { name = 'labelsInbarBackground'; } properties[name] = value; return this; }; // // A peudo 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 properties[name]; }; // // Draws the chart // this.draw = function () { // // Fire the onbeforedraw event // RGraph.fireCustomEvent(this, 'onbeforedraw'); // Translate half a pixel for antialiasing purposes - but only if it hasn't been // done already // // MUST be the first thing done! // if (!this.canvas.__rgraph_aa_translated__) { this.context.translate(0.5,0.5); this.canvas.__rgraph_aa_translated__ = true; } // // Make the margins easy ro access // this.marginLeft = properties.marginLeft; this.marginRight = properties.marginRight; this.marginTop = properties.marginTop; this.marginBottom = properties.marginBottom; // // Stop this growing uncntrollably // this.coordsText = []; // // Parse the colors. This allows for simple gradient syntax // if (!this.colorsParsed) { this.parseColors(); // Don't want to do this again this.colorsParsed = true; } // // Work out the graphArea // this.graphArea = this.canvas.width - this.marginLeft - this.marginRight; this.graphHeight = this.canvas.height - this.marginTop - this.marginBottom; this.numEvents = this.data.length this.barHeight = this.graphHeight / this.numEvents; this.halfBarHeight = this.barHeight / 2; // Set the number of horizontal grid lines to the same as that of the // data items that we have. if (RGraph.isNullish(properties.backgroundGridHlinesCount)) { this.set('backgroundGridHlinesCount', this.data.length); } // // Populate the yaxisLabels property from the data. But only do this once // properties.yaxisLabelsSpecific = []; for (var i=0; i<this.data.length; ++i) { if (typeof this.data[i] === 'object' && typeof this.data[i][0] === 'object') { properties.yaxisLabelsSpecific.push(this.data[i][0].label); } else { properties.yaxisLabelsSpecific.push(this.data[i].label); } } properties.yaxisLabels = properties.yaxisLabelsSpecific; // // Install clipping // if (!RGraph.isNullish(this.properties.clip)) { RGraph.clipTo.start(this, this.properties.clip); } // // Draw the background // RGraph.Background.draw(this); // // Draw the labels at the top // this.drawLabels(); // // Draw the events // this.drawEvents(); // // Setup the context menu if required // if (properties.contextmenu) { RGraph.showContext(this); } // // Add custom text thats specified // RGraph.addCustomText(this); // // This installs the event listeners // RGraph.installEventListeners(this); // // End clipping // if (!RGraph.isNullish(this.properties.clip)) { RGraph.clipTo.end(); } // // Fire the onfirstdraw event // if (this.firstDraw) { this.firstDraw = false; RGraph.fireCustomEvent(this, 'onfirstdraw'); this.firstDrawFunc(); } // // Fire the RGraph draw event // RGraph.fireCustomEvent(this, 'ondraw'); // // Install any inline responsive configuration. This // should be last in the draw function - even after // the draw events. // RGraph.installInlineResponsive(this); 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; }; // // Draws the labels at the top and the left of the chart // this.drawLabels = function () { // Use the RGraph.drawXAxis() function to draw the X axis labels RGraph.drawXAxis(this); // Use the RGraph.drawYAxis() function to draw the Y axis labels RGraph.drawYAxis(this); }; // // Draws the events to the canvas // this.drawEvents = function () { var events = this.data; // // Reset the coords array to prevent it growing // this.coords = []; // // First draw the vertical bars that have been added // if (properties.backgroundVbars) { for (i=0,len=properties.backgroundVbars.length; i<len; ++i) { // Boundary checking if (properties.backgroundVbars[i][0] + properties.backgroundVbars[i][1] > properties.xaxisScaleMax) { properties.backgroundVbars[i][1] = 364 - properties.backgroundVbars[i][0]; } var barX = this.marginLeft + (( (properties.backgroundVbars[i][0] - properties.xaxisScaleMin) / (properties.xaxisScaleMax - properties.xaxisScaleMin) ) * this.graphArea), barY = this.marginTop, width = (this.graphArea / (properties.xaxisScaleMax - properties.xaxisScaleMin) ) * properties.backgroundVbars[i][1], height = this.canvas.height - this.marginTop - this.marginBottom; // Right hand bounds checking if ( (barX + width) > (this.canvas.width - this.marginRight) ) { width = this.canvas.width - this.marginRight - barX; } this.context.fillStyle = properties.backgroundVbars[i][2]; this.context.fillRect(barX, barY, width, height); } } // Now draw the horizontal bars if (properties.backgroundHbars) { for (i=0,len=properties.backgroundHbars.length; i<len; ++i) { if (properties.backgroundHbars[i]) { var barX = this.marginLeft, barY = ((this.canvas.height - this.marginTop - this.marginBottom) / this.data.length) * i + this.marginTop, width = this.graphArea, height = this.barHeight this.context.fillStyle = properties.backgroundHbars[i]; this.context.fillRect(barX, barY, width, height); } } } // Draw the events var sequentialIndex = 0; for (i=0; i<events.length; ++i) { if (typeof events[i].start === 'number') { this.drawSingleEvent( events[i], i, sequentialIndex++ ); } else { for (var j=0; j<events[i].length; ++j) { var index = j; this.drawSingleEvent( events[i][j], i, sequentialIndex++, index ); } } } }; // // Retrieves the bar (if any) that has been click on or is hovered over // // @param object e The event object // this.getShape = function (e) { var mouseXY = RGraph.getMouseXY(e), mouseX = mouseXY[0], mouseY = mouseXY[1]; // // Loop through the bars determining if the mouse is over a bar // for (var i=0,len=this.coords.length; i<len; i++) { if (RGraph.tooltipsHotspotIgnore(this, i)) { continue; } var left = this.coords[i][0], top = this.coords[i][1], width = this.coords[i][2], height = this.coords[i][3]; // // Draw a rect on the chart to test the click but // don't fill or stroke it. // this.rect( this.context, left, top, width, height, { begin: true, radius: this.properties.corners === 'round' ? this.properties.cornersRoundRadius : 0 } ); if ( this.context.isPointInPath(mouseX, mouseY) && (this.properties.clip ? RGraph.clipTo.test(this, mouseX, mouseY) : true) ) { if (RGraph.parseTooltipText && properties.tooltips) { var tooltip = RGraph.parseTooltipText(properties.tooltips, i); } var dataset = this.coords[i][4].dataset; var index = (this.coords[i][4] && typeof this.coords[i][4].index === 'number' ? this.coords[i][4].index : 0); var label = this.coords[i][4].label; var ret = { object: this, x: left, y: top, width: width, height: height, dataset: dataset, index: index, sequentialIndex: this.coords[i][5], label: label, tooltip: typeof tooltip === 'string' ? tooltip : null }; return ret; } } }; // Draws a single event this.drawSingleEvent = function (ev, dataset, sequentialIndex) { // Store the indexes on the original data ev.dataset = dataset; if (typeof arguments[3] === 'number') { ev.index = arguments[3] } var min = properties.xaxisScaleMin; this.context.beginPath(); this.context.strokeStyle = 'black'; this.context.fillStyle = ev.color ? ev.color : properties.colorsDefault; var barStartX = this.marginLeft + (((ev.start - min) / (properties.xaxisScaleMax - min)) * this.graphArea), barStartY = this.marginTop + (dataset * this.barHeight), barWidth = (ev.duration / (properties.xaxisScaleMax - min) ) * this.graphArea; // If the width is greater than the graph atrea, curtail it if ( (barStartX + barWidth) > (this.canvas.width - this.marginRight) ) { barWidth = this.canvas.width - this.marginRight - barStartX; } // Draw the bar storing the coordinates this.coords.push([ barStartX, barStartY + properties.marginInner, barWidth, this.barHeight - (2 * properties.marginInner), ev, sequentialIndex, ]); // draw the border around the bar if (properties.borders || typeof ev.border === 'number') { this.context.strokeStyle = typeof ev.border === 'string' ? ev.border : 'black'; this.context.lineWidth = (typeof ev.linewidth === 'number' ? ev.linewidth : 1); if (ev.linewidth !== 0) { this.rect( this.context, barStartX, barStartY + properties.marginInner, barWidth, this.barHeight - (2 * properties.marginInner), { stroke: this.context.strokeStyle, begin: true, radius: this.properties.corners === 'round' ? this.properties.cornersRoundRadius : 0 } ); } } // Not entirely sure what this does... if (RGraph.isNullish(ev.complete)) { this.context.fillStyle = ev.color ? ev.color : properties.colorsDefault; } else { this.context.fillStyle = ev.background ? ev.background : properties.colorsDefault; } this.rect( this.context, barStartX, barStartY + properties.marginInner, barWidth, this.barHeight - (2 * properties.marginInner), { fill: this.context.fillStyle, begin: true, radius: this.properties.corners === 'round' ? this.properties.cornersRoundRadius : 0 } ); // Work out the completeage indicator var complete = (ev.complete / 100) * barWidth; // Draw the % complete indicator. If it's greater than 0 if (typeof ev.complete === 'number') { this.context.fillStyle = ev.color ? ev.color : '#0c0'; // Draw the percent complete bar if the complete // option is given this.rect( this.context, barStartX, barStartY + properties.marginInner, (ev.complete / 100) * barWidth, this.barHeight - (2 * properties.marginInner), { fill: this.context.fillStyle, begin: true, radius: this.properties.corners === 'round' ? this.properties.cornersRoundRadius : 0 } ); // Don't necessarily have to draw the label if (properties.labelsComplete) { this.context.beginPath(); // Get the text configuration var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsComplete' }); RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: barStartX + barWidth + 5 + properties.labelsCompleteOffsetx, y: barStartY + this.halfBarHeight + properties.labelsCompleteOffsety, text: String(ev.complete) + '%', valign: 'center', tag: 'labels.complete' }); } } // // Draw the inbar label if it's defined // if (properties.labelsInbar && properties.labelsInbar[sequentialIndex]) { var label = String(properties.labelsInbar[sequentialIndex]), halign = properties.labelsInbarAlign == 'left' ? 'left' : 'center'; halign = properties.labelsInbarAlign == 'right' ? 'right' : halign; // Work out the position of the text if (halign == 'right') { var x = (barStartX + barWidth) - 5; } else if (halign == 'center') { var x = barStartX + (barWidth / 2); } else { var x = barStartX + 5; } // Draw the labels "above" the bar if (properties.labelsInbarAbove) { x = barStartX + barWidth + 5; halign = 'left'; } // Get the text configuration var textConf = RGraph.getTextConf({ object: this, prefix: 'labelsInbar' }); // Set the color this.context.fillStyle = properties.labelsInbarColor; RGraph.text({ object: this, font: textConf.font, size: textConf.size, color: textConf.color, bold: textConf.bold, italic: textConf.italic, x: x + properties.labelsInbarOffsetx, y: barStartY + this.halfBarHeight + properties.labelsInbarOffsety, text: label, valign: 'center', halign: halign, bounding: typeof properties.labelsInbarBackground == 'string', boundingStroke: 'transparent', boundingFill: typeof properties.labelsInbarBackground === 'string' ? properties.labelsInbarBackground : null, tag: 'labels.inbar' }); } }; // // Each object type has its own Highlight() function which highlights the appropriate shape // // @param object shape The shape to highlight // this.highlight = function (shape) { // First check for inverted highlighting if (typeof properties.highlightStyle === 'string' && properties.highlightStyle === 'invert') { for (var i=0; i<this.coords.length; ++i) { if (i !== shape.sequentialIndex) { this.rect( this.context, this.coords[i][0] - 0.5, this.coords[i][1] - 0.5, this.coords[i][2] + 1, this.coords[i][3] + 1, { fill: this.properties.highlightFill, stroke: this.properties.highlightStroke, begin: true, radius: this.properties.corners === 'round' ? this.properties.cornersRoundRadius : 0 } ); } } return; } if (typeof properties.highlightStyle === 'function') { (properties.highlightStyle)(shape); } else { this.rect( this.context, shape.x, shape.y, shape.width, shape.height, { fill: this.properties.highlightFill, stroke: this.properties.highlightStroke, begin: true, radius: this.properties.corners === 'round' ? this.properties.cornersRoundRadius : 0 } ); } }; // // 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); if ( mouseXY[0] > this.marginLeft && mouseXY[0] < (this.canvas.width - this.marginRight) && mouseXY[1] > this.marginTop && mouseXY[1] < (this.canvas.height - this.marginBottom) ) { return this; } }; // // This method handles the adjusting calculation for when the mouse is moved // // @param object e The event object // this.adjusting_mousemove = function (e) { // Handle adjusting for the Bar if (properties.adjustable && RGraph.Registry.get('adjusting') && RGraph.Registry.get('adjusting').uid == this.uid) { var bar = RGraph.Registry.get('adjusting.gantt'); if (bar) { var mouseXY = RGraph.getMouseXY(e), obj = bar.object, dataset = bar.dataset, index = bar.index, diff = ((mouseXY[0] - bar.mousex) / (obj.canvas.width - obj.marginLeft - obj.marginRight)) * properties.xaxisScaleMax, eventStart = bar.event_start || 0, duration = bar.event_duration, event = typeof obj.data[dataset][index] === 'object' ? obj.data[dataset][index] : obj.data[dataset] if (bar['mode'] === 'move') { diff = Math.round(diff); // Single event if (!RGraph.isArray(obj.data[dataset])) { event.start = eventStart + diff; if (eventStart + diff < 0) { obj.data[dataset].start = 0; } else if ((eventStart + diff + obj.data[dataset].duration) > properties.xaxisScaleMax) { obj.data[dataset].start = properties.xaxisScaleMax - obj.data[dataset].duration; } // Multiple events } else { var dataset = bar.dataset, index = typeof bar.index === 'number' ? bar.index : 0, event = obj.data[dataset][index]; event.start = eventStart + diff; if ( (eventStart + diff) < 0) { event.start = 0; } else if ( (eventStart + diff + event.duration) > properties.xaxisScaleMax ) { event.start = properties.xaxisScaleMax - event.duration; } } } else if (bar.mode == 'resize') { // // Account for the right hand gutter. Appears to be a FF bug // if (mouseXY[0] > (obj.canvas.width - obj.marginRight)) { mouseXY[0] = obj.canvas.width - obj.marginRight; } var diff = ((mouseXY[0] - RGraph.Registry.get('adjusting.gantt')['mousex']) / (obj.canvas.width - obj.marginLeft - obj.marginRight)) * properties.xaxisScaleMax; diff = Math.round(diff); // Single event if (!RGraph.isArray(obj.data[dataset])) { obj.data[dataset].duration = duration + diff; if (obj.data[dataset].duration < 0) { obj.data[dataset].duration = 1; } // Multiple events } else { obj.data[dataset][index].duration = duration + diff; if (obj.data[dataset][index].duration <= 0) { obj.data[dataset][index].duration = 1; } } } RGraph.resetColorsToOriginalValues(obj); //RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas); RGraph.fireCustomEvent(obj, 'onadjust'); } } }; // // Returns the X coordinate for the given value // // @param number value The desired value (eg minute/hour/day etc) // this.getXCoord = function (value) { var min = properties.xaxisScaleMin, max = properties.xaxisScaleMax, graphArea = this.canvas.width - this.marginLeft - this.marginRight; if (value > max || value < min) { return null; } var x = (((value - min) / (max - min)) * graphArea) + this.marginLeft; return x; }; // // Returns the value given EITHER the event object OR a two element array containing the X/Y coords // this.getValue = function (arg) { if (arg.length == 2) { var mouseXY = arg; // Two coords given } else { var mouseXY = RGraph.getMouseXY(arg); // event given } var mouseX = mouseXY[0], mouseY = mouseXY[1]; var value = (mouseX - this.marginLeft) / (this.canvas.width - this.marginLeft - this.marginRight); value *= (properties.xaxisScaleMax - properties.xaxisScaleMin); // Bounds checking if (value < properties.xaxisScaleMin || value > properties.xaxisScaleMax) { value = null; } return value; }; // // This allows for easy specification of gradients. Could optimise this not to repeatedly call parseSingleColors() // 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.data = window.structuredClone(this.data); this.original_colors.backgroundBarsColor1 = RGraph.arrayClone(properties.backgroundBarsColor1); this.original_colors.backgroundBarsColor2 = RGraph.arrayClone(properties.backgroundBarsColor2); this.original_colors.backgroundGridColor = RGraph.arrayClone(properties.backgroundGridColor); this.original_colors.colorsDefault = RGraph.arrayClone(properties.colorsDefault); this.original_colors.highlightStroke = RGraph.arrayClone(properties.highlightStroke); this.original_colors.highlightFill = RGraph.arrayClone(properties.highlightFill); } // // this.coords can be used here as gradients are only parsed on the SECOND draw - not the first. // A .redraw() is downe at the end of the first draw. // for (var i=0,sequentialIndex=0; i<this.data.length; ++i) { // Multiple events if ( RGraph.isArray(this.data[i]) && typeof this.data[i][0] === 'object' && typeof this.data[i][0].start === 'number' ) { for (var j=0,len=this.data[i].length; j<len; j+=1,sequentialIndex+=1) { this.data[i][j].background = this.parseSingleColorForGradient( this.data[i][j].background, {start: this.data[i][j].start, duration: this.data[i][j].duration} ); this.data[i][j].color = this.parseSingleColorForGradient( this.data[i][j].color, {start: this.data[i][j].start, duration: this.data[i][j].duration} ); } // Single event } else { if (typeof this.data[i].background === 'string') { this.data[i].background = this.parseSingleColorForGradient( this.data[i].background, {start: this.data[i].start, duration: this.data[i].duration} ); } if (typeof this.data[i].color === 'string') { this.data[i].color = this.parseSingleColorForGradient( this.data[i].color, {start: this.data[i].start, duration: this.data[i].duration} ); } ++sequentialIndex; } } properties.backgroundBarsColor1 = this.parseSingleColorForGradient(properties.backgroundBarsColor1); properties.backgroundBarsColor2 = this.parseSingleColorForGradient(properties.backgroundBarsColor2); properties.backgroundGridColor = this.parseSingleColorForGradient(properties.backgroundGridColor); properties.backgroundColor = this.parseSingleColorForGradient(properties.backgroundColor); properties.colorsDefault = this.parseSingleColorForGradient(properties.colorsDefault); properties.highlightStroke = this.parseSingleColorForGradient(properties.highlightStroke); properties.highlightFill = this.parseSingleColorForGradient(properties.highlightFill); }; // // Use this function to reset the object to the post-constructor state. Eg reset colors if // need be etc // this.reset = function () { }; // // This parses a single color value // // @param string color The color to parse // this.parseSingleColorForGradient = function (color) { var opts = arguments[1] || {}; if (!color || typeof color != 'string') { return color; } if (color.match(/^gradient\((.*)\)$/i)) { // Allow for JSON gradients if (color.match(/^gradient\(({.*})\)$/i)) { return RGraph.parseJSONGradient({object: this, def: RegExp.$1}); } var parts = RegExp.$1.split(':'), value = (opts.start + opts.duration) > properties.xaxisScaleMax ? properties.xaxisScaleMax : (opts.start + opts.duration); // Create the gradient var grad = this.context.createLinearGradient( typeof opts.start === 'number' ? this.getXCoord(opts.start) : this.marginLeft, 0, typeof opts.start === 'number' ? this.getXCoord(value) : this.canvas.width - this.marginRight, 0 ); var diff = 1 / (parts.length - 1); grad.addColorStop(0, RGraph.trim(parts[0])); for (var j=1; j<parts.length; ++j) { grad.addColorStop(j * diff, RGraph.trim(parts[j])); } } return grad ? grad : color; }; // // 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; } if (typeof this[type] !== 'function') { this[type] = func; } else { RGraph.addCustomEventListener(this, type, func); } return this; }; // // This function runs once only // (put at the end of the file (before any effects)) // this.firstDrawFunc = function () { }; // // Gantt chart Grow effect // // @param object obj Options for the grow effect // @param function Optional callback (a function) // this.grow = function () { // Cancel any stop request if one is pending this.cancelStopAnimation(); // Reset the data to the original this.data = window.structuredClone(this.original_data); var obj = this, opt = arguments[0] || {}, callback = arguments[1] ? arguments[1] : function () {}, canvas = obj.canvas, context = obj.context, numFrames = opt.frames || 30, frame = 0; original_events = window.structuredClone(obj.data); function iterator () { if (obj.stopAnimationRequested) { // Reset the flag obj.stopAnimationRequested = false; return; } RGraph.clear(obj.canvas); RGraph.redrawCanvas(obj.canvas); if (frame <= numFrames) { // Update the events for (var i=0,len=obj.data.length; i<len; ++i) { if (typeof obj.data[i] === 'object' && obj.data[i][0] && typeof obj.data[i][0] === 'object') { for (var j=0; j<obj.data[i].length; ++j) { obj.data[i][j].duration = (frame / numFrames) * original_events[i][j].duration; } } else { obj.data[i].duration = (frame / numFrames) * original_events[i].duration; } } obj.reset(); frame++; RGraph.Effects.updateCanvas(iterator); } else { callback(obj); } } iterator(); return this; }; // // Couple of functions that allow you to control the // animation effect // this.stopAnimation = function () { this.stopAnimationRequested = true; }; this.cancelStopAnimation = function () { this.stopAnimationRequested = false; }; // // This helps the Gantt reset colors when the reset function is called. // It handles going through the data and resetting the colors. // this.resetColorsToOriginalValues = function () { // // Copy the original colors over for single-event-per-line data // for (var i=0; i<this.original_colors.data.length; ++i) { if (this.original_colors.data[i].background) { this.data[i].background = this.original_colors.data[i].background ? RGraph.arrayClone(this.original_colors.data[i].background) : null; } if (this.original_colors.data[i].color) { this.data[i].color = RGraph.arrayClone(this.original_colors.data[i].color); } if (typeof this.original_colors.data[i][0] === 'object' && typeof this.original_colors.data[i][0].start === 'number') { for (var j=0,len2=this.original_colors.data[i].length; j<len2; ++j) { this.data[i][j].background = RGraph.arrayClone(this.original_colors.data[i][j].background); this.data[i][j].color = RGraph.arrayClone(this.original_colors.data[i][j].color); } } } }; // // This function resets the object - clearing it of any previously gathered info // this.reset = function () { this.resetColorsToOriginalValues(); this.colorsParsed = false; this.coordsText = []; this.original_colors = []; this.firstDraw = true; this.coords = []; }; // An unused function this.sequentialIndex2Grouped=function(){alert('[RGRAPH] Something went badly wrong - contact support');}; // Determines whether the Gant is adjustable or not this.isAdjustable = function (shape) { if (RGraph.isNullish(properties.adjustableOnly)) { return true; } else if (RGraph.isArray(properties.adjustableOnly) && shape && properties.adjustableOnly[shape.sequentialIndex]) { return true; } return false; }; // // A worker function that handles Bar chart specific tooltip substitutions // this.tooltipSubstitutions = function (opt) { var event = this['$' + opt.index]; var value = { start: event.start, duration: event.duration, complete: event.complete }; // // Return the values to the user // return { index: event.index, dataset: event.dataset, sequentialIndex: opt.index, value: value }; }; // // 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) { return { //label:, //color:, //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) coords = this.coords[args.index]; // Position the tooltip in the X direction args.tooltip.style.left = ( canvasXY[0] // The X coordinate of the canvas + coords[0] // The X coordinate of the point on the chart + (coords[2] / 2) // Add half of the width of the bar - (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 + coords[1] // The Y coordinate of the bar on the chart - tooltip.offsetHeight // The height of the tooltip - 10 // An arbitrary amount + obj.properties.tooltipsOffsety // Add any user defined offset ) + 'px'; // If the top of the tooltip is off the top of the page // then move the tooltip down if(parseFloat(args.tooltip.style.top) < 0) { args.tooltip.style.top = parseFloat(args.tooltip.style.top) + (coords[3] / 2) + 5 + 'px'; } }; // // This function draws a single bar on the chart // accounting for whether the rects should be rounded // or not. // // @param x number The X coordinate of the rectangle // @param y number The Y coordinate of the rectangle // @param width number The width of the rectangle // @param height number The height of the rectangle // @param object opt Various options including: // begin bool Whether a beginPath // is performed // radius int The extent of the // rounding. Set to 0 if // rounded corners are // not desired // stroke mixed The color of the // stroke. If left null // there is no stroke. // fill mixed The color of the // fill. If left null // there is no fill // this.rect = function (context, x, y, width, height, opt = {}) { var context = this.context; if (opt.begin) { context.beginPath(); } if (RGraph.isNumber(opt.radius) && opt.radius > 0) { context.roundRect(x, y, width, height, opt.radius); } else { context.rect(x, y, width, height); } if (opt.stroke) { context.strokeStyle = opt.stroke; context.stroke(); } if (opt.fill) { context.fillStyle = opt.fill; context.fill(); } }; // // 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 in the // RGraph.clipTo.start() function. // // @param string clip The clip string as supplied by the // user in the chart configuration // this.clipToScaleWorker = function (clip) { // The Regular expression is actually done by the // calling RGraph.clipTo.start() function in the core // library if (RegExp.$1 === 'min') from = 0; else from = Number(RegExp.$1); if (RegExp.$2 === 'max') to = this.properties.xaxisScaleMax; else to = Number(RegExp.$2); var x = this.marginLeft + (((from - properties.xaxisScaleMin) / (properties.xaxisScaleMax - properties.xaxisScaleMin)) * this.graphArea) width = ((to-from) / (properties.xaxisScaleMax - properties.xaxisScaleMin) ) * this.graphArea; // Change the X if the number is "min" if (RegExp.$1 === 'min') { x = 0; width += this.properties.marginLeft; } // Change the width if the number is "max" if (RegExp.$2 === 'max') { width = this.canvas.width - x; } this.path( 'sa b r % % % % cl', x, 0, width, this.canvas.height ); }; // // This function handles TESTING clipping to scale values. // Because each chart handles scales differently, a worker // function is needed instead of it all being done // centrally in the RGraph.clipTo.start() function. // // @param string clip The clip string as supplied by the // user in the chart configuration // this.clipToScaleTestWorker = function (clip) { // The Regular expression is actually done by the // calling RGraph.clipTo.start() function in the core // library if (RegExp.$1 === 'min') from = 0; else from = Number(RegExp.$1); if (RegExp.$2 === 'max') to = this.properties.xaxisScaleMax; else to = Number(RegExp.$2); var x = this.marginLeft + (((from - properties.xaxisScaleMin) / (properties.xaxisScaleMax - properties.xaxisScaleMin)) * this.graphArea) width = ((to-from) / (properties.xaxisScaleMax - properties.xaxisScaleMin) ) * this.graphArea; // Change the X if the number is "min" if (RegExp.$1 === 'min') { x = 0; width += this.properties.marginLeft; } // Change the width if the number is "max" if (RegExp.$2 === 'max') { width = this.canvas.width - x; } this.path( 'b r % % % %', x, 0, width, this.canvas.height ); }; // // Register the object // RGraph.register(this); // // This is the 'end' of the constructor so if the first argument // contains configuration data - handle that. // RGraph.parseObjectStyleConfig(this, conf.options); };