001/* Attribute for generating the HTML file with JavaScript to plot simulation 002 * results using the dygraphs library. The HTML file is generated by 003 * "Export to Web". 004 005 Copyright (c) 2012-2014 The Regents of the University of California. 006 All rights reserved. 007 Permission is hereby granted, without written agreement and without 008 license or royalty fees, to use, copy, modify, and distribute this 009 software and its documentation for any purpose, provided that the above 010 copyright notice and the following two paragraphs appear in all copies 011 of this software. 012 013 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 014 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 015 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 016 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 017 SUCH DAMAGE. 018 019 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 020 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 021 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 022 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 023 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 024 ENHANCEMENTS, OR MODIFICATIONS. 025 026 PT_COPYRIGHT_VERSION_2 027 COPYRIGHTENDKEY 028 029 */ 030 031package ptolemy.vergil.basic.export.web; 032 033import java.util.HashMap; 034 035import ptolemy.kernel.util.IllegalActionException; 036import ptolemy.kernel.util.NameDuplicationException; 037import ptolemy.kernel.util.NamedObj; 038 039/////////////////////////////////////////////////////////////////// 040//// DyGraphsJSPlotter 041/** 042 * Attribute for generating the HTML file with JavaScript to plot simulation 043 * results using the Dygraphs library. The HTML file is generated by 044 * "Export to Web". An @link IconLink attribute can be customized with 045 * the generated file to open the HTML page. 046 * <p> 047 * Configure the export parameters by double-clicking on the attribute, 048 * then click the "Configure" button). 049 * </p><p> 050 * <i>dataJSON</i> contains the data series to be plotted in the chart area of 051 * the page. Two JavaScript array formats are acceptable:</p> 052 * <ul> 053 * <li>[{name:"series 1", value:[[x1, y1], [x2, y2], ..., [xn, yn]]}, 054 * {name:"series 2", value:[[x'1, y'1], [x'2, y'2], ..., [x'n, y'n]]}, 055 * ...]</li> 056 * <li>[{name:"series 1", value:[{x:x1, y:y1}, {x:x2, y:y2}, ..., {x:xn, y:yn}]}, 057 * {name:"series 2", value:[{x:x'1, y:y'1}, {x:x'2, y:y'2}, ..., {x:x'n, y:y'n}]}, 058 * ...]</li> 059 * </ul> 060 * <i>eventsJSON</i> contains the event information series to be annotated in 061 * the chart area. Only one JavaScript array format is acceptable: 062 * <ul> 063 * <li>[{name:"series 1", value:[{x:x1, y:y1, text:"event 1 info"}, {x:x2, y:y2, 064 * text:"event 2 info"}, ...]}, {...}, ...]</li> 065 * </ul> 066 * 067 * @author Baobing (Brian) Wang 068 * @version $Id$ 069 * @since Ptolemy II 10.0 070 * @Pt.ProposedRating Red (cxh) 071 * @Pt.AcceptedRating Red (cxh) 072 */ 073public class DygraphsJSPlotterAttribute extends JSPlotterAttribute { 074 075 /** Construct an attribute that will generate HTML to plot using the Dygraphs 076 * library. 077 * @param container The container. 078 * @param name The name. 079 * @exception IllegalActionException If the superclass throws it. 080 * @exception NameDuplicationException If the superclass throws it. 081 */ 082 public DygraphsJSPlotterAttribute(NamedObj container, String name) 083 throws IllegalActionException, NameDuplicationException { 084 super(container, name); 085 height.setExpression("2"); 086 setExpression("Customize by clicking \"Configure\". DyGraphsJSPlotter\n" 087 + "is free under the MIT license: http://dygraphs.com/"); 088 yAxisMode.addChoice("logarithmic"); 089 horizontalAlign.setVisibility(NONE); 090 verticalAlign.setVisibility(NONE); 091 } 092 093 /////////////////////////////////////////////////////////////////// 094 //// protected methods //// 095 096 /** Provide content to the specified web exporter to be 097 * included in a web page for the container of this object. 098 * 099 * @param exporter The web exporter to write content to 100 * @exception IllegalActionException If evaluating the value 101 * of this parameter fails, or creating a web attribute fails. 102 */ 103 @Override 104 protected void _provideAttributes(WebExporter exporter) 105 throws IllegalActionException { 106 super._provideAttributes(exporter); 107 HashMap<String, String> config = getBasicConfig(); 108 109 // Add the graph title and other libraries. 110 insertHeaderContent(false, true, 111 "<title>" + config.get("graphTitle") + "</title>\n\n"); 112 113 for (String line : _otherLibs) { 114 insertHeaderContent(false, true, line); 115 } 116 117 // Write the parameters to the JavaScript function in the header. 118 insertHeaderContent(true, true, "var mainChart;\n"); 119 // insertHeaderContent(true, true, "$(function () {\n"); 120 insertHeaderContent(true, true, 121 "\tvar data = " + config.get("dataJSON") + ";\n"); 122 insertHeaderContent(true, true, 123 "\tvar events = " + config.get("eventsJSON") + ";\n"); 124 125 insertHeaderContent(true, true, "\tvar config = {graphTitle: '" 126 + config.get("graphTitle") + "'" + ", enableLegend: " 127 + config.get("enableLegend") + ", horizontalAlign: '" 128 + config.get("horizontalAlign") + "'" + ", verticalAlign: '" 129 + config.get("verticalAlign") + "'" + ", dataConnectWidth: " 130 + config.get("dataConnectWidth") + ", enableDataMarker: " 131 + config.get("enableDataMarker") + ", dataMarkerRadius: " 132 + config.get("dataMarkerRadius") + ", eventsConnectWidth: " 133 + config.get("eventsConnectWidth") + ", enableEventsMarker: " 134 + config.get("enableEventsMarker") + ", eventsMarkerRadius: " 135 + config.get("eventsMarkerRadius") + ", xAxisMode: '" 136 + config.get("xAxisMode") + "'" + ", drawVerticalGridLine: " 137 + config.get("drawVerticalGridLine") + ", xAxisTitle: '" 138 + config.get("xAxisTitle") + "'" + ", yAxisMode: '" 139 + config.get("yAxisMode") + "'" + ", drawHorizontalGridLine: " 140 + config.get("drawHorizontalGridLine") + ", yAxisTitle: '" 141 + config.get("yAxisTitle") + "'};\n\n"); 142 143 // Output the JavaScript code for data plotting to the header. 144 for (String line : _plotCodeStart) { 145 insertHeaderContent(true, false, line + "\n"); 146 } 147 148 // Insert custom content, if any 149 if (customContent != null && customContent.getExpression() != null 150 && !customContent.getExpression().equals("")) { 151 insertHeaderContent(true, false, customContent.getExpression()); 152 } 153 154 // Insert rest of plotcode 155 for (String line : _plotCodeEnd) { 156 insertHeaderContent(true, false, line + "\n"); 157 } 158 159 String widthConfig = Boolean.valueOf(config.get("autoResize")) 160 ? "min-width" 161 : "width"; 162 String heightConfig = Boolean.valueOf(config.get("autoResize")) 163 ? "min-height" 164 : "height"; 165 insertBodyContent("<div id=\"mainChart-container\" style=\"" 166 + widthConfig + ": " + config.get("graphWidth") + "px; " 167 + heightConfig + ": " + config.get("graphHeight") 168 + "px; margin: 0 auto\"></div>\n"); 169 insertBodyContent("<hr><div id=\"sub-container\" style=\"" + widthConfig 170 + ": " + config.get("graphWidth") 171 + "px; height: 280px; margin:0 auto\">\n"); 172 173 // Output the body content. 174 for (String line : _bodyContent) { 175 insertBodyContent(line + "\n"); 176 } 177 178 // Generate the HTML page. 179 WebElement webElement = WebElement.createWebElement(getContainer(), 180 "outputHTMLFileWebAttribute", "outputHTMLFileWebAttribute"); 181 webElement.setExpression(getHTMLPageContent()); 182 webElement.setParent(config.get("outputHTMLFile")); 183 exporter.defineElement(webElement, true); 184 185 // Save the data and events series to a separate file if required. 186 if (Boolean.valueOf(config.get("saveDataToFile"))) { 187 webElement = WebElement.createWebElement(getContainer(), 188 "outputDataFileWebAttribute", "outputDataFileWebAttribute"); 189 webElement.setExpression( 190 config.get("dataJSON") + "\n" + config.get("eventsJSON")); 191 webElement.setParent(config.get("outputDataFile")); 192 exporter.defineElement(webElement, true); 193 } 194 } 195 196 /////////////////////////////////////////////////////////////////// 197 //// private parameters //// 198 199 /** Other JavaScript libraries that are required. */ 200 private static String[] _otherLibs = { 201 "<script src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js\"></script>\n", 202 "<script src=\"http://dygraphs.com/dygraph-combined.js\"></script>\n" }; 203 204 /** HTML code specifying the layout of the HTML page. */ 205 private static String[] _bodyContent = { 206 "\t<div id=\"legend-container\" style=\"width: 25%; height:100%; float: left\"></div>", 207 "\t<div id=\"panel-container\" style=\"width: 25%; height:100%; float: left\"></div>", 208 "\t<div id=\"eventsInfo-container\" style=\"width: 49%; height:100%; float: right\"></div>", 209 "</div><hr>", "<h2>Instructions:</h2>", "<ol>", 210 "\t<li><b style=\"color:blue;\">Hide/show</b> a data trace or event trace by <b style=\"color:blue;\">clicking </b> the corresponding checkbox.</li>", 211 "\t<li>Get <b style=\"color:blue;\">all events</b> that happened at a particular point on the X axis by <b style=\"color:blue;\">single click</b> on a data point on the upper chart</li>", 212 "\t<li><b style=\"color:blue;\">Zoom-in/out</b> to/from an interval by <b style=\"color:blue;\">dragging</b> on the lower chart.</li>", 213 "</ol>" }; 214 215 /** JavaScript code to plot the figure using the Dygraphs library. 216 * Separated into start and end so custom content can be inserted. */ 217 private static String[] _plotCodeStart = { 218 "\t\t\t\tvar pieChart, dataTable, seriesLabels;\n", 219 "\t\t\t\t$(document).ready(function() {", 220 "\t\t\t\t\t// parse data and create main chart", 221 "\t\t\t\t\tparseJSON();", "\t\t\t\t\tcreateMainChart();", 222 "\t\t\t\t});", "\t\t\t\t\tfunction createMainChart() {", 223 "\t\t\t\t\t\tmainChart = new Dygraph(", 224 "\t\t\t\t\t\t\tdocument.getElementById(\"mainChart-container\"),", 225 "\t\t\t\t\t\t\tdataTable,", "\t\t\t\t\t\t\t{" }; 226 227 /** JavaScript code to plot the figure using the Dygraphs library. 228 * Separated into start and end so custom content can be inserted. */ 229 private static String[] _plotCodeEnd = { 230 "\t\t\t\t\t\t\t\tlabels: seriesLabels,", 231 "\t\t\t\t\t\t\t\txlabel: config.xAxisTitle,", 232 "\t\t\t\t\t\t\t\tylabel: config.yAxisTitle,", 233 "\t\t\t\t\t\t\t\ttitle: config.graphTitle,", 234 "\t\t\t\t\t\t\t\tavoidMinZero: true,", 235 "\t\t\t\t\t\t\t\tdrawXGrid: config.drawVerticalGridLine,", 236 "\t\t\t\t\t\t\t\tdrawYGrid: config.drawHorizontalGridLine,", 237 "\t\t\t\t\t\t\t\tlogscale: (config.yAxisMode == 'logarithmic'),", 238 "\t\t\t\t\t\t\t\tlegend: config.enableLegend ? 'always' : 'onmouseover',", 239 "\t\t\t\t\t\t\t\trangeSelectorHeight: 60,", 240 "\t\t\t\t\t\t\t\tshowRangeSelector: true,", 241 "\t\t\t\t\t\t\t\tconnectSeparatedPoints: true,", 242 "\t\t\t\t\t\t\t\tlabelsDiv: \"legend-container\",", 243 "\t\t\t\t\t\t\t\tlabelsSeparateLines: true,", 244 "\t\t\t\t\t\t\t\tpointClickCallback: function(e, point) {", 245 "\t\t\t\t\t\t\t\t\t$(\"#eventsInfo-container\").html('<b>Events at ' + ", 246 "\t\t\t\t\t\t\t\t\t\t\tparseDatetime(point.xval) + '</b>: <br>' +", 247 "\t\t\t\t\t\t\t\t\t\t\tgetEvents(point.xval));", 248 "\t\t\t\t\t\t\t\t}", "\t\t\t\t\t\t\t}", 249 "\t\t\t\t\t\t);// End of new Dygraph()\n", 250 "\t\t\t\t\t\t// customize for each series", 251 "\t\t\t\t\t\tvar seriesOpts = [], pointShapes = [], panelContent = '';", 252 "\t\t\t\t\t\tfor (var shape in Dygraph.Circles){", 253 "\t\t\t\t\t\t\tpointShapes.push(Dygraph.Circles[shape]);", 254 "\t\t\t\t\t\t}", 255 "\t\t\t\t\t\tvar strokePatterns = [null, [6, 3], [2, 3], [6, 2, 2, 2],", 256 "\t\t\t\t\t\t [6, 2, 2, 2, 2, 2], [2, 4],", 257 "\t\t\t\t\t\t [12, 5], [18, 5], [12, 5, 2, 5],", 258 "\t\t\t\t\t\t [18, 5, 2, 5], [18, 5, 2, 5, 2, 5]];\n", 259 "\t\t\t\t\t\tjQuery.each(seriesLabels, function(i, seriesName){", 260 "\t\t\t\t\t\t\t// add checkbox for visibility control", 261 "\t\t\t\t\t\t\tif (i >= 1){", 262 "\t\t\t\t\t\t\t\tpanelContent += '<input type=\"checkbox\" checked onClick=\"mainChart.setVisibility(' +", 263 "\t\t\t\t\t\t\t\t\t\t(i-1) + ', this.checked)\">' + seriesName + '<br>';", 264 "\t\t\t\t\t\t\t}\n", "\t\t\t\t\t\t\t// customize data series", 265 "\t\t\t\t\t\t\tif (isEventTrace(seriesName)){", 266 "\t\t\t\t\t\t\t\tseriesOpts[seriesName] = {", 267 "\t\t\t\t\t\t\t\t\t\tdrawPoints: config.enableEventsMarker,", 268 "\t\t\t\t\t\t\t\t\t\tstrokeWidth: config.eventsConnectWidth", 269 "\t\t\t\t\t\t\t\t};", "\t\t\t\t\t\t\t}else {", 270 "\t\t\t\t\t\t\t\tseriesOpts[seriesName] = {", 271 "\t\t\t\t\t\t\t\t\t\tdrawPoints: config.enableDataMarker,", 272 "\t\t\t\t\t\t\t\t\t\tpointSize: config.dataMarkerRadius,", 273 "\t\t\t\t\t\t\t\t\t\tdrawPointCallback: pointShapes[i % pointShapes.length],", 274 "\t\t\t\t\t\t\t\t\t\tstrokePattern: strokePatterns[i % strokePatterns.length],", 275 "\t\t\t\t\t\t\t\t\t\tstrokeWidth: config.dataConnectWidth,", 276 "\t\t\t\t\t\t\t\t\t\thighlightCircleSize: config.dataMarkerRadius + 2,", 277 "\t\t\t\t\t\t\t\t\t\tdrawHighlightPointCallback: pointShapes[i % pointShapes.length]", 278 "\t\t\t\t\t\t\t\t};", "\t\t\t\t\t\t\t}", "\t\t\t\t\t\t});", 279 "\t\t\t\t\t\t$(\"#panel-container\").html(panelContent);", 280 "\t\t\t\t\t\tmainChart.updateOptions(seriesOpts);", 281 "\t\t\t\t\t}// End of createMainChart()", "\t\t\t\t\t", 282 "\t\t\t\t\t// Parse the data and events series to the native format", 283 "\t\t\t\t\tfunction parseJSON(){", 284 "\t\t\t\t\t\tseriesLabels = ['x'], dataTable = [];", 285 "\t\t\t\t\t\tvar dataObj = {};", "\t\t\t\t\t\t", 286 "\t\t\t\t\t\tjQuery.each(data, function(i, dataTrace){", 287 "\t\t\t\t\t\t\tseriesLabels.push(dataTrace.name); ", 288 "\t\t\t\t\t\t\tjQuery.each(dataTrace.value, function(j, item){", 289 "\t\t\t\t\t\t\t\tvar rowObj = {}, pointObj = {};", 290 "\t\t\t\t\t\t\t\tpointObj[dataTrace.name] = item.y;", 291 "\t\t\t\t\t\t\t\trowObj[item.x] = pointObj;", 292 "\t\t\t\t\t\t\t\tdataObj = jQuery.extend(true, dataObj, rowObj);", 293 "\t\t\t\t\t\t\t});", "\t\t\t\t\t\t});", "\t\t\t\t\t\t", 294 "\t\t\t\t\t\tjQuery.each(events, function(i, eventTrace){", 295 "\t\t\t\t\t\t\tseriesLabels.push(eventTrace.name);", 296 "\t\t\t\t\t\t\tjQuery.each(eventTrace.value, function(j, item){", 297 "\t\t\t\t\t\t\t\tvar rowObj = {}, pointObj = {};", 298 "\t\t\t\t\t\t\t\tpointObj[eventTrace.name] = item.y;", 299 "\t\t\t\t\t\t\t\trowObj[item.x] = pointObj;", 300 "\t\t\t\t\t\t\t\tdataObj = jQuery.extend(true, dataObj, rowObj);", 301 "\t\t\t\t\t\t\t});", "\t\t\t\t\t\t});\n", 302 "\t\t\t\t\t\tjQuery.each(dataObj, function(x, rowObj){", 303 "\t\t\t\t\t\t\tvar row = [];", 304 "\t\t\t\t\t\t\tif (config.xAxisMode == 'datetime'){", 305 "\t\t\t\t\t\t\t\trow.push(new Date(Number(x)));", 306 "\t\t\t\t\t\t\t}else {", "\t\t\t\t\t\t\t\trow.push(Number(x));", 307 "\t\t\t\t\t\t\t}", 308 "\t\t\t\t\t\t\tjQuery.each(seriesLabels, function(i, name){", 309 "\t\t\t\t\t\t\t\tif (i >= 1){", 310 "\t\t\t\t\t\t\t\t\tif (name in rowObj){", 311 "\t\t\t\t\t\t\t\t\t\trow.push(rowObj[name]);", 312 "\t\t\t\t\t\t\t\t\t}else {", "\t\t\t\t\t\t\t\t\t\trow.push(null);", 313 "\t\t\t\t\t\t\t\t\t}", "\t\t\t\t\t\t\t\t}", "\t\t\t\t\t\t\t});", 314 "\t\t\t\t\t\t\tdataTable.push(row);", "\t\t\t\t\t\t});", 315 "\t\t\t\t\t}\n", 316 "\t\t\t\t\t// Get all events at a certain X axis point", 317 "\t\t\t\t\tfunction getEvents(xPoint){", 318 "\t\t\t\t\t\tvar text = '';", 319 "\t\t\t\t\t\tjQuery.each(events, function(i, eventTrace){", 320 "\t\t\t\t\t\t\tvar tempArray = jQuery.grep(eventTrace.value, function(item, j){", 321 "\t\t\t\t\t\t\t\treturn (item.x == xPoint);", "\t\t\t\t\t\t\t});", 322 "\t\t\t\t\t\t\t", "\t\t\t\t\t\t\tif (tempArray.length > 0){", 323 "\t\t\t\t\t\t\t\ttext += '<b>' + eventTrace.name + '</b>: ';", 324 "\t\t\t\t\t\t\t\tjQuery.each(tempArray, function(j, item){", 325 "\t\t\t\t\t\t\t\t\tif (j > 0)", "\t\t\t\t\t\t\t\t\t\ttext += ', ';", 326 "\t\t\t\t\t\t\t\t\ttext += item.text;", "\t\t\t\t\t\t\t\t});", 327 "\t\t\t \t\ttext += '<br>';", "\t\t\t\t\t\t\t}", 328 "\t\t\t\t\t\t});", "\t\t\t\t\t\treturn text||'None';", 329 "\t\t\t\t\t}", "", "\t\t\t\t\t// Parse Datetime", 330 "\t\t\t\t\tfunction parseDatetime(value){", 331 "\t\t\t\t\t\tif (config.xAxisMode == 'datetime')", 332 "\t\t\t\t\t\t\treturn new Date(value).toUTCString();", 333 "\t\t\t\t\t\telse ", "\t\t\t\t\t\t\treturn value;", "\t\t\t\t\t}\n", 334 "\t\t\t\t\tfunction isEventTrace(name){", 335 "\t\t\t\t\t\tvar count = 0;", 336 "\t\t\t\t\t\tjQuery.each(events, function(i, eventTrace){", 337 "\t\t\t\t\t\t\tif (eventTrace.name == name)", 338 "\t\t\t\t\t\t\t\tcount++;", "\t\t\t\t\t\t});", 339 "\t\t\t\t\t\treturn (count > 0);", "\t\t\t\t\t}" }; 340}