Skip to content
Browse files

Added web gui

  • Loading branch information...
1 parent f3193c7 commit f116b2d656a29acc5de7fb0cf347d9f5f744e9d5 @zsolt-erl committed Jul 10, 2011
Showing with 1,708 additions and 0 deletions.
  1. +34 −0 priv/www/cadamgui/Manifest.json
  2. +80 −0 priv/www/cadamgui/config.json
  3. +72 −0 priv/www/cadamgui/generate.py
  4. +10 −0 priv/www/cadamgui/readme.txt
  5. +242 −0 priv/www/cadamgui/source/class/cadamgui/Application.js
  6. +241 −0 priv/www/cadamgui/source/class/cadamgui/Application_withWindows.js
  7. +309 −0 priv/www/cadamgui/source/class/cadamgui/QxSmoothie.js
  8. +48 −0 priv/www/cadamgui/source/class/cadamgui/WebSocket.js
  9. +59 −0 priv/www/cadamgui/source/class/cadamgui/WebSocket2.js
  10. +55 −0 priv/www/cadamgui/source/class/cadamgui/test/DemoTest.js
  11. +18 −0 priv/www/cadamgui/source/class/cadamgui/theme/Appearance.js
  12. +18 −0 priv/www/cadamgui/source/class/cadamgui/theme/Color.js
  13. +18 −0 priv/www/cadamgui/source/class/cadamgui/theme/Decoration.js
  14. +18 −0 priv/www/cadamgui/source/class/cadamgui/theme/Font.js
  15. +21 −0 priv/www/cadamgui/source/class/cadamgui/theme/Theme.js
  16. +9 −0 priv/www/cadamgui/source/index.html
  17. BIN priv/www/cadamgui/source/resource/cadamgui/close-button-hovered.png
  18. BIN priv/www/cadamgui/source/resource/cadamgui/close-button-pressed.png
  19. BIN priv/www/cadamgui/source/resource/cadamgui/close-button.png
  20. BIN priv/www/cadamgui/source/resource/cadamgui/close-button1.png
  21. BIN priv/www/cadamgui/source/resource/cadamgui/test.png
  22. +164 −0 priv/www/cadamgui/source/script/cadamgui.js
  23. +3 −0 priv/www/cadamgui/source/translation/readme.txt
  24. +29 −0 priv/www/metrics_ws_server_connector.yaws
  25. +260 −0 priv/www/smoothie.js
