From 2df2298c9b91145d5797ce8b0ae847327e4b4ded Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Mon, 2 Jun 2014 14:30:54 +0200 Subject: [PATCH] added graph3d --- dist/vis.js | 8 +- src/3dgraph/doc/.gitignore | 1 + src/3dgraph/doc/default.css | 87 + src/3dgraph/doc/example.html | 68 + src/3dgraph/doc/graph3d.png | Bin 0 -> 101584 bytes src/3dgraph/doc/graph3d120x60.png | Bin 0 -> 9904 bytes src/3dgraph/doc/index.html | 770 ++++ src/3dgraph/doc/prettify/lang-apollo.js | 2 + src/3dgraph/doc/prettify/lang-css.js | 2 + src/3dgraph/doc/prettify/lang-hs.js | 2 + src/3dgraph/doc/prettify/lang-lisp.js | 2 + src/3dgraph/doc/prettify/lang-lua.js | 2 + src/3dgraph/doc/prettify/lang-ml.js | 2 + src/3dgraph/doc/prettify/lang-proto.js | 1 + src/3dgraph/doc/prettify/lang-scala.js | 2 + src/3dgraph/doc/prettify/lang-sql.js | 2 + src/3dgraph/doc/prettify/lang-vb.js | 2 + src/3dgraph/doc/prettify/lang-vhdl.js | 3 + src/3dgraph/doc/prettify/lang-wiki.js | 2 + src/3dgraph/doc/prettify/lang-yaml.js | 2 + src/3dgraph/doc/prettify/prettify.css | 1 + src/3dgraph/doc/prettify/prettify.js | 33 + src/3dgraph/examples/ajax.js | 141 + src/3dgraph/examples/datasource.php | 156 + src/3dgraph/examples/datasource_csv.php | 54 + .../examples/datasource_csv_to_json.php | 113 + src/3dgraph/examples/datastream_csv.php | 79 + src/3dgraph/examples/default.css | 87 + src/3dgraph/examples/example01_basis.html | 71 + src/3dgraph/examples/example02_camera.html | 131 + src/3dgraph/examples/example03_filter.html | 75 + src/3dgraph/examples/example04_animate.html | 81 + .../examples/example05_datasource.html | 101 + src/3dgraph/examples/example06_line.html | 70 + .../example07_internet_explorer_9.html | 90 + .../examples/example08_moving_dots.html | 90 + .../examples/example09_dot_cloud_colors.html | 77 + .../examples/example10_dot_cloud_size.html | 79 + .../example11_datasource_refresh.html | 92 + .../examples/example12_datastream.html | 185 + src/3dgraph/examples/example13_mobile.html | 87 + src/3dgraph/examples/example14_styles.html | 131 + src/3dgraph/examples/example15_tooltips.html | 114 + src/3dgraph/examples/index.html | 28 + src/3dgraph/graph3d.js | 3270 +++++++++++++++++ src/3dgraph/playground/csv2array.js | 120 + src/3dgraph/playground/csv2datatable.html | 80 + src/3dgraph/playground/datasource.html | 173 + src/3dgraph/playground/datasource.php | 155 + src/3dgraph/playground/index.html | 247 ++ src/3dgraph/playground/playground.css | 91 + src/3dgraph/playground/playground.js | 657 ++++ .../playground/prettify/lang-apollo.js | 2 + src/3dgraph/playground/prettify/lang-css.js | 2 + src/3dgraph/playground/prettify/lang-hs.js | 2 + src/3dgraph/playground/prettify/lang-lisp.js | 2 + src/3dgraph/playground/prettify/lang-lua.js | 2 + src/3dgraph/playground/prettify/lang-ml.js | 2 + src/3dgraph/playground/prettify/lang-proto.js | 1 + src/3dgraph/playground/prettify/lang-scala.js | 2 + src/3dgraph/playground/prettify/lang-sql.js | 2 + src/3dgraph/playground/prettify/lang-vb.js | 2 + src/3dgraph/playground/prettify/lang-vhdl.js | 3 + src/3dgraph/playground/prettify/lang-wiki.js | 2 + src/3dgraph/playground/prettify/lang-yaml.js | 2 + src/3dgraph/playground/prettify/prettify.css | 1 + src/3dgraph/playground/prettify/prettify.js | 33 + src/3dgraph/tests/example01_basis.html | 75 + src/3dgraph/tests/example04_animate.html | 77 + .../tests/sebleedelisle/canvas3d2.html | 99 + .../tests/sebleedelisle/canvas3d3.html | 111 + .../tests/sebleedelisle/canvas3d4.html | 130 + src/3dgraph/tests/sebleedelisle/url.txt | 1 + src/3dgraph/tests/test.html | 83 + src/3dgraph/tests/test_extreme_data.html | 78 + src/3dgraph/tests/test_slider.html | 43 + 76 files changed, 8671 insertions(+), 5 deletions(-) create mode 100644 src/3dgraph/doc/.gitignore create mode 100644 src/3dgraph/doc/default.css create mode 100644 src/3dgraph/doc/example.html create mode 100644 src/3dgraph/doc/graph3d.png create mode 100644 src/3dgraph/doc/graph3d120x60.png create mode 100644 src/3dgraph/doc/index.html create mode 100644 src/3dgraph/doc/prettify/lang-apollo.js create mode 100644 src/3dgraph/doc/prettify/lang-css.js create mode 100644 src/3dgraph/doc/prettify/lang-hs.js create mode 100644 src/3dgraph/doc/prettify/lang-lisp.js create mode 100644 src/3dgraph/doc/prettify/lang-lua.js create mode 100644 src/3dgraph/doc/prettify/lang-ml.js create mode 100644 src/3dgraph/doc/prettify/lang-proto.js create mode 100644 src/3dgraph/doc/prettify/lang-scala.js create mode 100644 src/3dgraph/doc/prettify/lang-sql.js create mode 100644 src/3dgraph/doc/prettify/lang-vb.js create mode 100644 src/3dgraph/doc/prettify/lang-vhdl.js create mode 100644 src/3dgraph/doc/prettify/lang-wiki.js create mode 100644 src/3dgraph/doc/prettify/lang-yaml.js create mode 100644 src/3dgraph/doc/prettify/prettify.css create mode 100644 src/3dgraph/doc/prettify/prettify.js create mode 100644 src/3dgraph/examples/ajax.js create mode 100644 src/3dgraph/examples/datasource.php create mode 100644 src/3dgraph/examples/datasource_csv.php create mode 100644 src/3dgraph/examples/datasource_csv_to_json.php create mode 100644 src/3dgraph/examples/datastream_csv.php create mode 100644 src/3dgraph/examples/default.css create mode 100644 src/3dgraph/examples/example01_basis.html create mode 100644 src/3dgraph/examples/example02_camera.html create mode 100644 src/3dgraph/examples/example03_filter.html create mode 100644 src/3dgraph/examples/example04_animate.html create mode 100644 src/3dgraph/examples/example05_datasource.html create mode 100644 src/3dgraph/examples/example06_line.html create mode 100644 src/3dgraph/examples/example07_internet_explorer_9.html create mode 100644 src/3dgraph/examples/example08_moving_dots.html create mode 100644 src/3dgraph/examples/example09_dot_cloud_colors.html create mode 100644 src/3dgraph/examples/example10_dot_cloud_size.html create mode 100644 src/3dgraph/examples/example11_datasource_refresh.html create mode 100644 src/3dgraph/examples/example12_datastream.html create mode 100644 src/3dgraph/examples/example13_mobile.html create mode 100644 src/3dgraph/examples/example14_styles.html create mode 100644 src/3dgraph/examples/example15_tooltips.html create mode 100644 src/3dgraph/examples/index.html create mode 100644 src/3dgraph/graph3d.js create mode 100644 src/3dgraph/playground/csv2array.js create mode 100644 src/3dgraph/playground/csv2datatable.html create mode 100644 src/3dgraph/playground/datasource.html create mode 100644 src/3dgraph/playground/datasource.php create mode 100644 src/3dgraph/playground/index.html create mode 100644 src/3dgraph/playground/playground.css create mode 100644 src/3dgraph/playground/playground.js create mode 100644 src/3dgraph/playground/prettify/lang-apollo.js create mode 100644 src/3dgraph/playground/prettify/lang-css.js create mode 100644 src/3dgraph/playground/prettify/lang-hs.js create mode 100644 src/3dgraph/playground/prettify/lang-lisp.js create mode 100644 src/3dgraph/playground/prettify/lang-lua.js create mode 100644 src/3dgraph/playground/prettify/lang-ml.js create mode 100644 src/3dgraph/playground/prettify/lang-proto.js create mode 100644 src/3dgraph/playground/prettify/lang-scala.js create mode 100644 src/3dgraph/playground/prettify/lang-sql.js create mode 100644 src/3dgraph/playground/prettify/lang-vb.js create mode 100644 src/3dgraph/playground/prettify/lang-vhdl.js create mode 100644 src/3dgraph/playground/prettify/lang-wiki.js create mode 100644 src/3dgraph/playground/prettify/lang-yaml.js create mode 100644 src/3dgraph/playground/prettify/prettify.css create mode 100644 src/3dgraph/playground/prettify/prettify.js create mode 100644 src/3dgraph/tests/example01_basis.html create mode 100644 src/3dgraph/tests/example04_animate.html create mode 100644 src/3dgraph/tests/sebleedelisle/canvas3d2.html create mode 100644 src/3dgraph/tests/sebleedelisle/canvas3d3.html create mode 100644 src/3dgraph/tests/sebleedelisle/canvas3d4.html create mode 100644 src/3dgraph/tests/sebleedelisle/url.txt create mode 100644 src/3dgraph/tests/test.html create mode 100644 src/3dgraph/tests/test_extreme_data.html create mode 100644 src/3dgraph/tests/test_slider.html diff --git a/dist/vis.js b/dist/vis.js index 439353b1..8704181c 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5530,16 +5530,14 @@ Linegraph.prototype.setData = function() { var dataset = this._extractData(datapoints); var data = dataset.data; - console.log("height",data,datapoints, dataset); - this.yAxis.setRange({start:dataset.range.low,end:dataset.range.high}); this.yAxis.repaint(); data = this.yAxis.convertValues(data); var d, d2, d3; - d = this._catmullRom(data,0.5); - d3 = this._catmullRom(data,0); - d2 = this._catmullRom(data,1); + d = this._catmullRom(data,0.5); // centripetal + d3 = this._catmullRom(data,0); // uniform + d2 = this._catmullRom(data,1); // chordial // var data2 = []; diff --git a/src/3dgraph/doc/.gitignore b/src/3dgraph/doc/.gitignore new file mode 100644 index 00000000..30d1e49a --- /dev/null +++ b/src/3dgraph/doc/.gitignore @@ -0,0 +1 @@ +jsdoc diff --git a/src/3dgraph/doc/default.css b/src/3dgraph/doc/default.css new file mode 100644 index 00000000..f0c251df --- /dev/null +++ b/src/3dgraph/doc/default.css @@ -0,0 +1,87 @@ +html, body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; +} + +body, td, th { + font-family: arial, sans-serif; + font-size: 11pt; + color: #4D4D4D; + line-height: 1.7em; +} + +#container { + margin: 0 auto; + padding-bottom: 50px; + width: 900px; +} + +h1 { + font-size: 180%; + font-weight: bold; + padding: 0; + margin: 1em 0 1em 0; +} + +h2 { + padding-top: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #a0c0f0; + color: #2B7CE9; +} + +h3 { + font-size: 140%; +} + + +a { + color: #2B7CE9; + text-decoration: none; +} +a:visited { + color: #2E60A4; +} +a:hover { + color: red; + text-decoration: underline; +} + +hr { + border: none 0; + border-top: 1px solid #abc; + height: 1px; +} + +pre { + display: block; + font-size: 10pt; + line-height: 1.5em; + font-family: monospace; +} + +pre, code { + background-color: #f5f5f5; +} + +table +{ + border-collapse: collapse; +} + +th { + font-weight: bold; + border: 1px solid lightgray; + background-color: #E5E5E5; + text-align: left; + vertical-align: top; + padding: 5px; +} + +td { + border: 1px solid lightgray; + padding: 5px; + vertical-align: top; +} diff --git a/src/3dgraph/doc/example.html b/src/3dgraph/doc/example.html new file mode 100644 index 00000000..b09d4929 --- /dev/null +++ b/src/3dgraph/doc/example.html @@ -0,0 +1,68 @@ + + + + Graph 3D demo + + + + + + + + + + +
+ + diff --git a/src/3dgraph/doc/graph3d.png b/src/3dgraph/doc/graph3d.png new file mode 100644 index 0000000000000000000000000000000000000000..72fc45cd0a7aa62e4136f65685d1b6f67eaf8eca GIT binary patch literal 101584 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

jsD;3iT%c+7>T(`~$=z7WEK3JZC8(zClQ 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 z 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 + + + Graph3d documentation + + + + + + + +

+ +

Graph3d documentation

+ + + + + + + + + + + + + + + +
AuthorJos de Jong, Almende B.V.
WebpageChap Links Library
License Apache License, Version 2.0
+ + +

Contents

+
+ +

Overview

+

+ Graph3d is an interactive visualization chart to draw data in a three dimensional + graph. You can freely move and zoom in the graph by dragging and scrolling in the + window. + Graph3d also supports animation of a graph. +

+ +

+ The graph works smooth on any modern browser for data up to 10.000 points. +

+ +

+ The Graph is developed as a Google Visualization Chart in javascript. + + It runs on all modern browsers without additional requirements. + Graph is tested on Firefox 3.6+, Safari 5.0+, Chrome 6.0+, Opera 10.6+, + Internet Explorer 9+. +

+ +

+ To get started with Graph3d, download + graph3d.zip, + and unzip the contents in a subfolder graph3d on your site. + + Examples can be found in the + examples directory. + + The possiblities of Graph3d can be tested on the + playground. +

+ +

Example

+

+ Here a graph example. Click and drag to move the graph, scroll to zoom the graph. +

+ +

+ +

+
+<!DOCTYPE HTML PUBLIC
+     "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+  <head>
+    <title>Graph 3D demo</title>
+
+    <style>
+      body {font: 10pt arial;}
+    </style>
+
+    <script type="text/javascript" src="http://www.google.com/jsapi"></script>
+    <script type="text/javascript" src="../graph3d.js"></script>
+
+    <script type="text/javascript">
+      var data = null;
+      var graph = null;
+
+      google.load("visualization", "1");
+
+      // Set callback to run when API is loaded
+      google.setOnLoadCallback(drawVisualization);
+
+      function custom(x, y) {
+        return Math.sin(x/50) * Math.cos(y/50) * 50 + 50;
+      }
+
+      // Called when the Visualization API is loaded.
+      function drawVisualization() {
+        // Create and populate a data table.
+        data = new google.visualization.DataTable();
+        data.addColumn('number', 'x');
+        data.addColumn('number', 'y');
+        data.addColumn('number', 'value');
+
+        // create some nice looking data with sin/cos
+        var steps = 25;  // number of datapoints will be steps*steps
+        var axisMax = 314;
+        axisStep = axisMax / steps;
+        for (var x = 0; x < axisMax; x+=axisStep) {
+          for (var y = 0; y < axisMax; y+=axisStep) {
+            var value = custom(x,y);
+            data.addRow([x, y, value]);
+          }
+        }
+
+        // specify options
+        options = {width:  "400px",
+                   height: "400px",
+                   style: "surface",
+                   showPerspective: true,
+                   showGrid: true,
+                   showShadow: false,
+                   keepAspectRatio: true,
+                   verticalRatio: 0.5
+                   };
+
+        // Instantiate our graph object.
+        graph = new links.Graph3d(document.getElementById('mygraph'));
+
+        // Draw our graph with the created data and options
+        graph.draw(data, options);
+      }
+   </script>
+  </head>
+
+  <body>
+    <div id="mygraph"></div>
+  </body>
+</html>
+
+ + +

Loading

+

+ Graph3d is no built-in visualization of Google. + + To load Graph3d, download the file + graph3d.zip, + and unzip it in your html page such that the files are located in a subfolder graph3d. + Include the google API and the following files in the head of your html code: +

+ +
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
+<script type="text/javascript" src="graph/graph3d.js"></script>
+ +

+ The google visualization needs to be loaded in order to use DataTable. +

+
google.load("visualization", "1");
+google.setOnLoadCallback(drawVisualization);
+function drawVisualization() {
+  // load data and create the graph here
+}
+
+ +The class name of the Graph3d is links.Graph3d +
var graph = new links.Graph3d(container);
+ +After being loaded, the graph can be drawn via the function draw(), +provided with data and options. +
graph.draw(data, options);
+where data is a DataTable, and options is a name-value map in the JSON format. + +

Data Format

+

+ Graph3d requires a data table with tree to five columns, depending on the chosen style + and animation. + + The first three columns must contain the location coordinates for x-axis, + y-axis, and z-axis. + + The forth column is optional, and can contain a data value. + + The last column (this can be the fourth or fifth column) and can contain + filter values used for animation. +

+ +

+ Note that the column labels can be changed (for example instead of 'value' + one can use 'Temperature'). +

+ + +

Definition

+

+ The data columns are defined as: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredDescription
xnumberyesLocation on the x-axis.
ynumberyesLocation on the y-axis.
znumberyesLocation on the z-axis.
valuenumbernoThe data value, required for graph styles dot-color and + dot-size. +
filtervalueanytypenoFilter values used for the animation. + This column may have any type, such as a number, string, or Date.
+ +

Construction of 3D data

+

+ Graph3d supports the following styles to display data in three dimensions: + dot, dot-line, line, grid, + and surface. + + The data table for these styles is constructed as follows. +

+ +
+var data = new google.visualization.DataTable();
+data.addColumn('number', 'x');
+data.addColumn('number', 'y');
+data.addColumn('number', 'z');  // the data value, visualized as a height at the z-axis
+                                // and by a color.
+
+data.addRow([2.3, 5.2, 102.1]);
+// ...
+
+ + +

Construction of 4D data

+

+ Graph3d supports the following styles to display data in four dimensions: + dot-color, dot-size. + + The data table for these styles is constructed as follows. +

+ +
+var data = new google.visualization.DataTable();
+data.addColumn('number', 'x');
+data.addColumn('number', 'y');
+data.addColumn('number', 'z');
+data.addColumn('number', 'value');  // the data value, visualized by a color or size
+
+data.addRow([2.3, 5.2, 102.1, 45.2]);
+// ...
+
+ + +

Construction of animation data

+

+ If an extra column with filter values is provided in the data table, Graph3d + will use these values for animation. + + The filter values are grouped and each group represents one animation frame. + + When animating, Graph3d loops through the distinct filter values, + and draws all data points which have the current filter value. +

+

+ For example, to create an animation with three frames, first add all datapoints + for the first frame with a filtervalue 1. Next, add the datapoints for the + second frame and give them a filtervalue 2. Last, add the datapoints for the + third frame and give them a filtervalue of 3. Now, when the Graph3d is loaded, + a slider with buttons play/next/previous will be drawn below the graph, where one can loop through the + three frames. +

+ +

+ To create an animation, add an extra column to the data table. + This column may have any type (number, date, string, ...). +

+ +
+var data = new google.visualization.DataTable();
+data.addColumn('number', 'x');
+data.addColumn('number', 'y');
+data.addColumn('number', 'value');
+data.addColumn('number', 'filtervalue'); // Optional column with filter values for animation
+
+data.addRow([2.3, 5.2, 102.1, 23]);
+// ...
+
+ + +

Configuration Options

+ +

+ Options can be used to customize the graph. Options are defined as a JSON object. + All options are optional. +

+ +
+options = {
+    width:  "100%",
+    height: "400px",
+    style: "surface"
+};
+
+ +

+ The following options are available. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
animationIntervalnumber1000The animation interval in milliseconds. This determines how fast + the animation runs.
animationPreloadbooleanfalseIf false, the animation frames are loaded as soon as they are requested. + if animationPreload is true, the graph will automatically load + all frames in the background, resulting in a smoother animation as soon as + all frames are loaded. The load progress is shown on screen.
animationAutoStartbooleanfalseIf true, the animation starts playing automatically after the graph + is created.
backgroundColorstring or Object"white"The background color for the main area of the chart. + Can be either a simple HTML color string, for example: 'red' or '#00cc00', + or an object with the following properties.
backgroundColor.strokestring"gray"The color of the chart border, as an HTML color string.
backgroundColor.strokeWidthnumber1The border width, in pixels.
backgroundColor.fillstring"white"The chart fill color, as an HTML color string.
cameraPositionObject{"horizontal": 1.0, "vertical": 0.5, "distance": 1.7}Set the initial rotation and position of the camera. + The object cameraPosition contains three parameters: + horizontal, vertical, and distance. + Parameter horizontal is a value in radians and can have any + value (but normally in the range of 0 and 2*Pi). + Parameter vertical is a value in radians between 0 and 0.5*Pi. + Parameter distance is the (normalized) distance from the + camera to the center of the graph, in the range of 0.71 to 5.0. A + larger distance puts the graph further away, making it smaller. + All parameters are optional. +
heightstring"400px"The height of the graph in pixels or as a percentage.
keepAspectRatiobooleantrueIf keepAspectRatio is true, the x-axis and the y-axis + keep their aspect ratio. If false, the axes are scaled such that they + both have the same, maximum with.
showAnimationControlsbooleantrueIf true, animation controls are created at the bottom of the Graph. + The animation controls consists of buttons previous, start/stop, next, + and a slider showing the current frame. + Only applicable when the provided data contains an animation.
showGridbooleantrueIf true, grid lines are draw in the x-y surface (the bottom of the 3d + graph).
showPerspectivebooleantrueIf true, the graph is drawn in perspective: points and lines which + are further away are drawn smaller. + Note that the graph currently does not support a gray colored bottom side + when drawn in perspective. +
showShadowbooleanfalseShow shadow on the graph.
stylestring"dot"The style of the 3d graph. Available styles: + bar, + bar-color, + bar-size, + dot, + dot-line, + dot-color, + dot-size, + line, + grid, + or surface
tooltipboolean | functionfalseShow a tooltip showing the values of the hovered data point. + The contents of the tooltip can be customized by providing a callback + function as tooltip. In this case the function is called + with an object containing parameters x, + y, and z argument, + and must return a string which may contain HTML. +
valueMaxnumbernoneThe maximum value for the value-axis. Only available in combination + with the styles dot-color and dot-size.
valueMinnumbernoneThe minimum value for the value-axis. Only available in combination + with the styles dot-color and dot-size.
verticalRationumber0.5A value between 0.1 and 1.0. This scales the vertical size of the graph + When keepAspectRatio is set to false, and verticalRatio is set to 1.0, + the graph will be a cube.
widthstring"400px"The width of the graph in pixels or as a percentage.
xBarWidthnumbernoneThe width of bars in x direction. By default, the width is equal to the distance + between the data points, such that bars adjoin each other. + Only applicable for styles "bar" and "bar-color".
xCenterstring"55%"The horizontal center position of the graph, as a percentage or in + pixels.
xMaxnumbernoneThe maximum value for the x-axis.
xMinnumbernoneThe minimum value for the x-axis.
xStepnumbernoneStep size for the grid on the x-axis.
yBarWidthnumbernoneThe width of bars in y direction. By default, the width is equal to the distance + between the data points, such that bars adjoin each other. + Only applicable for styles "bar" and "bar-color".
yCenterstring"45%"The vertical center position of the graph, as a percentage or in + pixels.
yMaxnumbernoneThe maximum value for the y-axis.
yMinnumbernoneThe minimum value for the y-axis.
yStepnumbernoneStep size for the grid on the y-axis.
zMinnumbernoneThe minimum value for the z-axis.
zMaxnumbernoneThe maximum value for the z-axis.
zStepnumbernoneStep size for the grid on the z-axis.
+ + +

Methods

+

+ Graph3d supports the following methods. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodReturn TypeDescription
animationStart()noneStart playing the animation. + Only applicable when animation data is available.
animationStop()noneStop playing the animation. + Only applicable when animation data is available.
draw(data, options)noneLoads data, sets the provided options, and draws the 3d graph.
getCameraPosition()An object with parameters horizontal, + vertical and distanceReturns an object with parameters horizontal, + vertical and distance, + which each one of them is a number, representing the rotation and position + of the camera.
redraw()noneRedraw the graph. Useful after the camera position is changed externally, + when data is changed, or when the layout of the webpage changed.
setSize(width, height)noneParameters width and height are strings, + containing a new size for the graph. Size can be provided in pixels + or in percentages.
setCameraPosition (pos){"horizontal": 1.0, "vertical": 0.5, "distance": 1.7}Set the rotation and position of the camera. Parameter pos + is an object which contains three parameters: horizontal, + vertical, and distance. + Parameter horizontal is a value in radians and can have any + value (but normally in the range of 0 and 2*Pi). + Parameter vertical is a value in radians between 0 and 0.5*Pi. + Parameter distance is the (normalized) distance from the + camera to the center of the graph, in the range of 0.71 to 5.0. A + larger distance puts the graph further away, making it smaller. + All parameters are optional. +
+ +

Events

+

+ Graph3d fires events after the camera position has been changed. + The event can be catched by creating a listener. + Here an example on how to catch a camerapositionchange event. +

+ +
+function oncamerapositionchange(event) {
+  alert("The camera position changed to:\n" +
+        "Horizontal: " + event.horizontal + "\n" +
+        "Vertical: " + event.vertical + "\n" +
+        "Distance: " + event.distance);
+}
+
+google.visualization.events.addListener(mygraph, 'camerapositionchange', oncamerapositionchange);
+
+ +

+ The following events are available. +

+ + + + + + + + + + + + + + + + + + + + + + + +
nameDescriptionProperties
camerapositionchangeThe camera position changed. Fired after the user modified the camera position + by moving (dragging) the graph, or by zooming (scrolling), + but not after a call to setCameraPosition method. + The new camera position can be retrieved by calling the method + getCameraPosition. +
    +
  • horizontal: Number. The horizontal angle of the camera.
  • +
  • vertical: Number. The vertical angle of the camera.
  • +
  • distance: Number. The distance of the camera to the center of the graph.
  • +
+
readyThe graph is ready for external method calls. + If you want to interact with the graph, and call methods after you draw it, + you should set up a listener for this event before you call the draw method, + and call them only after the event was fired.none
+ +

Data Policy

+

+ All code and data are processed and rendered in the browser. No data is sent to any server. +

+ +
+ + diff --git a/src/3dgraph/doc/prettify/lang-apollo.js b/src/3dgraph/doc/prettify/lang-apollo.js new file mode 100644 index 00000000..bfc0014c --- /dev/null +++ b/src/3dgraph/doc/prettify/lang-apollo.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["com",/^#[^\r\n]*/,null,"#"],["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)/,null,'"']],[["kwd",/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/, +null],["typ",/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[SE]?BANK\=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],["lit",/^\'(?:-*(?:\w|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?)?/],["pln",/^-*(?:[!-z_]|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?/i],["pun",/^[^\w\t\n\r \xA0()\"\\\';]+/]]),["apollo","agc","aea"]) \ No newline at end of file diff --git a/src/3dgraph/doc/prettify/lang-css.js b/src/3dgraph/doc/prettify/lang-css.js new file mode 100644 index 00000000..61157f38 --- /dev/null +++ b/src/3dgraph/doc/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[ \t\r\n\f]+/,null," \t\r\n\u000c"]],[["str",/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],["str",/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],["kwd",/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//], +["com",/^(?: + + + + + + + + + +
+ + diff --git a/src/3dgraph/examples/example14_styles.html b/src/3dgraph/examples/example14_styles.html new file mode 100644 index 00000000..d1f1f447 --- /dev/null +++ b/src/3dgraph/examples/example14_styles.html @@ -0,0 +1,131 @@ + + + + Graph 3D styles + + + + + + + + + + + +

+ +

+ +

+ +

+ +

+ +

+

+ +

+ +
+ +
+ + diff --git a/src/3dgraph/examples/example15_tooltips.html b/src/3dgraph/examples/example15_tooltips.html new file mode 100644 index 00000000..ceaa1bdb --- /dev/null +++ b/src/3dgraph/examples/example15_tooltips.html @@ -0,0 +1,114 @@ + + + + Graph 3D tooltips + + + + + + + + + + + +

+ +

+ +
+ +
+ + diff --git a/src/3dgraph/examples/index.html b/src/3dgraph/examples/index.html new file mode 100644 index 00000000..b0092210 --- /dev/null +++ b/src/3dgraph/examples/index.html @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/src/3dgraph/graph3d.js b/src/3dgraph/graph3d.js new file mode 100644 index 00000000..b0b47dba --- /dev/null +++ b/src/3dgraph/graph3d.js @@ -0,0 +1,3270 @@ +/** + * @file graph3d.js + * + * @brief + * Graph3d is an interactive google visualization chart to draw data in a + * three dimensional graph. You can freely move and zoom in the graph by + * dragging and scrolling in the window. Graph3d also supports animation. + * + * Graph3d is part of the CHAP Links library. + * + * Graph3d is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and + * Internet Explorer 9+. + * + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (C) 2010-2014 Almende B.V. + * + * @author Jos de Jong, jos@almende.org + * @date 2014-05-27 + * @version 1.4 + */ + +/* + TODO + - add options to add text besides the circles/dots + + - add methods getAnimationIndex, getAnimationCount, setAnimationIndex, setAnimationNext, setAnimationPrev, ... + - add extra examples to the playground + - make default dot color customizable, and also the size, min size and max size of the dots + - calculating size of a dot with style dot-size is not created well. + - problem when animating and there is only one group + - enable gray bottom side of the graph + - add options to customize the color and with of the lines (when style:"line") + - add an option to draw multiple lines in 3d + - add options to draw dots in 3d, with a value represented by a radius or color + - create a function to export as png + window.open(graph.frame.canvas.toDataURL("image/png")); + http://www.nihilogic.dk/labs/canvas2image/ + - option to show network: dots connected by a line. The width or color of a line + can represent a value + + BUGS + - when playing, and you change the data, something goes wrong and the animation starts playing 2x, and cannot be stopped + - opera: right aligning the text on the axis does not work + + DOCUMENTATION + http://en.wikipedia.org/wiki/3D_projection + + */ + + +/** + * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library, + * "links" + */ +if (typeof links === 'undefined') { + links = {}; + // important: do not use var, as "var links = {};" will overwrite + // the existing links variable value with undefined in IE8, IE7. +} + +/** + * @constructor links.Graph3d + * The Graph is a visualization Graphs on a time line + * + * Graph is developed in javascript as a Google Visualization Chart. + * + * @param {Element} container The DOM element in which the Graph will + * be created. Normally a div element. + */ +links.Graph3d = function (container) { + // create variables and set default values + this.containerElement = container; + this.width = "400px"; + this.height = "400px"; + this.margin = 10; // px + this.defaultXCenter = "55%"; + this.defaultYCenter = "50%"; + + this.style = links.Graph3d.STYLE.DOT; + this.showPerspective = true; + this.showGrid = true; + this.keepAspectRatio = true; + this.showShadow = false; + this.showGrayBottom = false; // TODO: this does not work correctly + this.showTooltip = false; + this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a "cube" + + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; + + this.camera = new links.Graph3d.Camera(); + this.eye = new links.Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects + + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; + + this.xMin = 0; + this.xStep = undefined; // auto by default + this.xMax = 1; + this.yMin = 0; + this.yStep = undefined; // auto by default + this.yMax = 1; + this.zMin = 0; + this.zStep = undefined; // auto by default + this.zMax = 1; + this.valueMin = 0; + this.valueMax = 1; + this.xBarWidth = 1; + this.yBarWidth = 1; + // TODO: customize axis range + + // constants + this.colorAxis = "#4D4D4D"; + this.colorGrid = "#D3D3D3"; + this.colorDot = "#7DC1FF"; + this.colorDotBorder = "#3267D2"; + + // create a frame and canvas + this.create(); +}; + +/** + * @class Camera + * The camera is mounted on a (virtual) camera arm. The camera arm can rotate + * The camera is always looking in the direction of the origin of the arm. + * This way, the camera always rotates around one fixed point, the location + * of the camera arm. + * + * Documentation: + * http://en.wikipedia.org/wiki/3D_projection + */ +links.Graph3d.Camera = function () { + this.armLocation = new links.Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; + + this.cameraLocation = new links.Point3d(); + this.cameraRotation = new links.Point3d(0.5*Math.PI, 0, 0); + + this.calculateCameraOrientation(); +}; + +/** + * Set the location (origin) of the arm + * @param {number} x Normalized value of x + * @param {number} y Normalized value of y + * @param {number} z Normalized value of z + */ +links.Graph3d.Camera.prototype.setArmLocation = function(x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; + + this.calculateCameraOrientation(); +}; + +/** + * Set the rotation of the camera arm + * @param {number} horizontal The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * @param {number} vertical The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + */ +links.Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; + } + + if (vertical !== undefined) { + this.armRotation.vertical = vertical; + if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; + if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI; + } + + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); + } +}; + +/** + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical + */ +links.Graph3d.Camera.prototype.getArmRotation = function() { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; + + return rot; +}; + +/** + * Set the (normalized) length of the camera arm. + * @param {number} length A length between 0.71 and 5.0 + */ +links.Graph3d.Camera.prototype.setArmLength = function(length) { + if (length === undefined) + return; + + this.armLength = length; + + // Radius must be larger than the corner of the graph, + // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the + // graph + if (this.armLength < 0.71) this.armLength = 0.71; + if (this.armLength > 5.0) this.armLength = 5.0; + + this.calculateCameraOrientation(); +}; + +/** + * Retrieve the arm length + * @return {number} length + */ +links.Graph3d.Camera.prototype.getArmLength = function() { + return this.armLength; +}; + +/** + * Retrieve the camera location + * @return {links.Point3d} cameraLocation + */ +links.Graph3d.Camera.prototype.getCameraLocation = function() { + return this.cameraLocation; +}; + +/** + * Retrieve the camera rotation + * @return {links.Point3d} cameraRotation + */ +links.Graph3d.Camera.prototype.getCameraRotation = function() { + return this.cameraRotation; +}; + +/** + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm + */ +links.Graph3d.Camera.prototype.calculateCameraOrientation = function() { + // calculate location of the camera + this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); + + // calculate rotation of the camera + this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; + this.cameraRotation.y = 0; + this.cameraRotation.z = -this.armRotation.horizontal; +}; + +/** + * Calculate the scaling values, dependent on the range in x, y, and z direction + */ +links.Graph3d.prototype._setScale = function() { + this.scale = new links.Point3d(1 / (this.xMax - this.xMin), + 1 / (this.yMax - this.yMin), + 1 / (this.zMax - this.zMin)); + + // keep aspect ration between x and y scale if desired + if (this.keepAspectRatio) { + if (this.scale.x < this.scale.y) { + //noinspection JSSuspiciousNameCombination + this.scale.y = this.scale.x; + } + else { + //noinspection JSSuspiciousNameCombination + this.scale.x = this.scale.y; + } + } + + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? + + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); + + // position the camera arm + var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; + var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; + var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; + this.camera.setArmLocation(xCenter, yCenter, zCenter); +}; + + +/** + * Convert a 3D location to a 2D location on screen + * http://en.wikipedia.org/wiki/3D_projection + * @param {links.Point3d} point3d A 3D point with parameters x, y, z + * @return {links.Point2d} point2d A 2D point with parameters x, y + */ +links.Graph3d.prototype._convert3Dto2D = function(point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); +}; + +/** + * Convert a 3D location its translation seen from the camera + * http://en.wikipedia.org/wiki/3D_projection + * @param {links.Point3d} point3d A 3D point with parameters x, y, z + * @return {links.Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera + */ +links.Graph3d.prototype._convertPointToTranslation = function(point3d) { + var ax = point3d.x * this.scale.x, + ay = point3d.y * this.scale.y, + az = point3d.z * this.scale.z, + + cx = this.camera.getCameraLocation().x, + cy = this.camera.getCameraLocation().y, + cz = this.camera.getCameraLocation().z, + + // calculate angles + sinTx = Math.sin(this.camera.getCameraRotation().x), + cosTx = Math.cos(this.camera.getCameraRotation().x), + sinTy = Math.sin(this.camera.getCameraRotation().y), + cosTy = Math.cos(this.camera.getCameraRotation().y), + sinTz = Math.sin(this.camera.getCameraRotation().z), + cosTz = Math.cos(this.camera.getCameraRotation().z), + + // calculate translation + dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), + dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)), + dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx)); + + return new links.Point3d(dx, dy, dz); +}; + +/** + * Convert a translation point to a point on the screen + * @param {links.Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera + * @return {links.Point2d} point2d A 2D point with parameters x, y + */ +links.Graph3d.prototype._convertTranslationToScreen = function(translation) { + var ex = this.eye.x, + ey = this.eye.y, + ez = this.eye.z, + dx = translation.x, + dy = translation.y, + dz = translation.z; + + // calculate position on screen from translation + var bx; + var by; + if (this.showPerspective) { + bx = (dx - ex) * (ez / dz); + by = (dy - ey) * (ez / dz); + } + else { + bx = dx * -(ez / this.camera.getArmLength()); + by = dy * -(ez / this.camera.getArmLength()); + } + + // shift and scale the point to the center of the screen + // use the width of the graph to scale both horizontally and vertically. + return new links.Point2d( + this.xcenter + bx * this.frame.canvas.clientWidth, + this.ycenter - by * this.frame.canvas.clientWidth); +}; + +/** + * Main drawing logic. This is the function that needs to be called + * in the html page, to draw the Graph. + * + * A data table with the events must be provided, and an options table. + * @param {google.visualization.DataTable} data The data containing the events + * for the Graph. + * @param {Object} options A name/value map containing settings for the Graph. + */ +links.Graph3d.prototype.draw = function(data, options) { + var cameraPosition = undefined; + + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; + + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + + if (options.style !== undefined) { + var styleNumber = this._getStyleNumber(options.style); + if (styleNumber !== -1) { + this.style = styleNumber; + } + } + if (options.showGrid !== undefined) this.showGrid = options.showGrid; + if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; + if (options.showShadow !== undefined) this.showShadow = options.showShadow; + if (options.tooltip !== undefined) this.showTooltip = options.tooltip; + if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; + if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; + if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; + + if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart; + + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; + + if (options.xMin !== undefined) this.defaultXMin = options.xMin; + if (options.xStep !== undefined) this.defaultXStep = options.xStep; + if (options.xMax !== undefined) this.defaultXMax = options.xMax; + if (options.yMin !== undefined) this.defaultYMin = options.yMin; + if (options.yStep !== undefined) this.defaultYStep = options.yStep; + if (options.yMax !== undefined) this.defaultYMax = options.yMax; + if (options.zMin !== undefined) this.defaultZMin = options.zMin; + if (options.zStep !== undefined) this.defaultZStep = options.zStep; + if (options.zMax !== undefined) this.defaultZMax = options.zMax; + if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; + if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; + + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + } + + this._setBackgroundColor(options.backgroundColor); + + this.setSize(this.width, this.height); + + if (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); + } + else { + this.camera.setArmRotation(1.0, 0.5); + this.camera.setArmLength(1.7); + } + + // draw the Graph + this.redraw(data); + + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + + // fire the ready event + google.visualization.events.trigger(this, 'ready', null); +}; + + +/** + * Set the background styling for the graph + * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor + */ +links.Graph3d.prototype._setBackgroundColor = function(backgroundColor) { + var fill = "white"; + var stroke = "gray"; + var strokeWidth = 1; + + if (typeof(backgroundColor) === "string") { + fill = backgroundColor; + stroke = "none"; + strokeWidth = 0; + } + else if (typeof(backgroundColor) === "object") { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } + else if (backgroundColor === undefined) { + // use use defaults + } + else { + throw "Unsupported type of backgroundColor"; + } + + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + "px"; + this.frame.style.borderStyle = "solid"; +}; + + +/// enumerate the available styles +links.Graph3d.STYLE = { + BAR: 0, + BARCOLOR: 1, + BARSIZE: 2, + DOT : 3, + DOTLINE : 4, + DOTCOLOR: 5, + DOTSIZE: 6, + GRID : 7, + LINE: 8, + SURFACE : 9 +}; + +/** + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as "dot", "grid", "dot-line" + * @return {number} styleNumber Enumeration value representing the style, or -1 + * when not found + */ +links.Graph3d.prototype._getStyleNumber = function(styleName) { + switch (styleName) { + case "dot": return links.Graph3d.STYLE.DOT; + case "dot-line": return links.Graph3d.STYLE.DOTLINE; + case "dot-color": return links.Graph3d.STYLE.DOTCOLOR; + case "dot-size": return links.Graph3d.STYLE.DOTSIZE; + case "line": return links.Graph3d.STYLE.LINE; + case "grid": return links.Graph3d.STYLE.GRID; + case "surface": return links.Graph3d.STYLE.SURFACE; + case "bar": return links.Graph3d.STYLE.BAR; + case "bar-color": return links.Graph3d.STYLE.BARCOLOR; + case "bar-size": return links.Graph3d.STYLE.BARSIZE; + } + + return -1; +}; + +/** + * Determine the indexes of the data columns, based on the given style and data + * @param {google.visualization.DataTable} data + * @param {number} style + */ +links.Graph3d.prototype._determineColumnIndexes = function(data, style) { + if (this.style === links.Graph3d.STYLE.DOT || + this.style === links.Graph3d.STYLE.DOTLINE || + this.style === links.Graph3d.STYLE.LINE || + this.style === links.Graph3d.STYLE.GRID || + this.style === links.Graph3d.STYLE.SURFACE || + this.style === links.Graph3d.STYLE.BAR) { + // 3 columns expected, and optionally a 4th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = undefined; + + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; + } + } + else if (this.style === links.Graph3d.STYLE.DOTCOLOR || + this.style === links.Graph3d.STYLE.DOTSIZE || + this.style === links.Graph3d.STYLE.BARCOLOR || + this.style === links.Graph3d.STYLE.BARSIZE) { + // 4 columns expected, and optionally a 5th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = 3; + + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; + } + } + else { + throw "Unknown style '" + this.style + "'"; + } +}; + +/** + * Initialize the data from the data table. Calculate minimum and maximum values + * and column index values + * @param {google.visualization.DataTable} data The data containing the events + * for the Graph. + * @param {number} style Style number + */ +links.Graph3d.prototype._dataInitialize = function (data, style) { + if (data === undefined || data.getNumberOfRows === undefined) + return; + + // determine the location of x,y,z,value,filter columns + this._determineColumnIndexes(data, style); + + this.dataTable = data; + this.dataFilter = undefined; + + // check if a filter column is provided + if (this.colFilter && data.getNumberOfColumns() >= this.colFilter) { + if (this.dataFilter === undefined) { + this.dataFilter = new links.Filter(data, this.colFilter, this); + + var me = this; + this.dataFilter.setOnLoadCallback(function() {me.redraw();}); + } + } + + var withBars = this.style == links.Graph3d.STYLE.BAR || + this.style == links.Graph3d.STYLE.BARCOLOR || + this.style == links.Graph3d.STYLE.BARSIZE; + + // determine barWidth from data + if (withBars) { + if (this.defaultXBarWidth !== undefined) { + this.xBarWidth = this.defaultXBarWidth; + } + else { + var dataX = data.getDistinctValues(this.colX); + this.xBarWidth = (dataX[1] - dataX[0]) || 1; + } + + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } + else { + var dataY = data.getDistinctValues(this.colY); + this.yBarWidth = (dataY[1] - dataY[0]) || 1; + } + } + + // calculate minimums and maximums + var xRange = data.getColumnRange(this.colX); + if (withBars) { + xRange.min -= this.xBarWidth / 2; + xRange.max += this.xBarWidth / 2; + } + this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min; + this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max; + if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; + this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5; + + var yRange = data.getColumnRange(this.colY); + if (withBars) { + yRange.min -= this.yBarWidth / 2; + yRange.max += this.yBarWidth / 2; + } + this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min; + this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max; + if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; + this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5; + + var zRange = data.getColumnRange(this.colZ); + this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min; + this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max; + if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; + this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5; + + if (this.colValue !== undefined) { + var valueRange = data.getColumnRange(this.colValue); + this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min; + this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max; + if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + } + + // set the scale dependent on the ranges. + this._setScale(); +}; + + + +/** + * Filter the data based on the current filter + * @param {google.visualization.DataTable} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen + */ +links.Graph3d.prototype._getDataPoints = function (data) { + // TODO: store the created matrix dataPoints in the filters instead of reloading each time + var x, y, i, z, obj, point; + + var dataPoints = []; + + if (this.style === links.Graph3d.STYLE.GRID || + this.style === links.Graph3d.STYLE.SURFACE) { + // copy all values from the google data table to a matrix + // the provided values are supposed to form a grid of (x,y) positions + + // create two lists with all present x and y values + var dataX = []; + var dataY = []; + for (i = 0; i < data.getNumberOfRows(); i++) { + x = data.getValue(i, this.colX) || 0; + y = data.getValue(i, this.colY) || 0; + + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } + } + + function sortNumber(a, b) { + return a - b; + } + dataX.sort(sortNumber); + dataY.sort(sortNumber); + + // create a grid, a 2d matrix, with all values. + var dataMatrix = []; // temporary data matrix + for (i = 0; i < data.getNumberOfRows(); i++) { + x = data.getValue(i, this.colX) || 0; + y = data.getValue(i, this.colY) || 0; + z = data.getValue(i, this.colZ) || 0; + + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); + + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } + + var point3d = new links.Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; + + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new links.Point3d(x, y, this.zMin); + + dataMatrix[xIndex][yIndex] = obj; + + dataPoints.push(obj); + } + + // fill in the pointers to the neighbors. + for (x = 0; x < dataMatrix.length; x++) { + for (y = 0; y < dataMatrix[x].length; y++) { + if (dataMatrix[x][y]) { + dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined; + dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined; + dataMatrix[x][y].pointCross = + (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ? + dataMatrix[x+1][y+1] : + undefined; + } + } + } + } + else { // "dot", "dot-line", etc. + // copy all values from the google data table to a list with Point3d objects + for (i = 0; i < data.getNumberOfRows(); i++) { + point = new links.Point3d(); + point.x = data.getValue(i, this.colX) || 0; + point.y = data.getValue(i, this.colY) || 0; + point.z = data.getValue(i, this.colZ) || 0; + + if (this.colValue !== undefined) { + point.value = data.getValue(i, this.colValue) || 0; + } + + obj = {}; + obj.point = point; + obj.bottom = new links.Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; + + dataPoints.push(obj); + } + } + + return dataPoints; +}; + + + + +/** + * Append suffix "px" to provided value x + * @param {int} x An integer value + * @return {string} the string value of x, followed by the suffix "px" + */ +links.Graph3d.px = function(x) { + return x + "px"; +}; + + +/** + * Create the main frame for the Graph3d. + * This function is executed once when a Graph3d object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + */ +links.Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } + + this.frame = document.createElement("div"); + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement( "canvas" ); + this.frame.canvas.style.position = "relative"; + this.frame.appendChild(this.frame.canvas); + //if (!this.frame.canvas.getContext) { + { + var noCanvas = document.createElement( "DIV" ); + noCanvas.style.color = "red"; + noCanvas.style.fontWeight = "bold" ; + noCanvas.style.padding = "10px"; + noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; + this.frame.canvas.appendChild(noCanvas); + } + + this.frame.filter = document.createElement( "div" ); + this.frame.filter.style.position = "absolute"; + this.frame.filter.style.bottom = "0px"; + this.frame.filter.style.left = "0px"; + this.frame.filter.style.width = "100%"; + this.frame.appendChild(this.frame.filter); + + // add event listeners to handle moving and zooming the contents + var me = this; + var onmousedown = function (event) {me._onMouseDown(event);}; + var ontouchstart = function (event) {me._onTouchStart(event);}; + var onmousewheel = function (event) {me._onWheel(event);}; + var ontooltip = function (event) {me._onTooltip(event);}; + // TODO: these events are never cleaned up... can give a "memory leakage" + + links.addEventListener(this.frame.canvas, "keydown", onkeydown); + links.addEventListener(this.frame.canvas, "mousedown", onmousedown); + links.addEventListener(this.frame.canvas, "touchstart", ontouchstart); + links.addEventListener(this.frame.canvas, "mousewheel", onmousewheel); + links.addEventListener(this.frame.canvas, "mousemove", ontooltip); + + // add the new graph to the container element + this.containerElement.appendChild(this.frame); +}; + + +/** + * Set a new size for the graph + * @param {string} width Width in pixels or percentage (for example "800px" + * or "50%") + * @param {string} height Height in pixels or percentage (for example "400px" + * or "30%") + */ +links.Graph3d.prototype.setSize = function(width, height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this._resizeCanvas(); +}; + +/** + * Resize the canvas to the current size of the frame + */ +links.Graph3d.prototype._resizeCanvas = function() { + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; + + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; + + // adjust with for margin + this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + "px"; +}; + +/** + * Start animation + */ +links.Graph3d.prototype.animationStart = function() { + if (!this.frame.filter || !this.frame.filter.slider) + throw "No animation available"; + + this.frame.filter.slider.play(); +}; + + +/** + * Stop animation + */ +links.Graph3d.prototype.animationStop = function() { + if (!this.frame.filter || !this.frame.filter.slider) + throw "No animation available"; + + this.frame.filter.slider.stop(); +}; + + +/** + * Resize the center position based on the current values in this.defaultXCenter + * and this.defaultYCenter (which are strings with a percentage or a value + * in pixels). The center positions are the variables this.xCenter + * and this.yCenter + */ +links.Graph3d.prototype._resizeCenter = function() { + // calculate the horizontal center position + if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === "%") { + this.xcenter = + parseFloat(this.defaultXCenter) / 100 * + this.frame.canvas.clientWidth; + } + else { + this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px + } + + // calculate the vertical center position + if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === "%") { + this.ycenter = + parseFloat(this.defaultYCenter) / 100 * + (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); + } + else { + this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px + } +}; + +/** + * Set the rotation and distance of the camera + * @param {Object} pos An object with the camera position. The object + * contains three parameters: + * - horizontal {number} + * The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * - vertical {number} + * The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + * - distance {number} + * The (normalized) distance of the camera to the + * center of the graph, a value between 0.71 and 5.0. + * Optional, can be left undefined. + */ +links.Graph3d.prototype.setCameraPosition = function(pos) { + if (pos === undefined) { + return; + } + + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); + } + + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); + } + + this.redraw(); +}; + + +/** + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance + */ +links.Graph3d.prototype.getCameraPosition = function() { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; +}; + +/** + * Load data into the 3D Graph + */ +links.Graph3d.prototype._readData = function(data) { + // read the data + this._dataInitialize(data, this.style); + + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } + else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); + } + + // draw the filter + this._redrawFilter(); +}; + + +/** + * Redraw the Graph. This needs to be executed after the start and/or + * end time are changed, or when data is added or removed dynamically. + * @param {google.visualization.DataTable} data Optional, new data table + */ +links.Graph3d.prototype.redraw = function(data) { + // load the data if needed + if (data !== undefined) { + this._readData(data); + } + + if (this.dataPoints === undefined) { + throw "Error: graph data not initialized"; + } + + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); + + if (this.style === links.Graph3d.STYLE.GRID || + this.style === links.Graph3d.STYLE.SURFACE) { + this._redrawDataGrid(); + } + else if (this.style === links.Graph3d.STYLE.LINE) { + this._redrawDataLine(); + } + else if (this.style === links.Graph3d.STYLE.BAR || + this.style === links.Graph3d.STYLE.BARCOLOR || + this.style === links.Graph3d.STYLE.BARSIZE) { + this._redrawDataBar(); + } + else { + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); + } + + this._redrawInfo(); + this._redrawLegend(); +}; + +/** + * Clear the canvas before redrawing + */ +links.Graph3d.prototype._redrawClear = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + + ctx.clearRect(0, 0, canvas.width, canvas.height); +}; + + +/** + * Redraw the legend showing the colors + */ +links.Graph3d.prototype._redrawLegend = function() { + var y; + + if (this.style === links.Graph3d.STYLE.DOTCOLOR || + this.style === links.Graph3d.STYLE.DOTSIZE) { + + var dotSize = this.frame.clientWidth * 0.02; + + var widthMin, widthMax; + if (this.style === links.Graph3d.STYLE.DOTSIZE) { + widthMin = dotSize / 2; // px + widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function + } + else { + widthMin = 20; // px + widthMax = 20; // px + } + + var height = Math.max(this.frame.clientHeight * 0.25, 100); + var top = this.margin; + var right = this.frame.clientWidth - this.margin; + var left = right - widthMax; + var bottom = top + height; + } + + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + ctx.lineWidth = 1; + ctx.font = "14px arial"; // TODO: put in options + + if (this.style === links.Graph3d.STYLE.DOTCOLOR) { + // draw the color bar + var ymin = 0; + var ymax = height; // Todo: make height customizable + for (y = ymin; y < ymax; y++) { + var f = (y - ymin) / (ymax - ymin); + + //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function + var hue = f * 240; + var color = this._hsv2rgb(hue, 1, 1); + + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } + + ctx.strokeStyle = this.colorAxis; + ctx.strokeRect(left, top, widthMax, height); + } + + if (this.style === links.Graph3d.STYLE.DOTSIZE) { + // draw border around color bar + ctx.strokeStyle = this.colorAxis; + ctx.fillStyle = this.colorDot; + ctx.beginPath(); + ctx.moveTo(left, top); + ctx.lineTo(right, top); + ctx.lineTo(right - widthMax + widthMin, bottom); + ctx.lineTo(left, bottom); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + + if (this.style === links.Graph3d.STYLE.DOTCOLOR || + this.style === links.Graph3d.STYLE.DOTSIZE) { + // print values along the color bar + var gridLineLen = 5; // px + var step = new links.StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true); + step.start(); + if (step.getCurrent() < this.valueMin) { + step.next(); + } + while (!step.end()) { + y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; + + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + + step.next(); + } + + ctx.textAlign = "right"; + ctx.textBaseline = "top"; + var label = this.dataTable.getColumnLabel(this.colValue); + ctx.fillText(label, right, bottom + this.margin); + } +}; + +/** + * Redraw the filter + */ +links.Graph3d.prototype._redrawFilter = function() { + this.frame.filter.innerHTML = ""; + + if (this.dataFilter) { + var options = { + 'visible': this.showAnimationControls + }; + var slider = new links.Slider(this.frame.filter, options); + this.frame.filter.slider = slider; + + // TODO: css here is not nice here... + this.frame.filter.style.padding = "10px"; + //this.frame.filter.style.backgroundColor = "#EFEFEF"; + + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); + + // create an event handler + var me = this; + var onchange = function () { + var index = slider.getIndex(); + + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); + + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } + else { + this.frame.filter.slider = undefined; + } +}; + +/** + * Redraw the slider + */ +links.Graph3d.prototype._redrawSlider = function() { + if ( this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); + } +}; + + +/** + * Redraw common information + */ +links.Graph3d.prototype._redrawInfo = function() { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + + ctx.font = "14px arial"; // TODO: put in options + ctx.lineStyle = "gray"; + ctx.fillStyle = "gray"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); + } +}; + + +/** + * Redraw the axis + */ +links.Graph3d.prototype._redrawAxis = function() { + var canvas = this.frame.canvas, + ctx = canvas.getContext("2d"), + from, to, step, prettyStep, + text, xText, yText, zText, + offset, xOffset, yOffset, + xMin2d, xMax2d; + + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + "px arial"; + + // calculate the length for the short grid lines + var gridLenX = 0.025 / this.scale.x; + var gridLenY = 0.025 / this.scale.y; + var textMargin = 5 / this.camera.getArmLength(); // px + var armAngle = this.camera.getArmRotation().horizontal; + + // draw x-grid lines + ctx.lineWidth = 1; + prettyStep = (this.defaultXStep === undefined); + step = new links.StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); + step.start(); + if (step.getCurrent() < this.xMin) { + step.next(); + } + while (!step.end()) { + var x = step.getCurrent(); + + if (this.showGrid) { + from = this._convert3Dto2D(new links.Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new links.Point3d(x, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } + else { + from = this._convert3Dto2D(new links.Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new links.Point3d(x, this.yMin+gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + + from = this._convert3Dto2D(new links.Point3d(x, this.yMax, this.zMin)); + to = this._convert3Dto2D(new links.Point3d(x, this.yMax-gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } + + yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax; + text = this._convert3Dto2D(new links.Point3d(x, yText, this.zMin)); + if (Math.cos(armAngle * 2) > 0) { + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + text.y += textMargin; + } + else if (Math.sin(armAngle * 2) < 0){ + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } + else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(" " + step.getCurrent() + " ", text.x, text.y); + + step.next(); + } + + // draw y-grid lines + ctx.lineWidth = 1; + prettyStep = (this.defaultYStep === undefined); + step = new links.StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); + step.start(); + if (step.getCurrent() < this.yMin) { + step.next(); + } + while (!step.end()) { + if (this.showGrid) { + from = this._convert3Dto2D(new links.Point3d(this.xMin, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new links.Point3d(this.xMax, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } + else { + from = this._convert3Dto2D(new links.Point3d(this.xMin, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new links.Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + + from = this._convert3Dto2D(new links.Point3d(this.xMax, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new links.Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } + + xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax; + text = this._convert3Dto2D(new links.Point3d(xText, step.getCurrent(), this.zMin)); + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + text.y += textMargin; + } + else if (Math.sin(armAngle * 2) > 0){ + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } + else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(" " + step.getCurrent() + " ", text.x, text.y); + + step.next(); + } + + // draw z-grid lines and axis + ctx.lineWidth = 1; + prettyStep = (this.defaultZStep === undefined); + step = new links.StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); + step.start(); + if (step.getCurrent() < this.zMin) { + step.next(); + } + xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; + yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; + while (!step.end()) { + // TODO: make z-grid lines really 3d? + from = this._convert3Dto2D(new links.Point3d(xText, yText, step.getCurrent())); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(from.x - textMargin, from.y); + ctx.stroke(); + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent() + " ", from.x - 5, from.y); + + step.next(); + } + ctx.lineWidth = 1; + from = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMin)); + to = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMax)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + + // draw x-axis + ctx.lineWidth = 1; + // line at yMin + xMin2d = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMin, this.zMin)); + xMax2d = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMin, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); + // line at ymax + xMin2d = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMax, this.zMin)); + xMax2d = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); + + // draw y-axis + ctx.lineWidth = 1; + // line at xMin + from = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMin, this.zMin)); + to = this._convert3Dto2D(new links.Point3d(this.xMin, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + // line at xMax + from = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMin, this.zMin)); + to = this._convert3Dto2D(new links.Point3d(this.xMax, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + + // draw x-label + var xLabel = this.dataTable.getColumnLabel(this.colX); + if (xLabel.length > 0) { + yOffset = 0.1 / this.scale.y; + xText = (this.xMin + this.xMax) / 2; + yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset; + text = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMin)); + if (Math.cos(armAngle * 2) > 0) { + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + } + else if (Math.sin(armAngle * 2) < 0){ + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } + else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(xLabel, text.x, text.y); + } + + // draw y-label + var yLabel = this.dataTable.getColumnLabel(this.colY); + if (yLabel.length > 0) { + xOffset = 0.1 / this.scale.x; + xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset; + yText = (this.yMin + this.yMax) / 2; + text = this._convert3Dto2D(new links.Point3d(xText, yText, this.zMin)); + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + } + else if (Math.sin(armAngle * 2) > 0){ + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } + else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(yLabel, text.x, text.y); + } + + // draw z-label + var zLabel = this.dataTable.getColumnLabel(this.colZ); + if (zLabel.length > 0) { + offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? + xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax; + yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax; + zText = (this.zMin + this.zMax) / 2; + text = this._convert3Dto2D(new links.Point3d(xText, yText, zText)); + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(zLabel, text.x - offset, text.y); + } +}; + +/** + * Calculate the color based on the given value. + * @param {number} H Hue, a value be between 0 and 360 + * @param {number} S Saturation, a value between 0 and 1 + * @param {number} V Value, a value between 0 and 1 + */ +links.Graph3d.prototype._hsv2rgb = function(H, S, V) { + var R, G, B, C, Hi, X; + + C = V * S; + Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(((H/60) % 2) - 1)); + + switch (Hi) { + case 0: R = C; G = X; B = 0; break; + case 1: R = X; G = C; B = 0; break; + case 2: R = 0; G = C; B = X; break; + case 3: R = 0; G = X; B = C; break; + case 4: R = X; G = 0; B = C; break; + case 5: R = C; G = 0; B = X; break; + + default: R = 0; G = 0; B = 0; break; + } + + return "RGB(" + parseInt(R*255) + "," + parseInt(G*255) + "," + parseInt(B*255) + ")"; +}; + + +/** + * Draw all datapoints as a grid + * This function can be used when the style is "grid" + */ +links.Graph3d.prototype._redrawDataGrid = function() { + var canvas = this.frame.canvas, + ctx = canvas.getContext("2d"), + point, right, top, cross, + i, + topSideVisible, fillStyle, strokeStyle, lineWidth, + h, s, v, zAvg; + + + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? + + // calculate the translations and screen position of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + + // calculate the translation of the point at the bottom (needed for sorting) + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + } + + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); + + if (this.style === links.Graph3d.STYLE.SURFACE) { + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + cross = this.dataPoints[i].pointCross; + + if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + + if (this.showGrayBottom || this.showShadow) { + // calculate the cross product of the two vectors from center + // to left and right, in order to know whether we are looking at the + // bottom or at the top side. We can also use the cross product + // for calculating light intensity + var aDiff = links.Point3d.subtract(cross.trans, point.trans); + var bDiff = links.Point3d.subtract(top.trans, right.trans); + var crossproduct = links.Point3d.crossProduct(aDiff, bDiff); + var len = crossproduct.length(); + // FIXME: there is a bug with determining the surface side (shadow or colored) + + topSideVisible = (crossproduct.z > 0); + } + else { + topSideVisible = true; + } + + if (topSideVisible) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + s = 1; // saturation + + if (this.showShadow) { + v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = fillStyle; + } + else { + v = 1; + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = this.colorAxis; + } + } + else { + fillStyle = "gray"; + strokeStyle = this.colorAxis; + } + lineWidth = 0.5; + + ctx.lineWidth = lineWidth; + ctx.fillStyle = fillStyle; + ctx.strokeStyle = strokeStyle; + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(right.screen.x, right.screen.y); + ctx.lineTo(cross.screen.x, cross.screen.y); + ctx.lineTo(top.screen.x, top.screen.y); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + } + } + else { // grid style + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + + if (point !== undefined) { + if (this.showPerspective) { + lineWidth = 2 / -point.trans.z; + } + else { + lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); + } + } + + if (point !== undefined && right !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + right.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + + ctx.lineWidth = lineWidth; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(right.screen.x, right.screen.y); + ctx.stroke(); + } + + if (point !== undefined && top !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + top.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + + ctx.lineWidth = lineWidth; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(top.screen.x, top.screen.y); + ctx.stroke(); + } + } + } +}; + + +/** + * Draw all datapoints as dots. + * This function can be used when the style is "dot" or "dot-line" + */ +links.Graph3d.prototype._redrawDataDot = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + var i; + + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? + + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + + // calculate the distance from the point at the bottom to the camera + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + } + + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); + + // draw the datapoints as colored circles + var dotSize = this.frame.clientWidth * 0.02; // px + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; + + if (this.style === links.Graph3d.STYLE.DOTLINE) { + // draw a vertical line from the bottom to the graph value + //var from = this._convert3Dto2D(new links.Point3d(point.point.x, point.point.y, this.zMin)); + var from = this._convert3Dto2D(point.bottom); + ctx.lineWidth = 1; + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(point.screen.x, point.screen.y); + ctx.stroke(); + } + + // calculate radius for the circle + var size; + if (this.style === links.Graph3d.STYLE.DOTSIZE) { + size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + } + else { + size = dotSize; + } + + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } + else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; + } + + var hue, color, borderColor; + if (this.style === links.Graph3d.STYLE.DOTCOLOR ) { + // calculate the color based on the value + hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } + else if (this.style === links.Graph3d.STYLE.DOTSIZE) { + color = this.colorDot; + borderColor = this.colorDotBorder; + } + else { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } + + // draw the circle + ctx.lineWidth = 1.0; + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true); + ctx.fill(); + ctx.stroke(); + } +}; + +/** + * Draw all datapoints as bars. + * This function can be used when the style is "bar", "bar-color", or "bar-size" + */ +links.Graph3d.prototype._redrawDataBar = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + var i, j, surface, corners; + + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? + + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + + // calculate the distance from the point at the bottom to the camera + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + } + + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); + + // draw the datapoints as bars + var xWidth = this.xBarWidth / 2; + var yWidth = this.yBarWidth / 2; + var dotSize = this.frame.clientWidth * 0.02; // px + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; + + // determine color + var hue, color, borderColor; + if (this.style === links.Graph3d.STYLE.BARCOLOR ) { + // calculate the color based on the value + hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } + else if (this.style === links.Graph3d.STYLE.BARSIZE) { + color = this.colorDot; + borderColor = this.colorDotBorder; + } + else { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } + + // calculate size for the bar + if (this.style === links.Graph3d.STYLE.BARSIZE) { + xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + } + + // calculate all corner points + var me = this; + var point3d = point.point; + var top = [ + {point: new links.Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)}, + {point: new links.Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)}, + {point: new links.Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)}, + {point: new links.Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)} + ]; + var bottom = [ + {point: new links.Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)}, + {point: new links.Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)}, + {point: new links.Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)}, + {point: new links.Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)} + ]; + + // calculate screen location of the points + top.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + bottom.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + + // create five sides, calculate both corner points and center points + var surfaces = [ + {corners: top, center: links.Point3d.avg(bottom[0].point, bottom[2].point)}, + {corners: [top[0], top[1], bottom[1], bottom[0]], center: links.Point3d.avg(bottom[1].point, bottom[0].point)}, + {corners: [top[1], top[2], bottom[2], bottom[1]], center: links.Point3d.avg(bottom[2].point, bottom[1].point)}, + {corners: [top[2], top[3], bottom[3], bottom[2]], center: links.Point3d.avg(bottom[3].point, bottom[2].point)}, + {corners: [top[3], top[0], bottom[0], bottom[3]], center: links.Point3d.avg(bottom[0].point, bottom[3].point)} + ]; + point.surfaces = surfaces; + + // calculate the distance of each of the surface centers to the camera + for (j = 0; j < surfaces.length; j++) { + surface = surfaces[j]; + var transCenter = this._convertPointToTranslation(surface.center); + surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; + // TODO: this dept calculation doesn't work 100% of the cases due to perspective, + // but the current solution is fast/simple and works in 99.9% of all cases + // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + } + + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; + + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = 1; + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside + for (j = 2; j < surfaces.length; j++) { + surface = surfaces[j]; + corners = surface.corners; + ctx.beginPath(); + ctx.moveTo(corners[3].screen.x, corners[3].screen.y); + ctx.lineTo(corners[0].screen.x, corners[0].screen.y); + ctx.lineTo(corners[1].screen.x, corners[1].screen.y); + ctx.lineTo(corners[2].screen.x, corners[2].screen.y); + ctx.lineTo(corners[3].screen.x, corners[3].screen.y); + ctx.fill(); + ctx.stroke(); + } + } +}; + + +/** + * Draw a line through all datapoints. + * This function can be used when the style is "line" + */ +links.Graph3d.prototype._redrawDataLine = function() { + var canvas = this.frame.canvas, + ctx = canvas.getContext("2d"), + point, i; + + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? + + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + } + + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; + + ctx.lineWidth = 1; // TODO: make customizable + ctx.strokeStyle = "blue"; // TODO: make customizable + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + } + + // draw the datapoints as colored circles + for (i = 1; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + ctx.lineTo(point.screen.x, point.screen.y); + } + + // finish the line + if (this.dataPoints.length > 0) { + ctx.stroke(); + } +}; + +/** + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) + */ +links.Graph3d.prototype._onMouseDown = function(event) { + event = event || window.event; + + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); + } + + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!this.leftButtonDown && !this.touchDown) return; + + // get mouse position (different code for IE and all other browsers) + this.startMouseX = links.getMouseX(event); + this.startMouseY = links.getMouseY(event); + + this.startStart = new Date(this.start); + this.startEnd = new Date(this.end); + this.startArmRotation = this.camera.getArmRotation(); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + links.addEventListener(document, "mousemove", me.onmousemove); + links.addEventListener(document, "mouseup", me.onmouseup); + links.preventDefault(event); +}; + + +/** + * Perform moving operating. + * This function activated from within the funcion links.Graph.mouseDown(). + * @param {Event} event Well, eehh, the event + */ +links.Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; + + // calculate change in mouse position + var diffX = parseFloat(links.getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(links.getMouseY(event)) - this.startMouseY; + + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; + + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + + // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... + // the -0.001 is to take care that the vertical axis is always drawn at the left front corner + if (Math.abs(Math.sin(horizontalNew)) < snapValue) { + horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001; + } + if (Math.abs(Math.cos(horizontalNew)) < snapValue) { + horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001; + } + + // snap vertically to nice angles + if (Math.abs(Math.sin(verticalNew)) < snapValue) { + verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI; + } + if (Math.abs(Math.cos(verticalNew)) < snapValue) { + verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI; + } + + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); + + // fire an oncamerapositionchange event + var parameters = this.getCameraPosition(); + google.visualization.events.trigger(this, 'camerapositionchange', parameters); + + links.preventDefault(event); +}; + + +/** + * Stop moving operating. + * This function activated from within the funcion links.Graph.mouseDown(). + * @param {event} event The event + */ +links.Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + this.leftButtonDown = false; + + // remove event listeners here + links.removeEventListener(document, "mousemove", this.onmousemove); + links.removeEventListener(document, "mouseup", this.onmouseup); + links.preventDefault(event); +}; + +/** + * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point + * @param {Event} event A mouse move event + */ +links.Graph3d.prototype._onTooltip = function (event) { + var delay = 300; // ms + var mouseX = links.getMouseX(event) - links.getAbsoluteLeft(this.frame); + var mouseY = links.getMouseY(event) - links.getAbsoluteTop(this.frame); + + if (!this.showTooltip) { + return; + } + + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); + } + + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; + } + + if (this.tooltip && this.tooltip.dataPoint) { + // tooltip is currently visible + var dataPoint = this._dataPointFromXY(mouseX, mouseY); + if (dataPoint !== this.tooltip.dataPoint) { + // datapoint changed + if (dataPoint) { + this._showTooltip(dataPoint); + } + else { + this._hideTooltip(); + } + } + } + else { + // tooltip is currently not visible + var me = this; + this.tooltipTimeout = setTimeout(function () { + me.tooltipTimeout = null; + + // show a tooltip if we have a data point + var dataPoint = me._dataPointFromXY(mouseX, mouseY); + if (dataPoint) { + me._showTooltip(dataPoint); + } + }, delay); + } +}; + +/** + * Event handler for touchstart event on mobile devices + */ +links.Graph3d.prototype._onTouchStart = function(event) { + this.touchDown = true; + + var me = this; + this.ontouchmove = function (event) {me._onTouchMove(event);}; + this.ontouchend = function (event) {me._onTouchEnd(event);}; + links.addEventListener(document, "touchmove", me.ontouchmove); + links.addEventListener(document, "touchend", me.ontouchend); + + this._onMouseDown(event); +}; + +/** + * Event handler for touchmove event on mobile devices + */ +links.Graph3d.prototype._onTouchMove = function(event) { + this._onMouseMove(event); +}; + +/** + * Event handler for touchend event on mobile devices + */ +links.Graph3d.prototype._onTouchEnd = function(event) { + this.touchDown = false; + + links.removeEventListener(document, "touchmove", this.ontouchmove); + links.removeEventListener(document, "touchend", this.ontouchend); + + this._onMouseUp(event); +}; + + +/** + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event + */ +links.Graph3d.prototype._onWheel = function(event) { + if (!event) /* For IE. */ + event = window.event; + + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta/120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail/3; + } + + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + var oldLength = this.camera.getArmLength(); + var newLength = oldLength * (1 - delta / 10); + + this.camera.setArmLength(newLength); + this.redraw(); + + this._hideTooltip(); + } + + // fire an oncamerapositionchange event + var parameters = this.getCameraPosition(); + google.visualization.events.trigger(this, 'camerapositionchange', parameters); + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here.. + links.preventDefault(event); +}; + +/** + * Test whether a point lies inside given 2D triangle + * @param {links.Point2d} point + * @param {links.Point2d[]} triangle + * @return {boolean} Returns true if given point lies inside or on the edge of the triangle + * @private + */ +links.Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; + + function sign (x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; + } + + var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)); + var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x)); + var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); + + // each of the three signs must be either equal to each other or zero + return (as == 0 || bs == 0 || as == bs) && + (bs == 0 || cs == 0 || bs == cs) && + (as == 0 || cs == 0 || as == cs); +}; + +/** + * Find a data point close to given screen position (x, y) + * @param {number} x + * @param {number} y + * @return {Object | null} The closest data point or null if not close to any data point + * @private + */ +links.Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new links.Point2d(x, y); + + if (this.style === links.Graph3d.STYLE.BAR || + this.style === links.Graph3d.STYLE.BARCOLOR || + this.style === links.Graph3d.STYLE.BARSIZE) { + // the data points are ordered from far away to closest + for (i = this.dataPoints.length - 1; i >= 0; i--) { + dataPoint = this.dataPoints[i]; + var surfaces = dataPoint.surfaces; + if (surfaces) { + for (var s = surfaces.length - 1; s >= 0; s--) { + // split each surface in two triangles, and see if the center point is inside one of these + var surface = surfaces[s]; + var corners = surface.corners; + var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; + var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; + if (this._insideTriangle(center, triangle1) || + this._insideTriangle(center, triangle2)) { + // return immediately at the first hit + return dataPoint; + } + } + } + } + } + else { + // find the closest data point, using distance to the center of the point on 2d screen + for (i = 0; i < this.dataPoints.length; i++) { + dataPoint = this.dataPoints[i]; + var point = dataPoint.screen; + if (point) { + var distX = Math.abs(x - point.x); + var distY = Math.abs(y - point.y); + var dist = Math.sqrt(distX * distX + distY * distY); + + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } + } + } + } + + + return closestDataPoint; +}; + +/** + * Display a tooltip for given data point + * @param {Object} dataPoint + * @private + */ +links.Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; + + if (!this.tooltip) { + content = document.createElement('div'); + content.style.position = 'absolute'; + content.style.padding = '10px'; + content.style.border = '1px solid #4d4d4d'; + content.style.color = '#1a1a1a'; + content.style.background = 'rgba(255,255,255,0.7)'; + content.style.borderRadius = '2px'; + content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; + + line = document.createElement('div'); + line.style.position = 'absolute'; + line.style.height = '40px'; + line.style.width = '0'; + line.style.borderLeft = '1px solid #4d4d4d'; + + dot = document.createElement('div'); + dot.style.position = 'absolute'; + dot.style.height = '0'; + dot.style.width = '0'; + dot.style.border = '5px solid #4d4d4d'; + dot.style.borderRadius = '5px'; + + this.tooltip = { + dataPoint: null, + dom: { + content: content, + line: line, + dot: dot + } + }; + } + else { + content = this.tooltip.dom.content; + line = this.tooltip.dom.line; + dot = this.tooltip.dom.dot; + } + + this._hideTooltip(); + + this.tooltip.dataPoint = dataPoint; + if (typeof this.showTooltip === 'function') { + content.innerHTML = this.showTooltip(dataPoint.point); + } + else { + content.innerHTML = '' + + '' + + '' + + '' + + '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; + } + + content.style.left = '0'; + content.style.top = '0'; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); + + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; + + var left = dataPoint.screen.x - contentWidth / 2; + left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + + line.style.left = dataPoint.screen.x + 'px'; + line.style.top = (dataPoint.screen.y - lineHeight) + 'px'; + content.style.left = left + 'px'; + content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px'; + dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px'; + dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px'; +}; + +/** + * Hide the tooltip when displayed + * @private + */ +links.Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; + + for (var prop in this.tooltip.dom) { + if (this.tooltip.dom.hasOwnProperty(prop)) { + var elem = this.tooltip.dom[prop]; + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + } + } +}; + +/** + * @prototype Point3d + * @param {Number} x + * @param {Number} y + * @param {Number} z + */ +links.Point3d = function (x, y, z) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + this.z = z !== undefined ? z : 0; +}; + +/** + * Subtract the two provided points, returns a-b + * @param {links.Point3d} a + * @param {links.Point3d} b + * @return {links.Point3d} a-b + */ +links.Point3d.subtract = function(a, b) { + var sub = new links.Point3d(); + sub.x = a.x - b.x; + sub.y = a.y - b.y; + sub.z = a.z - b.z; + return sub; +}; + +/** + * Add the two provided points, returns a+b + * @param {links.Point3d} a + * @param {links.Point3d} b + * @return {links.Point3d} a+b + */ +links.Point3d.add = function(a, b) { + var sum = new links.Point3d(); + sum.x = a.x + b.x; + sum.y = a.y + b.y; + sum.z = a.z + b.z; + return sum; +}; + +/** + * Calculate the average of two 3d points + * @param {links.Point3d} a + * @param {links.Point3d} b + * @return {links.Point3d} The average, (a+b)/2 + */ +links.Point3d.avg = function(a, b) { + return new links.Point3d( + (a.x + b.x) / 2, + (a.y + b.y) / 2, + (a.z + b.z) / 2 + ); +}; + +/** + * Calculate the cross product of the two provided points, returns axb + * Documentation: http://en.wikipedia.org/wiki/Cross_product + * @param {links.Point3d} a + * @param {links.Point3d} b + * @return {links.Point3d} cross product axb + */ +links.Point3d.crossProduct = function(a, b) { + var crossproduct = new links.Point3d(); + + crossproduct.x = a.y * b.z - a.z * b.y; + crossproduct.y = a.z * b.x - a.x * b.z; + crossproduct.z = a.x * b.y - a.y * b.x; + + return crossproduct; +}; + + +/** + * Rtrieve the length of the vector (or the distance from this point to the origin + * @return {Number} length + */ +links.Point3d.prototype.length = function() { + return Math.sqrt( + this.x * this.x + + this.y * this.y + + this.z * this.z + ); +}; + +/** + * @prototype links.Point2d + */ +links.Point2d = function (x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; +}; + + +/** + * @class Filter + * + * @param {google.visualization.DataTable} data The google data table + * @param {number} column The index of the column to be filtered + * @param {links.Graph} graph The graph + */ +links.Filter = function (data, column, graph) { + this.data = data; + this.column = column; + this.graph = graph; // the parent graph + + this.index = undefined; + this.value = undefined; + + // read all distinct values and select the first one + this.values = data.getDistinctValues(this.column); + if (this.values.length) { + this.selectValue(0); + } + + // create an array with the filtered datapoints. this will be loaded afterwards + this.dataPoints = []; + + this.loaded = false; + this.onLoadCallback = undefined; + + if (graph.animationPreload) { + this.loaded = false; + this.loadInBackground(); + } + else { + this.loaded = true; + } +}; + + +/** + * Return the label + * @return {string} label + */ +links.Filter.prototype.isLoaded = function() { + return this.loaded; +}; + + +/** + * Return the loaded progress + * @return {number} percentage between 0 and 100 + */ +links.Filter.prototype.getLoadedProgress = function() { + var len = this.values.length; + + var i = 0; + while (this.dataPoints[i]) { + i++; + } + + return Math.round(i / len * 100); +}; + + +/** + * Return the label + * @return {string} label + */ +links.Filter.prototype.getLabel = function() { + return this.data.getColumnLabel(this.column); +}; + + +/** + * Return the columnIndex of the filter + * @return {number} columnIndex + */ +links.Filter.prototype.getColumn = function() { + return this.column; +}; + +/** + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value + */ +links.Filter.prototype.getSelectedValue = function() { + if (this.index === undefined) + return undefined; + + return this.values[this.index]; +}; + +/** + * Retrieve all values of the filter + * @return {Array} values + */ +links.Filter.prototype.getValues = function() { + return this.values; +}; + +/** + * Retrieve one value of the filter + * @param {number} index + * @return {*} value + */ +links.Filter.prototype.getValue = function(index) { + if (index >= this.values.length) + throw "Error: index out of range"; + + return this.values[index]; +}; + + +/** + * Retrieve the (filtered) dataPoints for the currently selected filter index + * @param {number} index (optional) + * @return {Array} dataPoints + */ +links.Filter.prototype._getDataPoints = function(index) { + if (index === undefined) + index = this.index; + + if (index === undefined) + return []; + + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; + } + else { + var dataView = new google.visualization.DataView(this.data); + + var f = {}; + f.column = this.column; + f.value = this.values[index]; + var filteredRows = this.data.getFilteredRows([f]); + dataView.setRows(filteredRows); + + dataPoints = this.graph._getDataPoints(dataView); + + this.dataPoints[index] = dataPoints; + } + + return dataPoints; +}; + + + +/** + * Set a callback function when the filter is fully loaded. + */ +links.Filter.prototype.setOnLoadCallback = function(callback) { + this.onLoadCallback = callback; +}; + + +/** + * Add a value to the list with available values for this filter + * No double entries will be created. + * @param {number} index + */ +links.Filter.prototype.selectValue = function(index) { + if (index >= this.values.length) + throw "Error: index out of range"; + + this.index = index; + this.value = this.values[index]; +}; + +/** + * Load all filtered rows in the background one by one + * Start this method without providing an index! + */ +links.Filter.prototype.loadInBackground = function(index) { + if (index === undefined) + index = 0; + + var frame = this.graph.frame; + + if (index < this.values.length) { + var dataPointsTemp = this._getDataPoints(index); + //this.graph.redrawInfo(); // TODO: not neat + + // create a progress box + if (frame.progress === undefined) { + frame.progress = document.createElement("DIV"); + frame.progress.style.position = "absolute"; + frame.progress.style.color = "gray"; + frame.appendChild(frame.progress); + } + var progress = this.getLoadedProgress(); + frame.progress.innerHTML = "Loading animation... " + progress + "%"; + // TODO: this is no nice solution... + frame.progress.style.bottom = links.Graph3d.px(60); // TODO: use height of slider + frame.progress.style.left = links.Graph3d.px(10); + + var me = this; + setTimeout(function() {me.loadInBackground(index+1);}, 10); + this.loaded = false; + } + else { + this.loaded = true; + + // remove the progress box + if (frame.progress !== undefined) { + frame.removeChild(frame.progress); + frame.progress = undefined; + } + + if (this.onLoadCallback) + this.onLoadCallback(); + } +}; + + + +/** + * @prototype links.StepNumber + * The class StepNumber is an iterator for numbers. You provide a start and end + * value, and a best step size. StepNumber itself rounds to fixed values and + * a finds the step that best fits the provided step. + * + * If prettyStep is true, the step size is chosen as close as possible to the + * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... + * + * Example usage: + * var step = new links.StepNumber(0, 10, 2.5, true); + * step.start(); + * while (!step.end()) { + * alert(step.getCurrent()); + * step.next(); + * } + * + * Version: 1.0 + * + * @param {number} start The start value + * @param {number} end The end value + * @param {number} step Optional. Step size. Must be a positive value. + * @param {boolean} prettyStep Optional. If true, the step size is rounded + * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + */ +links.StepNumber = function (start, end, step, prettyStep) { + // set default values + this._start = 0; + this._end = 0; + this._step = 1; + this.prettyStep = true; + this.precision = 5; + + this._current = 0; + this.setRange(start, end, step, prettyStep); +}; + +/** + * Set a new range: start, end and step. + * + * @param {number} start The start value + * @param {number} end The end value + * @param {number} step Optional. Step size. Must be a positive value. + * @param {boolean} prettyStep Optional. If true, the step size is rounded + * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + */ +links.StepNumber.prototype.setRange = function(start, end, step, prettyStep) { + this._start = start ? start : 0; + this._end = end ? end : 0; + + this.setStep(step, prettyStep); +}; + +/** + * Set a new step size + * @param {number} step New step size. Must be a positive value + * @param {boolean} prettyStep Optional. If true, the provided step is rounded + * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + */ +links.StepNumber.prototype.setStep = function(step, prettyStep) { + if (step === undefined || step <= 0) + return; + + if (prettyStep !== undefined) + this.prettyStep = prettyStep; + + if (this.prettyStep === true) + this._step = links.StepNumber.calculatePrettyStep(step); + else + this._step = step; +}; + +/** + * Calculate a nice step size, closest to the desired step size. + * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an + * integer number. For example 1, 2, 5, 10, 20, 50, etc... + * @param {number} step Desired step size + * @return {number} Nice step size + */ +links.StepNumber.calculatePrettyStep = function (step) { + var log10 = function (x) {return Math.log(x) / Math.LN10;}; + + // try three steps (multiple of 1, 2, or 5 + var step1 = Math.pow(10, Math.round(log10(step))), + step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), + step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); + + // choose the best step (closest to minimum step) + var prettyStep = step1; + if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; + if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; + + // for safety + if (prettyStep <= 0) { + prettyStep = 1; + } + + return prettyStep; +}; + +/** + * returns the current value of the step + * @return {number} current value + */ +links.StepNumber.prototype.getCurrent = function () { + return parseFloat(this._current.toPrecision(this.precision)); +}; + +/** + * returns the current step size + * @return {number} current step size + */ +links.StepNumber.prototype.getStep = function () { + return this._step; +}; + +/** + * Set the current value to the largest value smaller than start, which + * is a multiple of the step size + */ +links.StepNumber.prototype.start = function() { + this._current = this._start - this._start % this._step; +}; + +/** + * Do a step, add the step size to the current value + */ +links.StepNumber.prototype.next = function () { + this._current += this._step; +}; + +/** + * Returns true whether the end is reached + * @return {boolean} True if the current value has passed the end value. + */ +links.StepNumber.prototype.end = function () { + return (this._current > this._end); +}; + + +/** + * @constructor links.Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + * @param {Object} options Available options: + * {boolean} visible If true (default) the + * slider is visible. + */ +links.Slider = function(container, options) { + if (container === undefined) { + throw "Error: No container element defined"; + } + this.container = container; + this.visible = (options && options.visible != undefined) ? options.visible : true; + + if (this.visible) { + this.frame = document.createElement("DIV"); + //this.frame.style.backgroundColor = "#E5E5E5"; + this.frame.style.width = "100%"; + this.frame.style.position = "relative"; + this.container.appendChild(this.frame); + + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); + + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); + + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); + + this.frame.bar = document.createElement("INPUT"); + this.frame.bar.type = "BUTTON"; + this.frame.bar.style.position = "absolute"; + this.frame.bar.style.border = "1px solid red"; + this.frame.bar.style.width = "100px"; + this.frame.bar.style.height = "6px"; + this.frame.bar.style.borderRadius = "2px"; + this.frame.bar.style.MozBorderRadius = "2px"; + this.frame.bar.style.border = "1px solid #7F7F7F"; + this.frame.bar.style.backgroundColor = "#E5E5E5"; + this.frame.appendChild(this.frame.bar); + + this.frame.slide = document.createElement("INPUT"); + this.frame.slide.type = "BUTTON"; + this.frame.slide.style.margin = "0px"; + this.frame.slide.value = " "; + this.frame.slide.style.position = "relative"; + this.frame.slide.style.left = "-100px"; + this.frame.appendChild(this.frame.slide); + + // create events + var me = this; + this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; + this.frame.prev.onclick = function (event) {me.prev(event);}; + this.frame.play.onclick = function (event) {me.togglePlay(event);}; + this.frame.next.onclick = function (event) {me.next(event);}; + } + + this.onChangeCallback = undefined; + + this.values = []; + this.index = undefined; + + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; +}; + +/** + * Select the previous index + */ +links.Slider.prototype.prev = function() { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); + } +}; + +/** + * Select the next index + */ +links.Slider.prototype.next = function() { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } +}; + +/** + * Select the next index + */ +links.Slider.prototype.playNext = function() { + var start = new Date(); + + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } + else if (this.playLoop) { + // jump to the start + index = 0; + this.setIndex(index); + } + + var end = new Date(); + var diff = (end - start); + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(this.playInterval - diff, 0); + // document.title = diff // TODO: cleanup + + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); +}; + +/** + * Toggle start or stop playing + */ +links.Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } +}; + +/** + * Start playing + */ +links.Slider.prototype.play = function() { + this.playNext(); + + if (this.frame) { + this.frame.play.value = "Stop"; + } +}; + +/** + * Stop playing + */ +links.Slider.prototype.stop = function() { + clearInterval(this.playTimeout); + this.playTimeout = undefined; + + if (this.frame) { + this.frame.play.value = "Play"; + } +}; + +/** + * Set a callback function which will be triggered when the value of the + * slider bar has changed. + */ +links.Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; +}; + +/** + * Set the interval for playing the list + * @param {number} interval The interval in milliseconds + */ +links.Slider.prototype.setPlayInterval = function(interval) { + this.playInterval = interval; +}; + +/** + * Retrieve the current play interval + * @return {number} interval The interval in milliseconds + */ +links.Slider.prototype.getPlayInterval = function(interval) { + return this.playInterval; +}; + +/** + * Set looping on or off + * @pararm {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +links.Slider.prototype.setPlayLoop = function(doLoop) { + this.playLoop = doLoop; +}; + + +/** + * Execute the onchange callback function + */ +links.Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } +}; + +/** + * redraw the slider on the correct place + */ +links.Slider.prototype.redraw = function() { + if (this.frame) { + // resize the bar + this.frame.bar.style.top = (this.frame.clientHeight/2 - + this.frame.bar.offsetHeight/2) + "px"; + this.frame.bar.style.width = (this.frame.clientWidth - + this.frame.prev.clientWidth - + this.frame.play.clientWidth - + this.frame.next.clientWidth - 30) + "px"; + + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = (left) + "px"; + } +}; + + +/** + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) + */ +links.Slider.prototype.setValues = function(values) { + this.values = values; + + if (this.values.length > 0) + this.setIndex(0); + else + this.index = undefined; +}; + +/** + * Select a value by its index + * @param {number} index + */ +links.Slider.prototype.setIndex = function(index) { + if (index < this.values.length) { + this.index = index; + + this.redraw(); + this.onChange(); + } + else { + throw "Error: index out of range"; + } +}; + +/** + * retrieve the index of the currently selected vaue + * @return {number} index + */ +links.Slider.prototype.getIndex = function() { + return this.index; +}; + + +/** + * retrieve the currently selected value + * @return {*} value + */ +links.Slider.prototype.get = function() { + return this.values[this.index]; +}; + + +links.Slider.prototype._onMouseDown = function(event) { + // only react on left mouse button down + var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!leftButtonDown) return; + + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + links.addEventListener(document, "mousemove", this.onmousemove); + links.addEventListener(document, "mouseup", this.onmouseup); + links.preventDefault(event); +}; + + +links.Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + var x = left - 3; + + var index = Math.round(x / width * (this.values.length-1)); + if (index < 0) index = 0; + if (index > this.values.length-1) index = this.values.length-1; + + return index; +}; + +links.Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x = index / (this.values.length-1) * width; + var left = x + 3; + + return left; +}; + + + +links.Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var index = this.leftToIndex(x); + + this.setIndex(index); + + links.preventDefault(); +}; + + +links.Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + // remove event listeners + links.removeEventListener(document, "mousemove", this.onmousemove); + links.removeEventListener(document, "mouseup", this.onmouseup); + + links.preventDefault(); +}; + + + +/**--------------------------------------------------------------------------**/ + + + +/** + * Add and event listener. Works for all browsers + * @param {Element} element An html element + * @param {string} action The action, for example "click", + * without the prefix "on" + * @param {function} listener The callback function to be executed + * @param {boolean} useCapture + */ +links.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent("on" + action, listener); // IE browsers + } +}; + +/** + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example "mousedown" + * @param {function} listener The listener function + * @param {boolean} useCapture + */ +links.removeEventListener = function(element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } +}; + +/** + * Stop event propagation + */ +links.stopPropagation = function (event) { + if (!event) + event = window.event; + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } +}; + + +/** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ +links.preventDefault = function (event) { + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } +}; + +/** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} left The absolute left position of this element + * in the browser page. + */ +links.getAbsoluteLeft = function(elem) { + var left = 0; + while( elem !== null ) { + left += elem.offsetLeft; + left -= elem.scrollLeft; + elem = elem.offsetParent; + } + return left; +}; + +/** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} top The absolute top position of this element + * in the browser page. + */ +links.getAbsoluteTop = function(elem) { + var top = 0; + while( elem !== null ) { + top += elem.offsetTop; + top -= elem.scrollTop; + elem = elem.offsetParent; + } + return top; +}; + +/** + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {number} mouse x + */ +links.getMouseX = function(event) { + if ('clientX' in event) return event.clientX; + return event.targetTouches[0] && event.targetTouches[0].clientX || 0; +}; + +/** + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {number} mouse y + */ +links.getMouseY = function(event) { + if ('clientY' in event) return event.clientY; + return event.targetTouches[0] && event.targetTouches[0].clientY || 0; +}; + diff --git a/src/3dgraph/playground/csv2array.js b/src/3dgraph/playground/csv2array.js new file mode 100644 index 00000000..95d0c4a6 --- /dev/null +++ b/src/3dgraph/playground/csv2array.js @@ -0,0 +1,120 @@ +/** + * Convert data in CSV (comma separated value) format to a javascript array. + * + * Values are separated by a comma, or by a custom one character delimeter. + * Rows are separated by a new-line character. + * + * Leading and trailing spaces and tabs are ignored. + * Values may optionally be enclosed by double quotes. + * Values containing a special character (comma's, double-quotes, or new-lines) + * must be enclosed by double-quotes. + * Embedded double-quotes must be represented by a pair of consecutive + * double-quotes. + * + * Example usage: + * var csv = '"x", "y", "z"\n12.3, 2.3, 8.7\n4.5, 1.2, -5.6\n'; + * var array = csv2array(csv); + * + * Author: Jos de Jong, 2010 + * + * @param {string} data The data in CSV format. + * @param {string} delimeter [optional] a custom delimeter. Comma ',' by default + * The Delimeter must be a single character. + * @return {Array} array A two dimensional array containing the data + * @throw {String} error The method throws an error when there is an + * error in the provided data. + */ +function csv2array(data, delimeter) { + // Retrieve the delimeter + if (delimeter == undefined) + delimeter = ','; + if (delimeter && delimeter.length > 1) + delimeter = ','; + + // initialize variables + var newline = '\n'; + var eof = ''; + var i = 0; + var c = data.charAt(i); + var row = 0; + var col = 0; + var array = new Array(); + + while (c != eof) { + // skip whitespaces + while (c == ' ' || c == '\t' || c == '\r') { + c = data.charAt(++i); // read next char + } + + // get value + var value = ""; + if (c == '\"') { + // value enclosed by double-quotes + c = data.charAt(++i); + + do { + if (c != '\"') { + // read a regular character and go to the next character + value += c; + c = data.charAt(++i); + } + + if (c == '\"') { + // check for escaped double-quote + var cnext = data.charAt(i+1); + if (cnext == '\"') { + // this is an escaped double-quote. + // Add a double-quote to the value, and move two characters ahead. + value += '\"'; + i += 2; + c = data.charAt(i); + } + } + } + while (c != eof && c != '\"'); + + if (c == eof) { + throw "Unexpected end of data, double-quote expected"; + } + + c = data.charAt(++i); + } + else { + // value without quotes + while (c != eof && c != delimeter && c!= newline && c != ' ' && c != '\t' && c != '\r') { + value += c; + c = data.charAt(++i); + } + } + + // add the value to the array + if (array.length <= row) + array.push(new Array()); + array[row].push(value); + + // skip whitespaces + while (c == ' ' || c == '\t' || c == '\r') { + c = data.charAt(++i); + } + + // go to the next row or column + if (c == delimeter) { + // to the next column + col++; + } + else if (c == newline) { + // to the next row + col = 0; + row++; + } + else if (c != eof) { + // unexpected character + throw "Delimiter expected after character " + i; + } + + // go to the next character + c = data.charAt(++i); + } + + return array; +} diff --git a/src/3dgraph/playground/csv2datatable.html b/src/3dgraph/playground/csv2datatable.html new file mode 100644 index 00000000..35dc9bd0 --- /dev/null +++ b/src/3dgraph/playground/csv2datatable.html @@ -0,0 +1,80 @@ + + + + Convert CSV to Google Datatable + + + + + + + + + + +
+ +
+ +CSV
+ +
+
+ +
+
+ +Google DataTable
+ + + + diff --git a/src/3dgraph/playground/datasource.html b/src/3dgraph/playground/datasource.html new file mode 100644 index 00000000..efb47e1c --- /dev/null +++ b/src/3dgraph/playground/datasource.html @@ -0,0 +1,173 @@ + + + + Graph3d documentation + + + + + + + + +
+<?php
+
+/*
+This datasource returns a response in the form of a google query response
+
+USAGE
+All parameters are optional
+datasource.php?xmin=0&xmax=314&xstepnum=25&ymin=0&ymax=314&ystepnum=25
+
+DOCUMENTATION
+http://code.google.com/apis/visualization/documentation/dev/implementing_data_source.html
+
+
+EXAMPLE OF A RESPONSE FILE
+
+Note that the reqId in the response must correspond with the reqId from the
+request.
+________________________________________________________________________________
+
+google.visualization.Query.setResponse({
+  version:'0.6',
+  reqId:'0',
+  status:'ok',
+  table:{
+    cols:[
+      {id:'x',
+       label:'x',
+       type:'number'},
+      {id:'y',
+       label:'y',
+       type:'number'},
+      {id:'value',
+       label:'value',
+       type:'number'}
+    ],
+    rows:[
+      {c:[{v:0}, {v:0}, {v:10.0}]},
+      {c:[{v:1}, {v:0}, {v:12.0}]},
+      {c:[{v:2}, {v:0}, {v:13.0}]},
+      {c:[{v:0}, {v:1}, {v:11.0}]},
+      {c:[{v:1}, {v:1}, {v:14.0}]},
+      {c:[{v:2}, {v:1}, {v:11.0}]}
+    ]
+  }
+});
+________________________________________________________________________________
+
+*/
+
+
+/**
+ * A custom function
+ */
+function custom($x, $y) {
+  $d = sqrt(pow($x/100, 2) + pow($y/100, 2));
+
+  return 50 * exp(-5 * $d / 10) * sin($d*5)
+}
+
+
+
+
+// retrieve parameters
+$default_stepnum = 25;
+
+$xmin     = isset($_REQUEST['xmin'])     ? (float)$_REQUEST['xmin']   : -100;
+$xmax     = isset($_REQUEST['xmax'])     ? (float)$_REQUEST['xmax']   : 100;
+$xstepnum = isset($_REQUEST['xstepnum']) ? (int)$_REQUEST['xstepnum'] : $default_stepnum;
+
+$ymin     = isset($_REQUEST['ymin'])     ? (float)$_REQUEST['ymin']   : -100;
+$ymax     = isset($_REQUEST['ymax'])     ? (float)$_REQUEST['ymax']   : 100;
+$ystepnum = isset($_REQUEST['ystepnum']) ? (int)$_REQUEST['ystepnum'] : $default_stepnum;
+
+// in the reply we must fill in the request id that came with the request
+$reqId = getReqId();
+
+// check for a maximum number of datapoints (for safety)
+if ($xstepnum * $ystepnum > 10000) {
+  echo "google.visualization.Query.setResponse({
+    version:'0.6',
+    reqId:'$reqId',
+    status:'error',
+    errors:[{reason:'not_supported', message:'Maximum number of datapoints exceeded'}]
+  });";
+
+  exit;
+}
+
+
+// output the header part of the response
+echo "google.visualization.Query.setResponse({
+  version:'0.6',
+  reqId:'$reqId',
+  status:'ok',
+  table:{
+    cols:[
+      {id:'x',
+       label:'x',
+       type:'number'},
+      {id:'y',
+       label:'y',
+       type:'number'},
+      {id:'value',
+       label:'',
+       type:'number'}
+    ],
+    rows:[";
+
+// output the actual values
+$first = true;
+$xstep = ($xmax - $xmin) / $xstepnum;
+$ystep = ($ymax - $ymin) / $ystepnum;
+for ($x = $xmin; $x < $xmax; $x+=$xstep) {
+  for ($y = $ymin; $y < $ymax; $y+=$ystep) {
+    $value = custom($x,$y);
+
+    if (!$first) {
+      echo ",\n";
+    }
+    else {
+      echo "\n";
+    }
+    echo "      {c:[{v:$x}, {v:$y}, {v:$value}]}";
+
+    $first = false;
+  }
+}
+
+
+// output the end part of the response
+echo "
+    ]
+  }
+});
+";
+
+
+/**
+ * Retrieve the request id from the get/post data
+ * @return {number} $reqId       The request id, or 0 if not found
+ */
+function getReqId() {
+  $reqId = 0;
+
+  foreach ($_REQUEST as $req) {
+    if (substr($req, 0,6) == "reqId:") {
+      $reqId = substr($req, 6);
+    }
+  }
+
+  return $reqId;
+}
+
+
+?>
+
+
+ + + diff --git a/src/3dgraph/playground/datasource.php b/src/3dgraph/playground/datasource.php new file mode 100644 index 00000000..9c265cb9 --- /dev/null +++ b/src/3dgraph/playground/datasource.php @@ -0,0 +1,155 @@ + 10000) { + echo "google.visualization.Query.setResponse({ + version:'0.6', + reqId:'$reqId', + status:'error', + errors:[{reason:'not_supported', message:'Maximum number of datapoints exceeded'}] + });"; + + exit; +} + + +// output the header part of the response +echo "google.visualization.Query.setResponse({ + version:'0.6', + reqId:'$reqId', + status:'ok', + table:{ + cols:[ + {id:'x', + label:'x', + type:'number'}, + {id:'y', + label:'y', + type:'number'}, + {id:'value', + label:'', + type:'number'} + ], + rows:["; + +// output the actual values +$first = true; +$xstep = ($xmax - $xmin) / $xstepnum; +$ystep = ($ymax - $ymin) / $ystepnum; +for ($x = $xmin; $x < $xmax; $x+=$xstep) { + for ($y = $ymin; $y < $ymax; $y+=$ystep) { + $value = custom($x,$y); + + if (!$first) { + echo ",\n"; + } + else { + echo "\n"; + } + echo " {c:[{v:$x}, {v:$y}, {v:$value}]}"; + + $first = false; + } +} + + +// output the end part of the response +echo " + ] + } +}); +"; + + +/** + * Retrieve the request id from the get/post data + * @return {number} $reqId The request id, or 0 if not found + */ +function getReqId() { + $reqId = 0; + + foreach ($_REQUEST as $req) { + if (substr($req, 0,6) == "reqId:") { + $reqId = substr($req, 6); + } + } + + return $reqId; +} + + +?> diff --git a/src/3dgraph/playground/index.html b/src/3dgraph/playground/index.html new file mode 100644 index 00000000..d3df6e52 --- /dev/null +++ b/src/3dgraph/playground/index.html @@ -0,0 +1,247 @@ + + + + + Graph 3D - Playground + + + + + + + + + + + + + + +

Graph 3D - Playground

+ + +++ + + + + + + + +
+

Data

+

+ Graph 3D expects a data table with first three to five columns: + colums x, y, z (optional), + value, filtervalue (optional). +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Csv + +
+

+ The csv table must contain a header line with column names. +

+ +

+ Simple example + Line example + Animation example + Moving dots example + Colored dots example + Sized dots example +

+
+
+ Json + +
+

+ +

+ +

+ Simple example +

+
+
+ Javascript + +
+

+ The javascript source must create a global variable named data + which contains a google visualization DataTable. +

+ +

+ Simple example + Function example +

+
+
+ Google Spreadsheet + + +
+ Datasource + +
+ +

+ Example + (view source code) +

+
+
+ +
+
+

Graph

+

+ +

+ +
+
+

Options

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionValue
width for example "500px" or "100%"
height for example "500px" or "100%"
style + +
showAnimationControls
showGrid
showPerspective
showShadow
keepAspectRatio
verticalRatio a value between 0.1 and 1.0
animationInterval in milliseconds
animationPreload
animationAutoStart
xCenter
yCenter
xMin
xMax
xStep
yMin
yMax
yStep
zMin
zMax
zStep
valueMin
valueMax
xBarWidth
yBarWidth
+ +
+ + diff --git a/src/3dgraph/playground/playground.css b/src/3dgraph/playground/playground.css new file mode 100644 index 00000000..c18ba24a --- /dev/null +++ b/src/3dgraph/playground/playground.css @@ -0,0 +1,91 @@ +body +{ + font: 13px "Lucida Grande", Tahoma, Arial, Helvetica, sans-serif; +} + +h1 +{ + font-size: 180%; + font-weight: bold; + + margin: 1em 0 1em 0; +} + +h2 +{ + font-size: 140%; + color: white; + background-color: #7F8FB1; + padding: 5px; +} + +h3 +{ + font-size: 100%; +} + +hr +{ + border: none 0; + border-top: 1px solid #7F8FB1; + height: 1px; +} + +pre.code +{ + display: block; + padding: 8px; + border: 1px dashed #ccc; +} + +table +{ + border-collapse: collapse; +} + +th, td +{ + font: 12px "Lucida Grande", Tahoma, Arial, Helvetica, sans-serif; + text-align: left; + vertical-align: top; + /*border: 1px solid #888;*/ + padding: 3px; +} + +th +{ + font-weight: bold; +} + + +textarea { + width: 500px; + height: 200px; + border: 1px solid #888; +} + +input[type=text] { + border: 1px solid #888; +} + +#datasourceText, #googlespreadsheetText { + width: 500px; + +} + +.info { + color: gray; +} + +a { + color: gray; +} +a:hover { + color: red; +} + + +#graph { + width: 100%; + height: 600px; +} diff --git a/src/3dgraph/playground/playground.js b/src/3dgraph/playground/playground.js new file mode 100644 index 00000000..1f8c0b93 --- /dev/null +++ b/src/3dgraph/playground/playground.js @@ -0,0 +1,657 @@ + +var query = null; + + +function load() { + selectDataType(); + + loadCsvExample(); + loadJsonExample(); + loadJavascriptExample(); + loadGooglespreadsheetExample(); + loadDatasourceExample(); + + draw(); +} + + + +/** + * Upate the UI based on the currently selected datatype + */ +function selectDataType() { + var datatype = getDataType(); + + document.getElementById("csv").style.overflow = "hidden"; + document.getElementById("json").style.overflow = "hidden"; + document.getElementById("javascript").style.overflow = "hidden"; + document.getElementById("googlespreadsheet").style.overflow = "hidden"; + document.getElementById("datasource").style.overflow = "hidden"; + + document.getElementById("csv").style.visibility = (datatype == "csv") ? "" : "hidden"; + document.getElementById("json").style.visibility = (datatype == "json") ? "" : "hidden"; + document.getElementById("javascript").style.visibility = (datatype == "javascript") ? "" : "hidden"; + document.getElementById("googlespreadsheet").style.visibility = (datatype == "googlespreadsheet") ? "" : "hidden"; + document.getElementById("datasource").style.visibility = (datatype == "datasource") ? "" : "hidden"; + + document.getElementById("csv").style.height = (datatype == "csv") ? "auto" : "0px"; + document.getElementById("json").style.height = (datatype == "json") ? "auto" : "0px"; + document.getElementById("javascript").style.height = (datatype == "javascript") ? "auto" : "0px"; + document.getElementById("googlespreadsheet").style.height = (datatype == "googlespreadsheet") ? "auto" : "0px"; + document.getElementById("datasource").style.height = (datatype == "datasource") ? "auto" : "0px"; +} + + +function round(value, decimals) { + return parseFloat(value.toFixed(decimals)); +} + +function loadCsvExample() { + var csv = ""; + + // headers + csv += '"x", "y", "value"\n'; + + // create some nice looking data with sin/cos + var steps = 30; + var axisMax = 314; + var axisStep = axisMax / steps; + for (var x = 0; x < axisMax; x+=axisStep) { + for (var y = 0; y < axisMax; y+=axisStep) { + var value = Math.sin(x/50) * Math.cos(y/50) * 50 + 50; + + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(value, 2) + '\n'; + } + } + + document.getElementById("csvTextarea").innerHTML = csv; + + // also adjust some settings + document.getElementById("style").value = "surface"; + document.getElementById("verticalRatio").value = "0.5"; +} + + +function loadCsvAnimationExample() { + var csv = ""; + + // headers + csv += '"x", "y", "value", "time"\n'; + + // create some nice looking data with sin/cos + var steps = 20; + var axisMax = 314; + var tMax = 31; + var axisStep = axisMax / steps; + for (var t = 0; t < tMax; t++) { + for (var x = 0; x < axisMax; x+=axisStep) { + for (var y = 0; y < axisMax; y+=axisStep) { + var value = Math.sin(x/50 + t/10) * Math.cos(y/50 + t/10) * 50 + 50; + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(value, 2) + ', ' + t + '\n'; + } + } + } + + document.getElementById("csvTextarea").innerHTML = csv; + + // also adjust some settings + document.getElementById("style").value = "surface"; + document.getElementById("verticalRatio").value = "0.5"; + document.getElementById("animationInterval").value = 100; + +} + + +function loadCsvLineExample() { + var csv = ""; + + // headers + csv += '"sin(t)", "cos(t)", "t"\n'; + + // create some nice looking data with sin/cos + var steps = 100; + var axisMax = 314; + var tmax = 4 * 2 * Math.PI; + var axisStep = axisMax / steps; + for (t = 0; t < tmax; t += tmax / steps) { + var r = 1; + var x = r * Math.sin(t); + var y = r * Math.cos(t); + var z = t; + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(z, 2) + '\n'; + } + + document.getElementById("csvTextarea").innerHTML = csv; + + // also adjust some settings + document.getElementById("style").value = "line"; + document.getElementById("verticalRatio").value = "1.0"; + document.getElementById("showPerspective").checked = false; +} + +function loadCsvMovingDotsExample() { + var csv = ""; + + // headers + csv += '"x", "y", "z", "color value", "time"\n'; + + // create some shortcuts to math functions + var sin = Math.sin; + var cos = Math.cos; + var pi = Math.PI; + + // create the animation data + var tmax = 2.0 * pi; + var tstep = tmax / 75; + var dotCount = 1; // set this to 1, 2, 3, 4, ... + for (var t = 0; t < tmax; t += tstep) { + var tgroup = parseFloat(t.toFixed(2)); + var value = t; + + // a dot in the center + var x = 0; + var y = 0; + var z = 0; + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(z, 2) + ', ' + round(value, 2)+ ', ' + round(tgroup, 2) + '\n'; + + // one or multiple dots moving around the center + for (var dot = 0; dot < dotCount; dot++) { + var tdot = t + 2*pi * dot / dotCount; + //data.addRow([sin(tdot), cos(tdot), sin(tdot), value, tgroup]); + //data.addRow([sin(tdot), -cos(tdot), sin(tdot + tmax*1/2), value, tgroup]); + + var x = sin(tdot); + var y = cos(tdot); + var z = sin(tdot); + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(z, 2) + ', ' + round(value, 2)+ ', ' + round(tgroup, 2) + '\n'; + + var x = sin(tdot); + var y = -cos(tdot); + var z = sin(tdot + tmax*1/2); + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(z, 2) + ', ' + round(value, 2)+ ', ' + round(tgroup, 2) + '\n'; + + } + } + + document.getElementById("csvTextarea").innerHTML = csv; + + // also adjust some settings + document.getElementById("style").value = "dot-color"; + document.getElementById("verticalRatio").value = "1.0"; + document.getElementById("animationInterval").value = "35"; + document.getElementById("animationAutoStart").checked = true; + document.getElementById("showPerspective").checked = true; +} + +function loadCsvColoredDotsExample() { + var csv = ""; + + // headers + csv += '"x", "y", "z", "distance"\n'; + + // create some shortcuts to math functions + var sqrt = Math.sqrt; + var pow = Math.pow; + var random = Math.random; + + // create the animation data + var imax = 200; + for (var i = 0; i < imax; i++) { + var x = pow(random(), 2); + var y = pow(random(), 2); + var z = pow(random(), 2); + var dist = sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); + + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(z, 2) + ', ' + round(dist, 2)+ '\n'; + } + + document.getElementById("csvTextarea").innerHTML = csv; + + // also adjust some settings + document.getElementById("style").value = "dot-color"; + document.getElementById("verticalRatio").value = "1.0"; + document.getElementById("showPerspective").checked = true; +} + +function loadCsvSizedDotsExample() { + var csv = ""; + + // headers + csv += '"x", "y", "z", "range"\n'; + + // create some shortcuts to math functions + var sqrt = Math.sqrt; + var pow = Math.pow; + var random = Math.random; + + // create the animation data + var imax = 200; + for (var i = 0; i < imax; i++) { + var x = pow(random(), 2); + var y = pow(random(), 2); + var z = pow(random(), 2); + var dist = sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); + var range = sqrt(2) - dist; + + csv += round(x, 2) + ', ' + round(y, 2) + ', ' + round(z, 2) + ', ' + round(range, 2)+ '\n'; + } + + document.getElementById("csvTextarea").innerHTML = csv; + + // also adjust some settings + document.getElementById("style").value = "dot-size"; + document.getElementById("verticalRatio").value = "1.0"; + document.getElementById("showPerspective").checked = true; +} + + +function loadJsonExample() { + var json = ""; + // TODO: get json working + + // headers + json += + '{\n' + + ' "cols":[\n' + + ' {"id":"x",\n' + + ' "label":"x",\n' + + ' "type":"number"},\n' + + ' {"id":"y",\n' + + ' "label":"y",\n' + + ' "type":"number"},\n' + + ' {"id":"value",\n' + + ' "label":"value",\n' + + ' "type":"number"}\n' + + ' ],\n' + + ' "rows":['; + + // create some nice looking data with sin/cos + var steps = 20; + var axisMax = 314; + var first = true; + var axisStep = axisMax / steps; + for (var x = 0; x < axisMax; x+=axisStep) { + for (var y = 0; y < axisMax; y+=axisStep) { + var value = Math.sin(x/50) * Math.cos(y/50) * 50 + 50; + if (first) { + json += '\n'; + first = false; + } + else { + json += ',\n'; + } + + json += ' {"c":[{"v":' + round(x, 2) + '}, {"v":' + round(y, 2) + '}, {"v":' + round(value, 2) + '}]}'; + } + } + + // end of the table + json += + '\n' + + ' ]\n' + + '}\n'; + + document.getElementById("jsonTextarea").innerHTML = json; + + document.getElementById("verticalRatio").value = "0.5"; +} + + +function loadJavascriptExample() { + var js = + 'data = new google.visualization.DataTable();\n' + + 'data.addColumn("number", "x");\n' + + 'data.addColumn("number", "y");\n' + + 'data.addColumn("number", "value");\n' + + '\n'; + + js += '// insert data\n'; + + var axisStep = 7; + for (var x = -100; x < 100; x += axisStep) { + for (var y = -100; y < 300; y += axisStep) { + //var value = Math.sin(x/50) * Math.cos(y/50) * 50 + 50; + + var d = Math.sqrt(Math.pow(x/100, 2) + Math.pow(y/100, 2)); + var value = 50 * Math.exp(-5 * d / 10) * Math.sin(d*5) + + js += 'data.addRow([' + round(x, 2) + ', ' + round(y,2) + ', ' + round(value, 2) + ']);\n'; + } + } + + document.getElementById("javascriptTextarea").innerHTML = js; + + document.getElementById("verticalRatio").value = "0.5"; +} + +function loadJavascriptFunctionExample() { + var js = + 'data = new google.visualization.DataTable();\n' + + 'data.addColumn("number", "x");\n' + + 'data.addColumn("number", "y");\n' + + 'data.addColumn("number", "value");\n' + + '\n' + + '// create some nice looking data with sin/cos\n' + + 'var steps = 50;\n' + + 'var axisMax = 314;\n' + + 'axisStep = axisMax / steps;\n' + + 'for (var x = 0; x < axisMax; x+=axisStep) {\n' + + ' for (var y = 0; y < axisMax; y+=axisStep) {\n' + + ' var value = Math.sin(x/50) * Math.cos(y/50) * 50 + 50;\n' + + ' data.addRow([x, y, value]);\n' + + ' }\n' + + '}'; + + document.getElementById("javascriptTextarea").innerHTML = js; + + document.getElementById("verticalRatio").value = "0.5"; +} + +function loadGooglespreadsheetExample() { + var url = + "https://spreadsheets.google.com/a/almende.org/ccc?key=tJ6gaeq2Ldy82VVMr5dPQoA&hl=en#gid=0"; + + document.getElementById("googlespreadsheetText").value = url; + + document.getElementById("verticalRatio").value = "0.5"; +} + + +function loadDatasourceExample() { + var url = "datasource.php"; + + document.getElementById("datasourceText").value = url; + + document.getElementById("verticalRatio").value = "0.5"; +} + + + +/** + * Retrieve teh currently selected datatype + * @return {string} datatype + */ +function getDataType() { + if (document.getElementById("datatypeCsv").checked) return "csv"; + if (document.getElementById("datatypeJson").checked) return "json"; + if (document.getElementById("datatypeJavascript").checked) return "javascript"; + if (document.getElementById("datatypeDatasource").checked) return "datasource"; + if (document.getElementById("datatypeGooglespreadsheet").checked) return "googlespreadsheet"; +} + + +/** + * Retrieve the datatable from the entered contents of the csv text + * @return {Google DataTable} + */ +function getDataCsv() { + var csv = document.getElementById("csvTextarea").value; + + // parse the csv content + var csvArray = csv2array(csv); + + // the first line of the csv file contains the column names + var data = new google.visualization.DataTable(); + var columnTypes = []; + var row = 0; + for (var col = 0; col < csvArray[row].length; col++) { + var label = csvArray[row][col]; + var columnType = "number"; + + if (col >= 4) { + if (csvArray.length > 1) { + var value = csvArray[1][3]; + if (value) { + columnType = typeof(value); + } + } + else { + columnType = "string"; + } + } + columnTypes[col] = columnType; + + data.addColumn(columnType, label); + } + + // read all data + var colCount = data.getNumberOfColumns(); + for (var row = 1; row < csvArray.length; row++) { + var rowData = csvArray[row]; + if (rowData.length == colCount) { + data.addRow(); + + for (var col = 0; col < csvArray[row].length; col++) { + if (columnTypes[col] == 'number') { + var value = parseFloat(csvArray[row][col]); + } + else { + var value = trim(csvArray[row][col]); + } + //alert(value) + data.setValue(row-1, col, value); + } + } + } + + return data; +} + +/** + * remove leading and trailing spaces + */ +function trim(text) { + while (text.length && text.charAt(0) == ' ') + text = text.substr(1); + + while (text.length && text.charAt(text.length-1) == ' ') + text = text.substr(0, text.length-1); + + return text; +} + +/** + * Retrieve the datatable from the entered contents of the javascript text + * @return {Google DataTable} + */ +function getDataJson() { + var json = document.getElementById("jsonTextarea").value; + var data = new google.visualization.DataTable(json); + + return data; +} + + +/** + * Retrieve the datatable from the entered contents of the javascript text + * @return {Google DataTable} + */ +function getDataJavascript() { + var js = document.getElementById("javascriptTextarea").value; + + eval(js); + + return data; +} + + +/** + * Retrieve the datatable from the entered contents of the datasource text + * @return {Google DataTable} + */ +function getDataDatasource() { + // TODO + + throw "Sorry, datasource is not yet implemented..."; +} + +/** + * Retrieve a JSON object with all options + */ +function getOptions() { + return { + width: document.getElementById("width").value, + height: document.getElementById("height").value, + style: document.getElementById("style").value, + showAnimationControls: (document.getElementById("showAnimationControls").checked != false), + showGrid: (document.getElementById("showGrid").checked != false), + showPerspective: (document.getElementById("showPerspective").checked != false), + showShadow: (document.getElementById("showShadow").checked != false), + keepAspectRatio: (document.getElementById("keepAspectRatio").checked != false), + verticalRatio: document.getElementById("verticalRatio").value, + animationInterval: document.getElementById("animationInterval").value, + animationPreload: (document.getElementById("animationPreload").checked != false), + animationAutoStart:(document.getElementById("animationAutoStart").checked != false), + + xCenter: Number(document.getElementById("xCenter").value) || undefined, + yCenter: Number(document.getElementById("yCenter").value) || undefined, + + xMin: Number(document.getElementById("xMin").value) || undefined, + xMax: Number(document.getElementById("xMax").value) || undefined, + xStep: Number(document.getElementById("xStep").value) || undefined, + yMin: Number(document.getElementById("yMin").value) || undefined, + yMax: Number(document.getElementById("yMax").value) || undefined, + yStep: Number(document.getElementById("yStep").value) || undefined, + zMin: Number(document.getElementById("zMin").value) || undefined, + zMax: Number(document.getElementById("zMax").value) || undefined, + zStep: Number(document.getElementById("zStep").value) || undefined, + + valueMin: Number(document.getElementById("valueMin").value) || undefined, + valueMax: Number(document.getElementById("valueMax").value) || undefined, + + xBarWidth: Number(document.getElementById("xBarWidth").value) || undefined, + yBarWidth: Number(document.getElementById("yBarWidth").value) || undefined + }; +} + +/** + * Redraw the graph with the entered data and options + */ +function draw() { + try { + var datatype = getDataType(); + + switch (datatype) { + case "csv": return drawCsv(); + case "json": return drawJson(); + case "javascript": return drawJavascript(); + case "googlespreadsheet": return drawGooglespreadsheet(); + case "datasource": return drawDatasource(); + default: throw "Error: no data type specified"; + } + } + catch (error) { + document.getElementById('graph').innerHTML = + "" + error + ""; + } +} + +function drawCsv() { + // Instantiate our graph object. + var graph = new links.Graph3d(document.getElementById('graph')); + + // retrieve data and options + var data = getDataCsv(); + var options = getOptions(); + + // Draw our graph with the created data and options + graph.draw(data, options); +} + +function drawJson() { + // Instantiate our graph object. + var graph = new links.Graph3d(document.getElementById('graph')); + + // retrieve data and options + var data = getDataJson(); + var options = getOptions(); + + // Draw our graph with the created data and options + graph.draw(data, options); +} + +function drawJavascript() { + // Instantiate our graph object. + var graph = new links.Graph3d(document.getElementById('graph')); + + // retrieve data and options + var data = getDataJavascript(); + var options = getOptions(); + + // Draw our graph with the created data and options + graph.draw(data, options); +} + + +function drawGooglespreadsheet() { + // Instantiate our graph object. + drawGraph = function(response) { + document.getElementById("draw").disabled = ""; + + if (response.isError()) { + error = 'Error: ' + response.getMessage(); + document.getElementById('graph').innerHTML = + "" + error + ""; ; + } + + // retrieve the data from the query response + data = response.getDataTable(); + + // specify options + options = getOptions(); + + // Instantiate our graph object. + var graph = new links.Graph3d(document.getElementById('graph')); + + // Draw our graph with the created data and options + graph.draw(data, options); + } + + url = document.getElementById("googlespreadsheetText").value; + document.getElementById("draw").disabled = "disabled"; + + // send the request + query && query.abort(); + query = new google.visualization.Query(url); + query.send(drawGraph); +} + + +function drawDatasource() { + // Instantiate our graph object. + drawGraph = function(response) { + document.getElementById("draw").disabled = ""; + + if (response.isError()) { + error = 'Error: ' + response.getMessage(); + document.getElementById('graph').innerHTML = + "" + error + ""; ; + } + + // retrieve the data from the query response + data = response.getDataTable(); + + // specify options + options = getOptions(); + + // Instantiate our graph object. + var graph = new links.Graph3d(document.getElementById('graph')); + + // Draw our graph with the created data and options + graph.draw(data, options); + }; + + url = document.getElementById("datasourceText").value; + document.getElementById("draw").disabled = "disabled"; + + // if the entered url is a google spreadsheet url, replace the part + // "/ccc?" with "/tq?" in order to retrieve a neat data query result + if (url.indexOf("/ccc?")) { + url.replace("/ccc?", "/tq?"); + } + + // send the request + query && query.abort(); + query = new google.visualization.Query(url); + query.send(drawGraph); +} diff --git a/src/3dgraph/playground/prettify/lang-apollo.js b/src/3dgraph/playground/prettify/lang-apollo.js new file mode 100644 index 00000000..bfc0014c --- /dev/null +++ b/src/3dgraph/playground/prettify/lang-apollo.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["com",/^#[^\r\n]*/,null,"#"],["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)/,null,'"']],[["kwd",/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/, +null],["typ",/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[SE]?BANK\=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],["lit",/^\'(?:-*(?:\w|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?)?/],["pln",/^-*(?:[!-z_]|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?/i],["pun",/^[^\w\t\n\r \xA0()\"\\\';]+/]]),["apollo","agc","aea"]) \ No newline at end of file diff --git a/src/3dgraph/playground/prettify/lang-css.js b/src/3dgraph/playground/prettify/lang-css.js new file mode 100644 index 00000000..61157f38 --- /dev/null +++ b/src/3dgraph/playground/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[ \t\r\n\f]+/,null," \t\r\n\u000c"]],[["str",/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],["str",/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],["kwd",/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//], +["com",/^(?: + + + + + + + + + + +
+ +
+ + diff --git a/src/3dgraph/tests/test_extreme_data.html b/src/3dgraph/tests/test_extreme_data.html new file mode 100644 index 00000000..f9e55031 --- /dev/null +++ b/src/3dgraph/tests/test_extreme_data.html @@ -0,0 +1,78 @@ + + + + Graph 3D demo + + + + + + + + + + + + + + +
+ +
+ + diff --git a/src/3dgraph/tests/test_slider.html b/src/3dgraph/tests/test_slider.html new file mode 100644 index 00000000..7f7c7f01 --- /dev/null +++ b/src/3dgraph/tests/test_slider.html @@ -0,0 +1,43 @@ + + + Graph 3D demo + + + + + + + + + + + + +
+ +
+ +