/** * VTK-Web Widget Library. * * This module extend jQuery object to add support for graphical components * related to 2D chart visualization. This widget depends on D3 and Rickshaw. * * @class jQuery.vtk.ui.Chart */ (function ($) { // ======================================================================= // ==== Defaults constant values ========================================= // ======================================================================= var GRAPH_HTML_TEMPLATE = [ "
", "", "", "", "", "", "" ]; // ======================================================================= function toNumber(str) { return Number(str.replace(/^\s+|\s+$/g, '')); } // ======================================================================= function extractColumnHeaderMap(headerLine) { var header = headerLine.split(','), colIdxMap = {}; for(var idx in header) { colIdxMap[header[idx]] = idx; } return colIdxMap; } // ======================================================================= function singleDataCSVConverter(inputString, outputSeries, options) { var lines = inputString.split('\n'), data = [], serie = $.extend({data:data}, options), nbLines = lines.length; // Process data for(var i = 1; i < nbLines; ++i) { var values = lines[i].split(','); if(values.length === 2) { item = { x: toNumber(values[0]), y: toNumber(values[1]) }; if(isNaN(item.y)) { item.y = null; } data.push(item); } } outputSeries.push(serie); } // ======================================================================= function multiDataCSVConverter(inputString, outputSeries, options) { var lines = inputString.split('\n'), header = lines[0].split(','), headerMap = extractColumnHeaderMap(lines[0]), nbLines = lines.length, nbValuesByLines = header.length, xHeaderName = options['x'], xIdx = headerMap[xHeaderName], series = [], palette = new Rickshaw.Color.Palette(); if(options.hasOwnProperty('palette') && options['palette'] !== null) { palette = options['palette']; } // Remove time field header.splice(header.indexOf(xHeaderName), 1); // Create series for(var idx in header) { var serie = { data: [], color: palette.color(), name: header[idx] }; series.push(serie); outputSeries.push(serie); } // Process data for(var i = 1; i < nbLines; ++i) { var values = lines[i].split(','); if(values.length === nbValuesByLines) { xValue = toNumber(values[xIdx]); for(var idx in header) { var item = { x: xValue, y: toNumber(values[headerMap[header[idx]]])}; if(isNaN(item.y)) { item.y = null; } if(!isNaN(item.x)) { series[idx].data.push(item); } } } } } // ======================================================================= function updateLegend(container) { var legendContainer = $('.vtk-legend', container), chart = container.data('chart'), legend = chart['legends'].basic; // Empty UI legendContainer.children("ul").empty(); // Update model if(legend !== undefined) { legend.lines = []; var series = chart.graph.series.map( function(s) { return s } ) series.forEach(function(s) { legend.addLine(s); }); } } // ======================================================================= /** * Method used to create a 2D chart based on some available data. * * @member jQuery.vtk.ui.Chart * @method vtkChart * @param {Object} configuration * * Usage: * var options = { * 'renderer': 'line', // Type of chart [line, area, bar, scatterplot] * 'stacked' : false, * 'series': [ * { * data: [ { x:0, y:0 }, { x:100, y:10 }, { x:200, y:5 }, { x:300, y:20 }, { x:400, y:25 }, { x:1000, y:-10 } ], * color: 'steelblue', * name: 'field 0' * },{ * data: [ { x:0, y:20 }, { x:100, y:30 }, { x:200, y:25 }, { x:300, y:40 }, { x:400, y:55 }, { x:1000, y:-10 } ], * color: 'lightblue', * name: 'field 1' * } * ], * 'axes': [ "bottom", "left", "top"], // Draw axis on border with scale * 'chart-padding': [0, 150, 50, 0], // Graph padding [top, right, bottom, left] in px. Useful to save space for legend * }; * * $('.chart-container-div').vtkChart(options); */ $.fn.vtkChart = function(options) { // Handle data with default values var opts = $.extend({},$.fn.vtkChart.defaults, options); return this.each(function() { var me = $(this).empty().addClass('vtk-chart'), container = $("", { html: GRAPH_HTML_TEMPLATE.join('').replace(/AXIS_SIZE /g, opts.axisThickness) }), chartContainer = $('.vtk-center', container), legendContainer = $('.vtk-legend', container), axisContainer = { bottom: $('.vtk-bottom', container)[0], top: $('.vtk-top', container)[0], left: $('.vtk-left', container)[0], right: $('.vtk-right', container)[0] }, annotationContainer = $('.vtk-annotation', container); me.append(container); // container.css('width', (opts['width']+(2*opts.axisThickness)) + 'px'); var graphOptions = { element: chartContainer[0], width: opts['width'], height: opts['height'], renderer: opts['renderer'], min: 'auto', stroke: true, series: opts['series'] }, graph = new Rickshaw.Graph(graphOptions), axes = [], legends = {}, annotator = null, data = { configuration: graphOptions, options: opts, palette: new Rickshaw.Color.Palette(), graph: graph, axes: axes, legends: legends }; graph.renderer.unstack = !opts.stacked; graph.render(); // Complete graph accessories // => Axis for(var idx in opts.axes) { var orientation = opts.axes[idx], axis = null; if(orientation === 'top' || orientation === 'bottom') { axis = new Rickshaw.Graph.Axis.X({graph: graph, orientation: orientation, element: axisContainer[orientation]}); } else { axis = new Rickshaw.Graph.Axis.Y({graph: graph, orientation: orientation, element: axisContainer[orientation]}); } axes.push(axis); } // => Legend if(opts.legend.basic) { legends['basic'] = new Rickshaw.Graph.Legend({graph: graph, element: legendContainer[0]}); // if(opts.legend.toggle) { // legends['toggle'] = new Rickshaw.Graph.Behavior.Series.Toggle({graph: graph, legend: legends['basic']}); // } // if(opts.legend.highlight) { // legends['highlight'] = new Rickshaw.Graph.Behavior.Series.Highlight({graph: graph, legend: legends['basic']}); // } } // => Hover if(opts.hover !== null) { data['hover'] = new Rickshaw.Graph.HoverDetail({ graph: graph, xFormatter: opts.hover.xFormatter, yFormatter: opts.hover.yFormatter }); } // => Annotation data['annotator'] = new Rickshaw.Graph.Annotate({ graph: graph, element: annotationContainer[0] }); for(var idx in opts.annotations) { var annotation = opts.annotations[idx]; data['annotator'].add(annotation['time'], annotation['message']); } // Handle auto-resize if(opts.autosize) { function autoResize() { var w = $(window), padding = opts['chart-padding'], thickness = opts.axisThickness, size = { width: me.width() - (2*thickness) - (padding[1] + padding[3]), height: me.height() - (2*thickness) - (padding[0] + padding[2])}; $('.vtk-bottom, .vtk-top, .vtk-annotation', me).css('height', thickness +'px').css('width', size['width'] +'px'); $('.vtk-left, .vtk-right', me).css('width', thickness +'px').css('height', size['height'] +'px'); $('.vtk-right', me).css('right', padding[1] + 'px').css('top', (padding[0] + thickness) + 'px'); $('.vtk-left', me).css('left', padding[3] + 'px').css('top', (padding[0] + thickness) + 'px'); $('.vtk-top', me).css('top', padding[0] + 'px').css('left', (thickness+padding[3]) + 'px'); $('.vtk-bottom, .vtk-annotation', me).css('left', (thickness+padding[3]) + 'px'); $('.vtk-bottom', me).css('bottom', padding[2] + 'px'); $('.vtk-center', me).css('width', (size['width'] - 2*thickness - (padding[1] + padding[3]))+'px').css('height', (size['height'] - 2*thickness - (padding[0] + padding[2]))+'px').css('left', (padding[3]+thickness) + 'px').css('top', (padding[0]+thickness) + 'px'); data.graph.configure(size); data.graph.update(); } $(window).resize(autoResize).trigger('resize'); } // Save data me.data('chart', data); graph.render(); }); }; // ======================================================================= /** * Method used to update the data of the 2D chart. * * @member jQuery.vtk.ui.Chart * @method vtkChartUpdateData * @param {Array} series * @param {boolean} replace previous series * * Usage: * var series: [ * { * data: [ { x:0, y:0 }, { x:100, y:10 }, { x:200, y:5 }, { x:300, y:20 }, { x:400, y:25 }, { x:1000, y:-10 } ], * color: 'steelblue', * name: 'field 0' * },{ * data: [ { x:0, y:20 }, { x:100, y:30 }, { x:200, y:25 }, { x:300, y:40 }, { x:400, y:55 }, { x:1000, y:-10 } ], * color: 'lightblue', * name: 'field 1' * } * ]; * * $('.chart-container-div').vtkChartUpdateData(series); */ $.fn.vtkChartUpdateData = function(series, replace) { return this.each(function() { var me = $(this), data = me.data('chart'), dataset = data['configuration']['series']; if(replace) { while(dataset.length > 0) { dataset.pop(); } } for(var idx in series) { data.graph.series.push(series[idx]); } data.graph.validateSeries(data.graph.series); data.graph.update(); updateLegend(me); }); } // ======================================================================= /** * Method used to update the data of the 2D chart. * * @member jQuery.vtk.ui.Chart * @method vtkChartFetchData * @param {Object} options * * Usage: * var options_json = { replace: true, url: "data.json", type: 'json', converter: null }; * var options_csv_1 = { replace: true, url: "data1.csv", type: 'csv-xy', options: { name: 'Temperature', color: palette.color(), ... } }; * var options_csv_n = { replace: true, url: "data2.csv", type: 'csv-x*', options: { x: 'time', palette: null } }; * * $('.chart-container-div').vtkChartFetchData(options_*); * * Where data looks like: * * data.json * [ * { * data: [ { x:0, y:0 }, { x:100, y:10 }, { x:200, y:5 }, { x:300, y:20 }, { x:400, y:25 }, { x:1000, y:-10 } ], * color: 'steelblue', * name: 'field 0' * },{ * data: [ { x:0, y:20 }, { x:100, y:30 }, { x:200, y:25 }, { x:300, y:40 }, { x:400, y:55 }, { x:1000, y:-10 } ], * color: 'lightblue', * name: 'field 1' * } * ] * * * data1.csv * x,y * 0,0 * 1,0.234 * 2,0.5 * 2.5,7 * * * data2.csv * time,x,y,z * 0,0,0,0 * 1,0.234,1.2,7.6 * 2,0.5,3,6 * 2.5,7,8,9 */ $.fn.vtkChartFetchData = function(info) { return this.each(function() { var me = $(this), data = me.data('chart'), options = info['options']; $.ajax({ url: info.url, dataType: "text" }).done(function(data){ var series = []; if (info.type === 'json') { series = $.parseJSON(data); } else if(info.type === 'csv-xy') { singleDataCSVConverter(data, series, options); } else if(info.type === 'csv-x*') { multiDataCSVConverter(data, series, options); } me.vtkChartUpdateData(series, info['replace']); }); }); } // ======================================================================= /** * Method used to update the data of the 2D chart. * * @member jQuery.vtk.ui.Chart * @method vtkChartConfigure * @param {Object} options * * Usage: * * var options = { * 'renderer': 'line', // Type of chart [line, area, bar, scatterplot] * 'stacked' : false, * 'axes': [ "bottom", "left", "top"], // Draw axis on border with scale * 'chart-padding': [0, 150, 50, 0], // Graph padding [top, right, bottom, left] in px. Useful to save space for legend * }; * $('.chart-container-div').vtkChartConfigure(options); */ $.fn.vtkChartConfigure = function(conf) { return this.each(function() { var me = $(this), data = me.data('chart'); var opts = $.extend(data['options']['configuration'], conf); $('.x_axis_d3', me).height(data.axisThickness + 'px').width(($(window).width()-(2*data.axisThickness)) + 'px'); data.graph.configure(opts); data.graph.update(); }); }; // ======================================================================= $.fn.vtkChart.defaults = { width: 300, height: 200, axisThickness: 25, autosize: true, stacked: false, renderer: "line", interpolation: "linear", series: [], hover: { xFormatter: function(x) { return x; }, yFormatter: function(y) {return y;} }, legend: { basic: true, toggle: true, highlight: true }, annotations: [], // { time: 0, message: "Just a text" } ... axes: [ "bottom", "left" ], 'chart-padding': [0, 0, 0, 0] }; // ======================================================================= }(jQuery));