View
34 priv/www/cadamgui/Manifest.json
@@ -0,0 +1,34 @@
+{
+ "info" :
+ {
+ "name" : "cadamgui",
+
+ "summary" : "Custom Application",
+ "description" : "This is a skeleton for a custom application with qooxdoo.",
+
+ "homepage" : "http://some.homepage.url/",
+
+ "license" : "SomeLicense",
+ "authors" :
+ [
+ {
+ "name" : "First Author (uid)",
+ "email" : "first.author@some.domain"
+ }
+ ],
+
+ "version" : "trunk",
+ "qooxdoo-versions": ["1.2"]
+ },
+
+ "provides" :
+ {
+ "namespace" : "cadamgui",
+ "encoding" : "utf-8",
+ "class" : "source/class",
+ "resource" : "source/resource",
+ "translation" : "source/translation",
+ "type" : "application"
+ }
+}
+
View
80 priv/www/cadamgui/config.json
@@ -0,0 +1,80 @@
+{
+ "name" : "cadamgui",
+
+ "include" :
+ [
+ {
+ "path" : "${QOOXDOO_PATH}/tool/data/config/application.json"
+ }
+ ],
+
+ "export" :
+ [
+ "api",
+ "api-data",
+ "build",
+ "clean",
+ "distclean",
+ "fix",
+ "info",
+ "inspector",
+ "lint",
+ "migration",
+ "pretty",
+ "profiling",
+ "source",
+ "source-all",
+ "test",
+ "test-source",
+ "translation"
+ ],
+
+ "let" :
+ {
+ "APPLICATION" : "cadamgui",
+ "QOOXDOO_PATH" : "../../../../qoo12",
+// "QXTHEME" : "cadamgui.theme.Theme",
+ "QXTHEME" : "darktheme.DarkTheme",
+// "QXICONTHEME" : ["Tango"],
+ "API_EXCLUDE" : ["qx.test.*", "${APPLICATION}.theme.*", "${APPLICATION}.test.*"],
+ "LOCALES" : [ "en" ],
+ "CACHE" : "${TMPDIR}/cache",
+ "ROOT" : "."
+ },
+
+ // You only need to edit the remainder of this file, if you want to customize
+ // specific jobs, or add own job definitions.
+
+
+ "jobs" :
+ {
+ // Uncomment the following entry to add a contrib or library to your
+ // project; make sure to adapt the path to the Manifest.json; if you are
+ // using a contrib: library, it will be downloaded into the path specified
+ // by the 'cache/downloads' config key
+ "libraries" :
+ {
+ "library" :
+ [
+ {
+ "manifest" : "DarkTheme/trunk/Manifest.json"
+ }
+ ]
+ },
+
+ // If you want to tweak a job setting, see the following sample where
+ // the "format" feature of the "build-script" job is overridden.
+ // To see a list of available jobs, invoke 'generate.py x'.
+ "build-script" :
+ {
+ "compile-options" :
+ {
+ "code" :
+ {
+ "format" : false
+ }
+ }
+ }
+ }
+
+}
View
72 priv/www/cadamgui/generate.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+################################################################################
+#
+# qooxdoo - the new era of web development
+#
+# http://qooxdoo.org
+#
+# Copyright:
+# 2008 - 2009 1&1 Internet AG, Germany, http://www.1und1.de
+#
+# License:
+# LGPL: http://www.gnu.org/licenses/lgpl.html
+# EPL: http://www.eclipse.org/org/documents/epl-v10.php
+# See the LICENSE file in the project's top-level directory for details.
+#
+# Authors:
+# * Thomas Herchenroeder (thron7)
+#
+################################################################################
+
+##
+# This is a stub proxy for the real generator.py
+##
+
+import sys, os, re, subprocess
+
+CMD_PYTHON = 'python'
+QOOXDOO_PATH = '../../../../qoo12'
+
+def getQxPath():
+ path = QOOXDOO_PATH
+ # try updating from config file
+ if os.path.exists('config.json'):
+ # "using QOOXDOO_PATH from config.json"
+ qpathr=re.compile(r'"QOOXDOO_PATH"\s*:\s*"([^"]*)"\s*,?')
+ conffile = open('config.json')
+ aconffile = conffile.readlines()
+ for line in aconffile:
+ mo = qpathr.search(line)
+ if mo:
+ path = mo.group(1)
+ break # assume first occurrence is ok
+ path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), path))
+
+ return path
+
+os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) # switch to skeleton dir
+qxpath = getQxPath()
+REAL_GENERATOR = os.path.join(qxpath, 'tool', 'bin', 'generator.py')
+
+if not os.path.exists(REAL_GENERATOR):
+ print "Cannot find real generator script under: \"%s\"; aborting" % REAL_GENERATOR
+ sys.exit(1)
+
+argList = []
+argList.append(CMD_PYTHON)
+argList.append(REAL_GENERATOR)
+argList.extend(sys.argv[1:])
+if sys.platform == "win32":
+ argList1=[]
+ for arg in argList:
+ if arg.find(' ')>-1:
+ argList1.append('"%s"' % arg)
+ else:
+ argList1.append(arg)
+ argList = argList1
+else:
+ argList = ['"%s"' % x for x in argList] # quote argv elements
+
+cmd = " ".join(argList)
+retval = subprocess.call(cmd, shell=True)
+sys.exit(retval)
View
10 priv/www/cadamgui/readme.txt
@@ -0,0 +1,10 @@
+GUI Skeleton - A qooxdoo Application Template
+=============================================
+
+This is a qooxdoo application skeleton which is used as a template. The
+'create-application.py' script (usually under tool/bin/create-application.py)
+will use this and expand it into a self-contained qooxdoo application which
+can then be further extended. Please refer to the script and other documentation
+for further information.
+
+short:: is a standard qooxdoo GUI application
View
242 priv/www/cadamgui/source/class/cadamgui/Application.js
@@ -0,0 +1,242 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors: Zsolt Keszthelyi <zsolt.erl@gmail.com>
+
+************************************************************************ */
+
+/* ************************************************************************
+
+#asset(cadamgui/*)
+
+************************************************************************ */
+
+qx.Class.define("cadamgui.Application",
+{
+ extend : qx.application.Standalone,
+
+
+
+ /*
+ *****************************************************************************
+ MEMBERS
+ *****************************************************************************
+ */
+
+ members :
+ {
+ timeseries : [],
+ metric_routes : [],
+ ws : {},
+
+ /**
+ * This method contains the initial application code and gets called
+ * during startup of the application
+ *
+ * @lint ignoreDeprecated(alert)
+ */
+ main : function()
+ {
+ // Call super class
+ this.base(arguments);
+
+ // Enable logging in debug variant
+ if (qx.core.Variant.isSet("qx.debug", "on"))
+ {
+ // support native logging capabilities, e.g. Firebug for Firefox
+ qx.log.appender.Native;
+ // support additional cross-browser console. Press F7 to toggle visibility
+ qx.log.appender.Console;
+ }
+
+ var root = this.getRoot();
+ var timeseries = this.timeseries;
+ var metric_routes = this.metric_routes;
+
+ /*
+ * ---------------------------------------------------------------------------
+ * CREATE UI
+ * ---------------------------------------------------------------------------
+ */
+
+ // set up background
+ // var decorator=new qx.ui.decoration.Single();
+ // decorator.setBackgroundImage("cadam/bl1280.jpg");
+ // root.set({decorator:decorator});
+
+ var mainContainer = new qx.ui.container.Composite( new qx.ui.layout.VBox(10) );
+ var headerContainer = new qx.ui.container.Composite( new qx.ui.layout.VBox(10) );
+ var workContainer = new qx.ui.container.Composite( new qx.ui.layout.HBox() );
+
+ root.add(mainContainer);
+ mainContainer.add(headerContainer);
+ mainContainer.add(workContainer);
+
+ var menubar = new qx.ui.toolbar.ToolBar();
+ menubar.setWidth(2500);
+ menubar.setFont( new qx.bom.Font(16, ["Verdana", "Courier"]).set({bold: true}) );
+
+ menubar.add( new qx.ui.basic.Label("CADAM v0.1").set({padding: 7}) );
+ menubar.add( new qx.ui.toolbar.Separator() );
+ var toolsMenu = new qx.ui.toolbar.MenuButton("Tools");
+ menubar.add( toolsMenu );
+ menubar.add( new qx.ui.toolbar.Separator() );
+ var usageLabel = new qx.ui.basic.Label().set(
+ {
+ value: "Drag and Drop a metric from the list onto one of the empty gray containers!",
+ marginLeft: 20,
+ alignY: "middle"
+ });
+ menubar.add( usageLabel );
+
+ headerContainer.add(menubar);
+
+
+ var metricsList = new qx.ui.form.List().set(
+ {
+ textColor: "black",
+ font: new qx.bom.Font(12, ["Verdana", "Courier"]),
+ margin: 20,
+ width: 350,
+ height: 530,
+ draggable: true
+ });
+ workContainer.add(metricsList);
+
+ metricsList.addListener("dragstart", function(e)
+ {
+ e.addAction("move");
+ e.addType("metricid");
+ });
+
+ metricsList.addListener("droprequest", function(e)
+ {
+ var type = e.getCurrentType();
+ var action = e.getCurrentAction();
+ if ((type=="metricid") && (action=="move")) {
+ var metricid = this.getSelection()[0].getModel();
+ e.addData(type, metricid);
+ }
+ });
+
+ // create grid
+ var dashboardGrid = new qx.ui.container.Composite( new qx.ui.layout.Grid(10,10) );
+ dashboardGrid.set(
+ {
+ marginTop: 20
+ });
+ workContainer.add( dashboardGrid );
+
+ // populate grid with empty containers
+ var widgets = this.widgets = [];
+ for (var i=0; i<2; i++) {
+ widgets[i] = [];
+ for (var j=0; j<3; j++) {
+ widgets[i][j] = new qx.ui.container.Composite( new qx.ui.layout.VBox() ).set(
+ {
+ backgroundColor: "#3a3a3a",
+ width: 400,
+ height: 170,
+ droppable: true
+ });
+ widgets[i][j].addListener("drop", this.showChart, this);
+ dashboardGrid.add(widgets[i][j], {column: i, row: j});
+ };
+ };
+
+
+ /*
+ * ---------------------------------------------------------------------------
+ * COMMUNICATE WITH SERVER
+ * ---------------------------------------------------------------------------
+ */
+
+ // create a function to handle messages from server
+ var wsonmessage = function(m) {
+ if (m.data){
+ var message = qx.util.Json.parse(m.data);
+ console.log(message);
+ switch (message.response_for) {
+ case 'get_all_metrics' :
+ var metrics = message.response;
+ var listItem;
+ metricsList.removeAll();
+ for (var i=0; i<metrics.length; i++) {
+ listItem = new qx.ui.form.ListItem(metrics[i][0]+" - "+metrics[i][1], null, metrics[i]);
+ metricsList.add(listItem);
+ }
+ break;
+ case 'subscription' :
+ var metricid = message.response.metricid;
+ var value = message.response.value;
+ var ts = metric_routes[ metricid ];
+ // append new value to timeseries
+ ts.append(new Date().getTime(), value);
+ break;
+ default :
+ this.parent.debug("unexpected response:"+message.response_for);
+ }
+ }
+ };
+
+ // create websocket object
+ var ws = this.ws = new cadamgui.WebSocket2();
+ // overwrite onmessage call of the websocket
+ ws.onmessage = wsonmessage;
+ ws.connect("ws://localhost:9000/metrics_ws_server_connector.yaws");
+ ws.addListener("socket-ready", function()
+ {
+ ws.sendJSON({cmd: "get_all_metrics"});
+ });
+ },
+
+ // e is the drop event, 'this' is the qx.Application
+ showChart : function(e)
+ {
+ var ContainerWidget = e.getTarget();
+ var metricID = e.getData("metricid");
+
+ var Header = new qx.ui.container.Composite( new qx.ui.layout.HBox(0, "right"));
+ ContainerWidget.add(Header);
+
+ // add label
+ var Label = new qx.ui.basic.Label( metricID[0]+' - '+metricID[1]).set(
+ {
+ textColor: "yellow",
+ font: new qx.bom.Font(12, ["Verdana", "Courier"]),
+ marginTop: 2,
+ marginRight: 20
+ });
+ Header.add(Label);
+
+ var CloseButton = new qx.ui.basic.Image("cadamgui/close-button.png");
+ CloseButton.addListener("click", function(e){
+ this.ws.sendJSON({cmd: "unsubscribe_from_queue", metricid: metricID, queuenum: 1});
+ // take timeline out of metric_routes array
+ delete this.metric_routes[metricID];
+ ContainerWidget.removeAll();
+ }, this);
+
+
+ Header.add(CloseButton);
+
+ // create/add smoothie chart
+ var chartOptions = {grid: {verticalSections : 2, millisPerLine : 10000}, maxValueScale: 1.1,
+ fps : 20, millisPerPixel : 100};
+ var chart = new cadamgui.QxSmoothie(chartOptions, 4000).set(
+ {
+ canvasWidth: 400,
+ maxWidth: 400,
+ canvasHeight: 150,
+ height: 150
+ });
+ ContainerWidget.add(chart);
+ var ts = chart.getTimeseries();
+ this.metric_routes[metricID] = ts;
+ this.ws.sendJSON({cmd: "subscribe_to_queue", metricid: metricID, queuenum: 1});
+ }
+ }
+});
View
241 priv/www/cadamgui/source/class/cadamgui/Application_withWindows.js
@@ -0,0 +1,241 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors: Zsolt Keszthelyi <zsolt.erl@gmail.com>
+
+************************************************************************ */
+
+/* ************************************************************************
+
+#asset(cadamgui/*)
+
+************************************************************************ */
+
+qx.Class.define("cadamgui.Application_withWindows",
+{
+ extend : qx.application.Standalone,
+
+
+
+ /*
+ *****************************************************************************
+ MEMBERS
+ *****************************************************************************
+ */
+
+ members :
+ {
+ timeseries : [],
+ metric_routes : [],
+ ws : {},
+
+ gridTop : 120,
+ gridLeft : 400,
+ gridSpaceX : 20,
+ gridSpaceY : 20,
+ windowWidth : 404,
+ windowHeight : 154,
+
+
+
+ /**
+ * This method contains the initial application code and gets called
+ * during startup of the application
+ *
+ * @lint ignoreDeprecated(alert)
+ */
+ main : function()
+ {
+ // Call super class
+ this.base(arguments);
+
+ // Enable logging in debug variant
+ if (qx.core.Variant.isSet("qx.debug", "on"))
+ {
+ // support native logging capabilities, e.g. Firebug for Firefox
+ qx.log.appender.Native;
+ // support additional cross-browser console. Press F7 to toggle visibility
+ qx.log.appender.Console;
+ }
+
+ var root = this.getRoot();
+ var timeseries = this.timeseries;
+ var metric_routes = this.metric_routes;
+
+ /*
+ * ---------------------------------------------------------------------------
+ * CREATE UI
+ * ---------------------------------------------------------------------------
+ */
+
+ // set up background
+ // var decorator=new qx.ui.decoration.Single();
+ // decorator.setBackgroundImage("mtagui/bl1280.jpg");
+ // root.set({decorator:decorator});
+
+ var mainContainer = new qx.ui.container.Composite( new qx.ui.layout.VBox(10) );
+ var headerContainer = new qx.ui.container.Composite( new qx.ui.layout.VBox(10) );
+ var workContainer = new qx.ui.container.Composite( new qx.ui.layout.Canvas() );
+
+ root.add(mainContainer);
+ mainContainer.add(headerContainer);
+ mainContainer.add(workContainer);
+
+ var menubar = new qx.ui.toolbar.ToolBar();
+ menubar.setWidth(2500);
+ var toolsMenu = new qx.ui.toolbar.MenuButton("Tools");
+
+ menubar.add( new qx.ui.basic.Label("CADAM v0.1").set({padding: 7}) );
+ menubar.add( new qx.ui.toolbar.Separator() );
+ // menubar.add( toolsMenu );
+ headerContainer.add(menubar);
+
+ var usageLabel = new qx.ui.basic.Label( "Drag and Drop a metric from the list onto the gray area!" );
+ usageLabel.set(
+ {
+// font: new qx.bom.Font(18, ["Verdana", "Courier"]),
+ textColor: "white",
+ marginLeft: 50
+ });
+ menubar.add(usageLabel);
+
+ var metricsList = new qx.ui.form.List().set(
+ {
+ textColor: "black",
+ width: 300,
+ maxHeight: 400,
+ draggable: true
+ });
+ workContainer.add(metricsList, {left: 50, top: this.gridTop});
+
+ metricsList.addListener("dragstart", function(e)
+ {
+ e.addAction("move");
+ e.addType("metricid");
+ });
+
+ metricsList.addListener("droprequest", function(e)
+ {
+ var type = e.getCurrentType();
+ var action = e.getCurrentAction();
+ if ((type=="metricid") && (action=="move")) {
+ var metricid = this.getSelection()[0].getModel();
+ e.addData(type, metricid);
+ }
+ });
+
+ var desktop = this.desktop = new qx.ui.window.Desktop( new qx.ui.window.Manager() ).set(
+ {
+ width: (this.windowWidth + 20) * 2 + this.gridSpaceX,
+ height: this.windowHeight * 3 + this.gridSpaceY * 2,
+ backgroundColor: "#3a3a3a",
+ droppable: true
+ });
+
+ workContainer.add( desktop, {left: this.gridLeft, top: this.gridTop});
+ desktop.addListener("drop", this.showChartW, this);
+
+
+ /*
+ * ---------------------------------------------------------------------------
+ * COMMUNICATE WITH SERVER
+ * ---------------------------------------------------------------------------
+ */
+
+ // create a function to handle messages from server
+ var wsonmessage = function(m) {
+ if (m.data){
+ var message = qx.util.Json.parse(m.data);
+ console.log(message);
+ switch (message.response_for) {
+ case 'get_all_metrics' :
+ var metrics = message.response;
+ var listItem;
+ metricsList.removeAll();
+ for (var i=0; i<metrics.length; i++) {
+ listItem = new qx.ui.form.ListItem(metrics[i][0]+" - "+metrics[i][1], null, metrics[i]);
+ metricsList.add(listItem);
+ }
+ break;
+ case 'subscription' :
+ var metricid = message.response.metricid;
+ var value = message.response.value;
+ var ts = metric_routes[ metricid ];
+ // append new value to timeseries
+ ts.append(new Date().getTime(), value);
+ break;
+ default :
+ this.parent.debug("unexpected response:"+message.response_for);
+ }
+ }
+ };
+
+ // create websocket object
+ var ws = this.ws = new cadamgui.WebSocket2();
+ // overwrite onmessage call of the websocket
+ ws.onmessage = wsonmessage;
+ ws.connect("ws://localhost:9000/metrics_ws_server_connector.yaws");
+ ws.addListener("socket-ready", function()
+ {
+ ws.sendJSON({cmd: "get_all_metrics"});
+ });
+ },
+
+
+
+ // e is the drop event, 'this' is the qx.Application
+ showChartW : function(e)
+ {
+ var windowRealHeight = this.windowHeight + 20; // adjust for title bar
+
+ var ContainerWidget = e.getTarget();
+ var metricID = e.getData("metricid");
+
+ var cursorX = e.getDocumentLeft();
+ var cursorY = e.getDocumentTop();
+
+ Window = new qx.ui.window.Window( metricID[0]+' - '+metricID[1] ).set(
+ {
+ width: this.windowWidth,
+ height: this.windowHeight,
+ contentPadding: 2,
+
+ movable: true,
+ resizable: false,
+ showMaximize: false,
+ showMinimize: false
+ });
+ Window.setLayout(new qx.ui.layout.VBox());
+ var windowX = Math.floor((cursorX - this.gridLeft) / (this.windowWidth + this.gridSpaceX)) *
+ (this.windowWidth + this.gridSpaceX);// + this.gridLeft;
+
+ var windowY = Math.floor((cursorY - this.gridTop) / (windowRealHeight + this.gridSpaceY)) *
+ (windowRealHeight + this.gridSpaceY);// + this.gridTop;
+
+ this.desktop.add(Window, {left: windowX, top: windowY});
+
+ // create/add smoothie chart
+ var chartOptions = {grid: {verticalSections : 2, millisPerLine : 10000}, maxValueScale: 1.1,
+ fps : 20, millisPerPixel : 100};
+ var chart = new cadamgui.QxSmoothie(chartOptions, 4000).set(
+ {
+ canvasWidth: 400,
+ maxWidth: 400,
+ canvasHeight: 150,
+ height: 150
+ });
+ Window.add(chart);
+ Window.open();
+ var ts = chart.getTimeseries();
+ this.metric_routes[metricID] = ts;
+ this.ws.sendJSON({cmd: "subscribe_to_queue", metricid: metricID, queuenum: 1});
+ console.log('drop listener - metric_routes='+qx.util.Json.stringify(this.metric_routes));
+ }
+
+
+
+ }
+});
View
309 priv/www/cadamgui/source/class/cadamgui/QxSmoothie.js
@@ -0,0 +1,309 @@
+qx.Class.define("cadamgui.QxSmoothie",{
+ extend : qx.ui.embed.Canvas,
+ construct : function(chartOptions, delay) {
+ this.base(arguments);
+
+ if (document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1")){
+ this.setTimeseries(new TimeSeries());
+ this.setSmoothiechart(new SmoothieChart(chartOptions));
+ if (delay) {
+ this.setDelay(delay);
+ } else {
+ this.setDelay(1000);
+ };
+ this.addListenerOnce('appear',this._setCanvas,this);
+ this.addListener('resize',this._setSize,this);
+ this.getSmoothiechart().addTimeSeries( this.getTimeseries(),
+ {strokeStyle: 'rgba(0, 255, 0, 1)', fillStyle: 'rgba(0, 255, 0, 0.2)', lineWidth: 2});
+ }
+ else {
+ this._setLayout(new qx.ui.layout.Grow());
+ this._add(new qx.ui.basic.Atom('Your browser does not seem to have support for SVG.').set({
+ rich: true,
+ alignX: 'center',
+ alignY: 'middle',
+ padding: 20
+ }));
+ this.setTimeseries(null);
+ this.setSmoothiechart(null);
+ }
+ },
+ properties: {
+ timeseries: {},
+ smoothiechart: {},
+ delay: 0
+ },
+ members: {
+ _setCanvas: function(e){
+ var el = this.getContentElement().getDomElement();
+ this.getSmoothiechart().streamTo(el, 1000);
+ },
+ _setSize: function(e){
+ qx.html.Element.flush();
+ }
+ }
+});
+
+
+
+
+// MIT License:
+//
+// Copyright (c) 2010, Joe Walnes
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * Smoothie Charts - http://smoothiecharts.org/
+ * (c) 2010, Joe Walnes
+ *
+ * v1.0: Main charting library, by Joe Walnes
+ * v1.1: Auto scaling of axis, by Neil Dunn
+ * v1.2: fps (frames per second) option, by Mathias Petterson
+ * v1.3: Fix for divide by zero, by Paul Nikitochkin
+ * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
+ */
+
+function TimeSeries(options) {
+ options = options || {};
+ options.resetBoundsInterval = options.resetBoundsInterval || 3000; // Reset the max/min bounds after this many milliseconds
+ options.resetBounds = options.resetBounds || true; // Enable or disable the resetBounds timer
+ this.options = options;
+ this.data = [];
+
+ this.maxValue = Number.NaN; // The maximum value ever seen in this time series.
+ this.minValue = Number.NaN; // The minimum value ever seen in this time series.
+
+ // Start a resetBounds Interval timer desired
+ if (options.resetBounds) {
+ this.boundsTimer = setInterval(function(thisObj) { thisObj.resetBounds(); }, options.resetBoundsInterval, this);
+ }
+}
+
+// Reset the min and max for this timeseries so the graph rescales itself
+TimeSeries.prototype.resetBounds = function() {
+ this.maxValue = Number.NaN;
+ this.minValue = Number.NaN;
+ for (var i = 0; i < this.data.length; i++) {
+ this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, this.data[i][1]) : this.data[i][1];
+ this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, this.data[i][1]) : this.data[i][1];
+ }
+};
+
+TimeSeries.prototype.append = function(timestamp, value) {
+ this.data.push([timestamp, value]);
+ this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, value) : value;
+ this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, value) : value;
+};
+
+function SmoothieChart(options) {
+ // Defaults
+ options = options || {};
+ options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 };
+ options.millisPerPixel = options.millisPerPixel || 20;
+ options.fps = options.fps || 20;
+ options.maxValueScale = options.maxValueScale || 1;
+ options.minValue = options.minValue;
+ options.labels = options.labels || { fillStyle:'#ffffff' }
+ this.options = options;
+ this.seriesSet = [];
+}
+
+SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
+ this.seriesSet.push({timeSeries: timeSeries, options: options || {}});
+};
+
+SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
+ this.seriesSet.splice(this.seriesSet.indexOf(timeSeries), 1);
+};
+
+SmoothieChart.prototype.streamTo = function(canvas, delay) {
+ var self = this;
+ (function render() {
+ self.render(canvas, new Date().getTime() - (delay || 0));
+ setTimeout(render, 1000/self.options.fps);
+ })()
+};
+
+SmoothieChart.prototype.render = function(canvas, time) {
+ var canvasContext = canvas.getContext("2d");
+ var options = this.options;
+ var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight};
+
+ // Save the state of the canvas context, any transformations applied in this method
+ // will get removed from the stack at the end of this method when .restore() is called.
+ canvasContext.save();
+
+ // Round time down to pixel granularity, so motion appears smoother.
+ time = time - time % options.millisPerPixel;
+
+ // Move the origin.
+ canvasContext.translate(dimensions.left, dimensions.top);
+
+ // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
+ // This prevents the occasional pixels from curves near the edges overrunning and creating
+ // screen cheese (that phrase should neeed no explanation).
+ canvasContext.beginPath();
+ canvasContext.rect(0, 0, dimensions.width, dimensions.height);
+ canvasContext.clip();
+
+ // Clear the working area.
+ canvasContext.save();
+ canvasContext.fillStyle = options.grid.fillStyle;
+ canvasContext.fillRect(0, 0, dimensions.width, dimensions.height);
+ canvasContext.restore();
+
+ // Grid lines....
+ canvasContext.save();
+ canvasContext.lineWidth = options.grid.lineWidth || 1;
+ canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff';
+ // Vertical (time) dividers.
+ if (options.grid.millisPerLine > 0) {
+ for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) {
+ canvasContext.beginPath();
+ var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel));
+ canvasContext.moveTo(gx, 0);
+ canvasContext.lineTo(gx, dimensions.height);
+ canvasContext.stroke();
+ canvasContext.closePath();
+ }
+ }
+
+ // Horizontal (value) dividers.
+ for (var v = 1; v < options.grid.verticalSections; v++) {
+ var gy = Math.round(v * dimensions.height / options.grid.verticalSections);
+ canvasContext.beginPath();
+ canvasContext.moveTo(0, gy);
+ canvasContext.lineTo(dimensions.width, gy);
+ canvasContext.stroke();
+ canvasContext.closePath();
+ }
+ // Bounding rectangle.
+ canvasContext.beginPath();
+ canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height);
+ canvasContext.closePath();
+ canvasContext.restore();
+
+ // Calculate the current scale of the chart, from all time series.
+ var maxValue = Number.NaN;
+ var minValue = Number.NaN;
+
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ // TODO(ndunn): We could calculate / track these values as they stream in.
+ var timeSeries = this.seriesSet[d].timeSeries;
+ if (!isNaN(timeSeries.maxValue)) {
+ maxValue = !isNaN(maxValue) ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue;
+ }
+
+ if (!isNaN(timeSeries.minValue)) {
+ minValue = !isNaN(minValue) ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue;
+ }
+ }
+
+ if (isNaN(maxValue) && isNaN(minValue)) {
+ return;
+ }
+
+ // Scale the maxValue to add padding at the top if required
+ maxValue = maxValue * options.maxValueScale;
+ // Set the minimum if we've specified one
+ if (options.minValue != null)
+ minValue = options.minValue;
+ var valueRange = maxValue - minValue;
+
+ // For each data set...
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ canvasContext.save();
+ var timeSeries = this.seriesSet[d].timeSeries;
+ var dataSet = timeSeries.data;
+ var seriesOptions = this.seriesSet[d].options;
+
+ // Delete old data that's moved off the left of the chart.
+ // We must always keep the last expired data point as we need this to draw the
+ // line that comes into the chart, but any points prior to that can be removed.
+ while (dataSet.length >= 2 && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) {
+ dataSet.splice(0, 1);
+ }
+
+ // Set style for this dataSet.
+ canvasContext.lineWidth = seriesOptions.lineWidth || 1;
+ canvasContext.fillStyle = seriesOptions.fillStyle;
+ canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff';
+ // Draw the line...
+ canvasContext.beginPath();
+ // Retain lastX, lastY for calculating the control points of bezier curves.
+ var firstX = 0, lastX = 0, lastY = 0;
+ for (var i = 0; i < dataSet.length; i++) {
+ // TODO: Deal with dataSet.length < 2.
+ var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel));
+ var value = dataSet[i][1];
+ var offset = maxValue - value;
+ var scaledValue = valueRange ? Math.round((offset / valueRange) * dimensions.height) : 0;
+ var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart.
+
+ if (i == 0) {
+ firstX = x;
+ canvasContext.moveTo(x, y);
+ }
+ // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/B�zier_curve#Quadratic_curves
+ //
+ // Assuming A was the last point in the line plotted and B is the new point,
+ // we draw a curve with control points P and Q as below.
+ //
+ // A---P
+ // |
+ // |
+ // |
+ // Q---B
+ //
+ // Importantly, A and P are at the same y coordinate, as are B and Q. This is
+ // so adjacent curves appear to flow as one.
+ //
+ else {
+ canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
+ Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
+ Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
+ x, y); // endPoint (B)
+ }
+
+ lastX = x, lastY = y;
+ }
+ if (dataSet.length > 0 && seriesOptions.fillStyle) {
+ // Close up the fill region.
+ canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
+ canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
+ canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
+ canvasContext.fill();
+ }
+ canvasContext.stroke();
+ canvasContext.closePath();
+ canvasContext.restore();
+ }
+
+ // Draw the axis values on the chart.
+ if (!options.labels.disabled) {
+ canvasContext.fillStyle = options.labels.fillStyle;
+ var maxValueString = maxValue.toFixed(2);
+ var minValueString = minValue.toFixed(2);
+ canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10);
+ canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2);
+ }
+
+ canvasContext.restore(); // See .save() above.
+}
View
48 priv/www/cadamgui/source/class/cadamgui/WebSocket.js
@@ -0,0 +1,48 @@
+qx.Class.define("cadamgui.WebSocket",
+{
+ extend : qx.core.Object,
+
+ properties:
+ {
+ ws : {nullable : true}
+ },
+
+ members :
+ {
+// ws : {},
+
+ _onopen: function(){
+ this.parent.debug('websocket connecting');
+ this.send('client-connected');
+ },
+ _onclose: function(m) {
+ this.parent.debug('websocket closing');
+ //this._ws=null;
+ },
+ _send: function(msg) {
+ this.send(msg);
+ },
+ send: function(msg){
+ if (this.getWs()) this.getWs().send(msg);
+ },
+
+ // example endpoint: "ws://localhost:9000/metrics_ws_server_connector.yaws"
+ connect: function(endpoint){
+ this.ws = new WebSocket(endpoint);
+ this.ws.onopen = this._onopen;
+ this.ws.onmessage = this.onmessage;
+ this.ws.onclose = this._onclose;
+ this.ws.parent = this;
+ },
+// sendMsg: function(message){
+// this._ws.send(message);
+// if (this._ws) this._ws.send(message);
+// },
+ onmessage: function(m) {
+ if (m.data){
+ var text = m.data;
+ this.parent.debug('got data:'+text);
+ }
+ }
+ }
+});
View
59 priv/www/cadamgui/source/class/cadamgui/WebSocket2.js
@@ -0,0 +1,59 @@
+qx.Class.define("cadamgui.WebSocket2",
+{
+ extend : qx.core.Object,
+
+ events :
+ {
+ "socket-ready" : "qx.event.type.Data"
+ },
+
+ members :
+ {
+ _wsock : {},
+ _connected : false,
+
+ _onopen: function(){
+ console.log('websocket connecting');
+ this.send('client-connected');
+ this.parent._connected = true;
+ this.parent.fireDataEvent("socket-ready", true);
+ },
+
+ _onclose: function(m) {
+ console.log('websocket closing');
+ this._wsock = null;
+ this._connected = false;
+ },
+
+ sendTxt: function(msg){
+ if (this._connected) {
+ console.log('sending:'+msg);
+ this._wsock.send(msg);
+ }
+ },
+
+ sendJSON: function(msg){
+ if (this._connected) {
+ var msgString = qx.util.Json.stringify(msg);
+ console.log('sending:'+msgString);
+ this._wsock.send(msgString);
+ }
+ },
+
+ // example endpoint: "ws://localhost:9000/metrics_ws_server_connector.yaws"
+ connect: function(endpoint){
+ var ws = this._wsock = new WebSocket(endpoint);
+ ws.onopen = this._onopen;
+ ws.onmessage = this.onmessage;
+ ws.onclose = this._onclose;
+ ws.parent = this;
+ },
+
+ onmessage: function(m) {
+ if (m.data){
+ var text = m.data;
+ console.log('got data:'+text);
+ }
+ }
+ }
+});
View
55 priv/www/cadamgui/source/class/cadamgui/test/DemoTest.js
@@ -0,0 +1,55 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors:
+
+************************************************************************ */
+
+/**
+ * This class demonstrates how to define unit tests for your application.
+ *
+ * Execute <code>generate.py test</code> to generate a testrunner application
+ * and open it from <tt>test/index.html</tt>
+ *
+ * The methods that contain the tests are instance methods with a
+ * <code>test</code> prefix. You can create an arbitrary number of test
+ * classes like this one. They can be organized in a regular class hierarchy,
+ * i.e. using deeper namespaces and a corresponding file structure within the
+ * <tt>test</tt> folder.
+ */
+qx.Class.define("cadamgui.test.DemoTest",
+{
+ extend : qx.dev.unit.TestCase,
+
+ members :
+ {
+ /*
+ ---------------------------------------------------------------------------
+ TESTS
+ ---------------------------------------------------------------------------
+ */
+
+ /**
+ * Here are some simple tests
+ */
+ testSimple : function()
+ {
+ this.assertEquals(4, 3+1, "This should never fail!");
+ this.assertFalse(false, "Can false be true?!");
+ },
+
+ /**
+ * Here are some more advanced tests
+ */
+ testAdvanced: function ()
+ {
+ var a = 3;
+ var b = a;
+ this.assertIdentical(a, b, "A rose by any other name is still a rose");
+ this.assertInRange(3, 1, 10, "You must be kidding, 3 can never be outside [1,10]!");
+ }
+ }
+});
View
18 priv/www/cadamgui/source/class/cadamgui/theme/Appearance.js
@@ -0,0 +1,18 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors:
+
+************************************************************************ */
+
+qx.Theme.define("cadamgui.theme.Appearance",
+{
+ extend : qx.theme.modern.Appearance,
+
+ appearances :
+ {
+ }
+});
View
18 priv/www/cadamgui/source/class/cadamgui/theme/Color.js
@@ -0,0 +1,18 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors:
+
+************************************************************************ */
+
+qx.Theme.define("cadamgui.theme.Color",
+{
+ extend : qx.theme.modern.Color,
+
+ colors :
+ {
+ }
+});
View
18 priv/www/cadamgui/source/class/cadamgui/theme/Decoration.js
@@ -0,0 +1,18 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors:
+
+************************************************************************ */
+
+qx.Theme.define("cadamgui.theme.Decoration",
+{
+ extend : qx.theme.modern.Decoration,
+
+ decorations :
+ {
+ }
+});
View
18 priv/www/cadamgui/source/class/cadamgui/theme/Font.js
@@ -0,0 +1,18 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors:
+
+************************************************************************ */
+
+qx.Theme.define("cadamgui.theme.Font",
+{
+ extend : qx.theme.modern.Font,
+
+ fonts :
+ {
+ }
+});
View
21 priv/www/cadamgui/source/class/cadamgui/theme/Theme.js
@@ -0,0 +1,21 @@
+/* ************************************************************************
+
+ Copyright:
+
+ License:
+
+ Authors:
+
+************************************************************************ */
+
+qx.Theme.define("cadamgui.theme.Theme",
+{
+ meta :
+ {
+ color : cadamgui.theme.Color,
+ decoration : cadamgui.theme.Decoration,
+ font : cadamgui.theme.Font,
+ icon : qx.theme.icon.Tango,
+ appearance : cadamgui.theme.Appearance
+ }
+});
View
9 priv/www/cadamgui/source/index.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>cadamgui</title>
+ <script type="text/javascript" src="script/cadamgui.js"></script>
+</head>
+<body></body>
+</html>
View
BIN priv/www/cadamgui/source/resource/cadamgui/close-button-hovered.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN priv/www/cadamgui/source/resource/cadamgui/close-button-pressed.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN priv/www/cadamgui/source/resource/cadamgui/close-button.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN priv/www/cadamgui/source/resource/cadamgui/close-button1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN priv/www/cadamgui/source/resource/cadamgui/test.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
164 priv/www/cadamgui/source/script/cadamgui.js
164 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
3 priv/www/cadamgui/source/translation/readme.txt
@@ -0,0 +1,3 @@
+This directory will contain translation (.po) files once you run the
+'translation' job in your project.
+
View
29 priv/www/metrics_ws_server_connector.yaws
@@ -0,0 +1,29 @@
+<erl>
+out(A) ->
+ case get_upgrade_header(A#arg.headers) of
+ undefined ->
+ {content, "text/plain", "You're not a web sockets client! Go away!"};
+ "WebSocket" ->
+ WebSocketOwner = whereis(websocket_server),
+ {websocket, WebSocketOwner, passive}
+ end.
+
+get_upgrade_header(#headers{other=L}) ->
+ lists:foldl(fun({http_header,_,K0,_,V}, undefined) ->
+ K = case is_atom(K0) of
+ true ->
+ atom_to_list(K0);
+ false ->
+ K0
+ end,
+ case string:to_lower(K) of
+ "upgrade" ->
+ V;
+ _ ->
+ undefined
+ end;
+ (_, Acc) ->
+ Acc
+ end, undefined, L).
+
+</erl>
View
260 priv/www/smoothie.js
@@ -0,0 +1,260 @@
+// MIT License:
+//
+// Copyright (c) 2010, Joe Walnes
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * Smoothie Charts - http://smoothiecharts.org/
+ * (c) 2010, Joe Walnes
+ *
+ * v1.0: Main charting library, by Joe Walnes
+ * v1.1: Auto scaling of axis, by Neil Dunn
+ * v1.2: fps (frames per second) option, by Mathias Petterson
+ * v1.3: Fix for divide by zero, by Paul Nikitochkin
+ * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
+ */
+
+function TimeSeries(options) {
+ options = options || {};
+ options.resetBoundsInterval = options.resetBoundsInterval || 3000; // Reset the max/min bounds after this many milliseconds
+ options.resetBounds = options.resetBounds || true; // Enable or disable the resetBounds timer
+ this.options = options;
+ this.data = [];
+
+ this.maxValue = Number.NaN; // The maximum value ever seen in this time series.
+ this.minValue = Number.NaN; // The minimum value ever seen in this time series.
+
+ // Start a resetBounds Interval timer desired
+ if (options.resetBounds) {
+ this.boundsTimer = setInterval(function(thisObj) { thisObj.resetBounds(); }, options.resetBoundsInterval, this);
+ }
+}
+
+// Reset the min and max for this timeseries so the graph rescales itself
+TimeSeries.prototype.resetBounds = function() {
+ this.maxValue = Number.NaN;
+ this.minValue = Number.NaN;
+ for (var i = 0; i < this.data.length; i++) {
+ this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, this.data[i][1]) : this.data[i][1];
+ this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, this.data[i][1]) : this.data[i][1];
+ }
+};
+
+TimeSeries.prototype.append = function(timestamp, value) {
+ this.data.push([timestamp, value]);
+ this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, value) : value;
+ this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, value) : value;
+};
+
+function SmoothieChart(options) {
+ // Defaults
+ options = options || {};
+ options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 };
+ options.millisPerPixel = options.millisPerPixel || 20;
+ options.fps = options.fps || 20;
+ options.maxValueScale = options.maxValueScale || 1;
+ options.minValue = options.minValue;
+ options.labels = options.labels || { fillStyle:'#ffffff' }
+ this.options = options;
+ this.seriesSet = [];
+}
+
+SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
+ this.seriesSet.push({timeSeries: timeSeries, options: options || {}});
+};
+
+SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
+ this.seriesSet.splice(this.seriesSet.indexOf(timeSeries), 1);
+};
+
+SmoothieChart.prototype.streamTo = function(canvas, delay) {
+ var self = this;
+ (function render() {
+ self.render(canvas, new Date().getTime() - (delay || 0));
+ setTimeout(render, 1000/self.options.fps);
+ })()
+};
+
+SmoothieChart.prototype.render = function(canvas, time) {
+ var canvasContext = canvas.getContext("2d");
+ var options = this.options;
+ var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight};
+
+ // Save the state of the canvas context, any transformations applied in this method
+ // will get removed from the stack at the end of this method when .restore() is called.
+ canvasContext.save();
+
+ // Round time down to pixel granularity, so motion appears smoother.
+ time = time - time % options.millisPerPixel;
+
+ // Move the origin.
+ canvasContext.translate(dimensions.left, dimensions.top);
+
+ // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
+ // This prevents the occasional pixels from curves near the edges overrunning and creating
+ // screen cheese (that phrase should neeed no explanation).
+ canvasContext.beginPath();
+ canvasContext.rect(0, 0, dimensions.width, dimensions.height);
+ canvasContext.clip();
+
+ // Clear the working area.
+ canvasContext.save();
+ canvasContext.fillStyle = options.grid.fillStyle;
+ canvasContext.fillRect(0, 0, dimensions.width, dimensions.height);
+ canvasContext.restore();
+
+ // Grid lines....
+ canvasContext.save();
+ canvasContext.lineWidth = options.grid.lineWidth || 1;
+ canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff';
+ // Vertical (time) dividers.
+ if (options.grid.millisPerLine > 0) {
+ for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) {
+ canvasContext.beginPath();
+ var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel));
+ canvasContext.moveTo(gx, 0);
+ canvasContext.lineTo(gx, dimensions.height);
+ canvasContext.stroke();
+ canvasContext.closePath();
+ }
+ }
+
+ // Horizontal (value) dividers.
+ for (var v = 1; v < options.grid.verticalSections; v++) {
+ var gy = Math.round(v * dimensions.height / options.grid.verticalSections);
+ canvasContext.beginPath();
+ canvasContext.moveTo(0, gy);
+ canvasContext.lineTo(dimensions.width, gy);
+ canvasContext.stroke();
+ canvasContext.closePath();
+ }
+ // Bounding rectangle.
+ canvasContext.beginPath();
+ canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height);
+ canvasContext.closePath();
+ canvasContext.restore();
+
+ // Calculate the current scale of the chart, from all time series.
+ var maxValue = Number.NaN;
+ var minValue = Number.NaN;
+
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ // TODO(ndunn): We could calculate / track these values as they stream in.
+ var timeSeries = this.seriesSet[d].timeSeries;
+ if (!isNaN(timeSeries.maxValue)) {
+ maxValue = !isNaN(maxValue) ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue;
+ }
+
+ if (!isNaN(timeSeries.minValue)) {
+ minValue = !isNaN(minValue) ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue;
+ }
+ }
+
+ if (isNaN(maxValue) && isNaN(minValue)) {
+ return;
+ }
+
+ // Scale the maxValue to add padding at the top if required
+ maxValue = maxValue * options.maxValueScale;
+ // Set the minimum if we've specified one
+ if (options.minValue != null)
+ minValue = options.minValue;
+ var valueRange = maxValue - minValue;
+
+ // For each data set...
+ for (var d = 0; d < this.seriesSet.length; d++) {
+ canvasContext.save();
+ var timeSeries = this.seriesSet[d].timeSeries;
+ var dataSet = timeSeries.data;
+ var seriesOptions = this.seriesSet[d].options;
+
+ // Delete old data that's moved off the left of the chart.
+ // We must always keep the last expired data point as we need this to draw the
+ // line that comes into the chart, but any points prior to that can be removed.
+ while (dataSet.length >= 2 && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) {
+ dataSet.splice(0, 1);
+ }
+
+ // Set style for this dataSet.
+ canvasContext.lineWidth = seriesOptions.lineWidth || 1;
+ canvasContext.fillStyle = seriesOptions.fillStyle;
+ canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff';
+ // Draw the line...
+ canvasContext.beginPath();
+ // Retain lastX, lastY for calculating the control points of bezier curves.
+ var firstX = 0, lastX = 0, lastY = 0;
+ for (var i = 0; i < dataSet.length; i++) {
+ // TODO: Deal with dataSet.length < 2.
+ var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel));
+ var value = dataSet[i][1];
+ var offset = maxValue - value;
+ var scaledValue = valueRange ? Math.round((offset / valueRange) * dimensions.height) : 0;
+ var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart.
+
+ if (i == 0) {
+ firstX = x;
+ canvasContext.moveTo(x, y);
+ }
+ // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/B�zier_curve#Quadratic_curves
+ //
+ // Assuming A was the last point in the line plotted and B is the new point,
+ // we draw a curve with control points P and Q as below.
+ //
+ // A---P
+ // |
+ // |
+ // |
+ // Q---B
+ //
+ // Importantly, A and P are at the same y coordinate, as are B and Q. This is
+ // so adjacent curves appear to flow as one.
+ //
+ else {
+ canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
+ Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
+ Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
+ x, y); // endPoint (B)
+ }
+
+ lastX = x, lastY = y;
+ }
+ if (dataSet.length > 0 && seriesOptions.fillStyle) {
+ // Close up the fill region.
+ canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
+ canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
+ canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
+ canvasContext.fill();
+ }
+ canvasContext.stroke();
+ canvasContext.closePath();
+ canvasContext.restore();
+ }
+
+ // Draw the axis values on the chart.
+ if (!options.labels.disabled) {
+ canvasContext.fillStyle = options.labels.fillStyle;
+ var maxValueString = maxValue.toFixed(2);
+ var minValueString = minValue.toFixed(2);
+ canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10);
+ canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2);
+ }
+
+ canvasContext.restore(); // See .save() above.
+}

0 comments on commit f116b2d

Please sign in to comment.
Something went wrong with that request. Please try again.