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}