From db88e958f4218f4280ef0be89f41364cd0a8e016 Mon Sep 17 00:00:00 2001 From: MaximilianV Date: Mon, 2 Jan 2017 20:32:09 +0100 Subject: [PATCH 01/30] add d3 as bower dependency --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 4cf2a5c1d..3f7fe8d83 100644 --- a/bower.json +++ b/bower.json @@ -4,7 +4,8 @@ "main": "barchart-basic.html", "dependencies": { "polymer": "Polymer/polymer#^1.4.0", - "plotly.js": "^1.20.5" + "plotly.js": "^1.20.5", + "d3": "^4.4.0" }, "devDependencies": { "iron-component-page": "PolymerElements/iron-component-page#^1.0.0", From 69a01b6f762a959fbdae7d699bd6d837226309b7 Mon Sep 17 00:00:00 2001 From: MaximilianV Date: Mon, 2 Jan 2017 21:43:06 +0100 Subject: [PATCH 02/30] create activitychart skeleton code and import @jak-ing code (currently not working and hard coded data) --- activitychart-basic.html | 163 +++++++++++++++++++++++++++++ all-imports.html | 1 + d3-import.html | 1 + demo/activitychart_basic_demo.html | 28 +++++ 4 files changed, 193 insertions(+) create mode 100644 activitychart-basic.html create mode 100644 d3-import.html create mode 100644 demo/activitychart_basic_demo.html diff --git a/activitychart-basic.html b/activitychart-basic.html new file mode 100644 index 000000000..b597f05fc --- /dev/null +++ b/activitychart-basic.html @@ -0,0 +1,163 @@ + + + + + + + + + + + diff --git a/all-imports.html b/all-imports.html index 77b43858f..edab58fa1 100644 --- a/all-imports.html +++ b/all-imports.html @@ -1,3 +1,4 @@ + diff --git a/d3-import.html b/d3-import.html new file mode 100644 index 000000000..10a8c3be7 --- /dev/null +++ b/d3-import.html @@ -0,0 +1 @@ + diff --git a/demo/activitychart_basic_demo.html b/demo/activitychart_basic_demo.html new file mode 100644 index 000000000..7d51d28db --- /dev/null +++ b/demo/activitychart_basic_demo.html @@ -0,0 +1,28 @@ + + + + + + + activitychart-basic demo + + + + + + + + + + +
+

basic activitychart demo (custom element)

+ + + +
+ + From da3fe2af3aa1763b118d49ab9b6546cc002d3384 Mon Sep 17 00:00:00 2001 From: MaximilianV Date: Sun, 8 Jan 2017 12:13:46 +0100 Subject: [PATCH 03/30] chart is now working, still hard coded data maybe we have an js expert who can find a cleaner approach to solve the "this" problem. --- activitychart-basic.html | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index b597f05fc..e795f176f 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -62,6 +62,7 @@ value: 20, }, }, + ready: function() { this.$.ajaxCaller.generateRequest(); }, @@ -73,7 +74,6 @@ handleResponse: function (data) { //var goodData = this._normalizeJSON(data.detail.response); var diagramDiv = this.$.diagram; - alert(this.squareSpacing); var svgWidth = 500, svgHeight = 500; //values should range between 1, 100 @@ -106,9 +106,10 @@ }, _showLabel: function (d, i) { + var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; d3.select('svg').append('rect') - .attr('x', this._spaceSquareHor(d, i) - 3) - .attr('y', this._spaceSquareVert(d, i) - 14) + .attr('x', polyElement._spaceSquareHor(d, i) - 3) + .attr('y', polyElement._spaceSquareVert(d, i) - 14) .attr('id', 'label-bg-' + i) .attr('fill', 'white') .attr('width', 60) @@ -116,8 +117,8 @@ d3.select('svg').append('text') .text(d + ' at ' + i) - .attr('x', this._spaceSquareHor(d, i)) - .attr('y', this._spaceSquareVert(d, i) - 3) + .attr('x', polyElement._spaceSquareHor(d, i)) + .attr('y', polyElement._spaceSquareVert(d, i) - 3) .attr('id', 'label-' + i) .style('font-size', '0.8em') .attr('fill', 'black'); @@ -129,11 +130,19 @@ }, _spaceSquareHor: function (d, i) { - return this.squareSpacing * (i % this.squaresPerRow); + var polyElement = this; + if (Polymer.dom(this).childNodes.length == 0) { + var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; + } + return polyElement.squareSpacing * (i % polyElement.squaresPerRow); }, _spaceSquareVert: function (d, i) { - return this.squareSpacing * (Math.floor(i / this.squaresPerRow)); + var polyElement = this; + if (Polymer.dom(this).childNodes.length == 0) { + var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; + } + return polyElement.squareSpacing * (Math.floor(i / polyElement.squaresPerRow)); }, /** * Normalizes the data supplied by the API to work with Plotly From 6ccb6e23c52ec4d2ea534e96c6888b512385ac22 Mon Sep 17 00:00:00 2001 From: JE Date: Mon, 9 Jan 2017 23:03:56 +0100 Subject: [PATCH 04/30] Labels are displayed inside the SVG. Width of the label's background rect is set relative to the label's text length. --- activitychart-basic.html | 54 ++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index e795f176f..d2c7c3a95 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -106,29 +106,57 @@ }, _showLabel: function (d, i) { + //using a group element 'label' with an appended text element and a rect element as background to display a label + var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; - d3.select('svg').append('rect') - .attr('x', polyElement._spaceSquareHor(d, i) - 3) - .attr('y', polyElement._spaceSquareVert(d, i) - 14) - .attr('id', 'label-bg-' + i) - .attr('fill', 'white') - .attr('width', 60) - .attr('height', 15); - - d3.select('svg').append('text') + + var label = d3.select('svg').append('g') + .attr('id', 'label-' + i); + + //append rect element first so that following text element is displayed above rect + label.append('rect'); + + var labelText = label.append('text') .text(d + ' at ' + i) - .attr('x', polyElement._spaceSquareHor(d, i)) - .attr('y', polyElement._spaceSquareVert(d, i) - 3) - .attr('id', 'label-' + i) .style('font-size', '0.8em') .attr('fill', 'black'); + //this split is necessary because the position is set relative to the size of the text element + labelText + .attr('x', polyElement._spaceLabelHor(d, i, labelText.node().getBBox().width)) + .attr('y', polyElement._spaceLabelVert(d, i)); + + //finish rect configuration that was appended previously + label.select('rect') + .attr('x', labelText.attr('x') - 3) + .attr('y', labelText.attr('y') - 11) + .attr('fill', 'white') + .attr('width', labelText.node().getBBox().width + 3) + .attr('height', labelText.node().getBBox().height); }, _hideLabel: function (d, i) { - d3.select('#label-bg-' + i).remove(); d3.select('#label-' + i).remove(); }, + _spaceLabelHor: function(d, i, labelWidth) { + var x = this._spaceSquareHor(d, i); + //calculate the maximum x position possible so that the label won't be cut off on the edge of the SVG + var maxPosition = ((this.squareSpacing * (this.squaresPerRow - 1)) + this.squareWidth) - labelWidth; + + if(x > maxPosition) { // label would normally be "shown" outside the SVG space + x = maxPosition + 1; + } + return x; + }, + + _spaceLabelVert: function(d, i) { + var y = this._spaceSquareVert(d, i) - 3; + if(y < 0) { // label would normally be "shown" outside the SVG space + y = this.squareSpacing * 1.5; + } + return y; + }, + _spaceSquareHor: function (d, i) { var polyElement = this; if (Polymer.dom(this).childNodes.length == 0) { From 018b182dec54c79b665ee63b506141b4109f585d Mon Sep 17 00:00:00 2001 From: MaximilianV Date: Tue, 10 Jan 2017 11:40:51 +0100 Subject: [PATCH 05/30] data might be passed as json in parameters if dataurl is provided, this will be used, otherwise the parameters dataset and labels are processed. --- activitychart-basic.html | 59 ++++++++++++++++++++---------- demo/activitychart_basic_demo.html | 16 +++++++- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index d2c7c3a95..3a5adbbb8 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -24,6 +24,7 @@ on-response="handleResponse" debounce-duration="300"> +
{{dataset}}
@@ -35,6 +36,15 @@ /** URL of data to be plotted */ dataurl: { type: String, + value: '', + }, + /** y values in JSON format (data to be displayed) */ + dataset: { + type: Array, + }, + /** x values in JSON format (labels for data) */ + labels: { + type: Array, }, /** Primary color of chart */ primarycolor: { @@ -64,7 +74,12 @@ }, ready: function() { - this.$.ajaxCaller.generateRequest(); + if(this.dataurl != "") { + this.$.ajaxCaller.generateRequest(); + } + else { + this._plotDiagram(); + } }, /** * Transforms the transmitted data into a plot @@ -72,27 +87,33 @@ * @param {Object} data The data to be plotted */ handleResponse: function (data) { - //var goodData = this._normalizeJSON(data.detail.response); + var goodData = this._normalizeJSON(data.detail.response); + this.dataset = goodData[0]["y"]; + this.labels = goodData[0]["x"]; + this._plotDiagram(); + }, + + _plotDiagram: function () { + console.log("Plotting..."); + console.log(this.dataset); + console.log(this.labels); var diagramDiv = this.$.diagram; var svgWidth = 500, - svgHeight = 500; - //values should range between 1, 100 - var dataset = [1, 10, 20, 35, 47, 65, 87, 100, - 1, 10, 65, 20, 35, 47, 87, 100, - 1, 10, 20, 47, 65, 87, 35, 100, - 1, 10, 20, 100, 35, 47, 65, 87, - 1, 20, 35, 10, 47, 65, 87, 100, - 47, 65, 87, 1, 10, 20, 35, 100]; - - //so far, the appropriate color is calculated based on the value of the datum, there is no set of fixed colors - var colorScale = d3.scaleLinear() - .domain([1, 100]) - .range(['#F5E8BB', '#930517']); + svgHeight = 500; + var dataset = this.dataset; + var datasetMin = Math.min.apply(null, dataset); + var datasetMax = Math.max.apply(null, dataset); + + //so far, the appropriate color is calculated based on the value of the datum, there is no set of fixed colors + var colorScale = d3.scaleLinear() + .domain([datasetMin, datasetMax]) + .range(['#F5E8BB', '#930517']); var svg = d3.select('#diagram').append('svg') .attr('width', svgWidth) .attr('height', svgHeight); + var squares = svg.selectAll('rect') .data(dataset) .enter().append('rect') @@ -109,15 +130,13 @@ //using a group element 'label' with an appended text element and a rect element as background to display a label var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; - var label = d3.select('svg').append('g') .attr('id', 'label-' + i); //append rect element first so that following text element is displayed above rect label.append('rect'); - var labelText = label.append('text') - .text(d + ' at ' + i) + .text(d + ' at ' + polyElement.labels[i]) .style('font-size', '0.8em') .attr('fill', 'black'); //this split is necessary because the position is set relative to the size of the text element @@ -159,7 +178,7 @@ _spaceSquareHor: function (d, i) { var polyElement = this; - if (Polymer.dom(this).childNodes.length == 0) { + if (Polymer.dom(this).node.nodeName != "ACTIVITYCHART-BASIC") { var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; } return polyElement.squareSpacing * (i % polyElement.squaresPerRow); @@ -167,7 +186,7 @@ _spaceSquareVert: function (d, i) { var polyElement = this; - if (Polymer.dom(this).childNodes.length == 0) { + if (Polymer.dom(this).node.nodeName != "ACTIVITYCHART-BASIC") { var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; } return polyElement.squareSpacing * (Math.floor(i / polyElement.squaresPerRow)); diff --git a/demo/activitychart_basic_demo.html b/demo/activitychart_basic_demo.html index 7d51d28db..2a0d47c77 100644 --- a/demo/activitychart_basic_demo.html +++ b/demo/activitychart_basic_demo.html @@ -20,7 +20,21 @@

basic activitychart demo (custom element)

From 1747cadc53c2d322e84f9a00ecf493bd5ef6cb4f Mon Sep 17 00:00:00 2001 From: MaximilianV Date: Tue, 10 Jan 2017 14:05:18 +0100 Subject: [PATCH 06/30] passing parameters now works --- activitychart-basic.html | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index 3a5adbbb8..205e01087 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -74,12 +74,7 @@ }, ready: function() { - if(this.dataurl != "") { - this.$.ajaxCaller.generateRequest(); - } - else { - this._plotDiagram(); - } + this.$.ajaxCaller.generateRequest(); }, /** * Transforms the transmitted data into a plot @@ -87,9 +82,11 @@ * @param {Object} data The data to be plotted */ handleResponse: function (data) { - var goodData = this._normalizeJSON(data.detail.response); - this.dataset = goodData[0]["y"]; - this.labels = goodData[0]["x"]; + if(this.dataurl != "") { + var goodData = this._normalizeJSON(data.detail.response); + this.dataset = goodData[0]["y"]; + this.labels = goodData[0]["x"]; + } this._plotDiagram(); }, From af4c3fd296f5b29faa857119a1909d7f7e6d120f Mon Sep 17 00:00:00 2001 From: MaximilianV Date: Wed, 11 Jan 2017 10:47:26 +0100 Subject: [PATCH 07/30] ajax call isn't performed when supplying data directly replacing ready by attached has done the trick --- activitychart-basic.html | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index 205e01087..2854d75d2 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -73,8 +73,13 @@ }, }, - ready: function() { - this.$.ajaxCaller.generateRequest(); + attached: function() { + if(this.dataurl != "") { + this.$.ajaxCaller.generateRequest(); + } + else { + this._plotDiagram(); + } }, /** * Transforms the transmitted data into a plot @@ -82,11 +87,9 @@ * @param {Object} data The data to be plotted */ handleResponse: function (data) { - if(this.dataurl != "") { - var goodData = this._normalizeJSON(data.detail.response); - this.dataset = goodData[0]["y"]; - this.labels = goodData[0]["x"]; - } + var goodData = this._normalizeJSON(data.detail.response); + this.dataset = goodData[0]["y"]; + this.labels = goodData[0]["x"]; this._plotDiagram(); }, From 7613df8f1480c6ea47d68d9cce1ae8ddcfe5c6f4 Mon Sep 17 00:00:00 2001 From: JE Date: Wed, 11 Jan 2017 13:51:26 +0100 Subject: [PATCH 08/30] Fix SVG chart dimensions, use ES6 notation Set SVG height and width dynamically based on the size of the dataset and squaresPerRow. Use ES6 notation for calculating min and max values of the dataset. --- activitychart-basic.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index 2854d75d2..75151ab50 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -98,11 +98,13 @@ console.log(this.dataset); console.log(this.labels); var diagramDiv = this.$.diagram; - var svgWidth = 500, - svgHeight = 500; + var dataset = this.dataset; - var datasetMin = Math.min.apply(null, dataset); - var datasetMax = Math.max.apply(null, dataset); + var datasetMin = Math.min(...dataset); + var datasetMax = Math.max(...dataset); + + var svgWidth = this.squaresPerRow * this.squareSpacing, + svgHeight = Math.ceil(dataset.length / this.squaresPerRow) * this.squareSpacing; //so far, the appropriate color is calculated based on the value of the datum, there is no set of fixed colors var colorScale = d3.scaleLinear() From 3bdcfaca3b2d02d28729b258f3b83b0985fcf7c2 Mon Sep 17 00:00:00 2001 From: JE Date: Wed, 11 Jan 2017 14:34:36 +0100 Subject: [PATCH 09/30] Use a more D3-y approach for the dataset. Make the dataset that is passed to D3 and which the visualization is built from look like this: [ {'x': timeStamp0, 'y': value0}, ... ,{'x': timeStampN, 'y': valueN}] --- activitychart-basic.html | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index 75151ab50..ffc988ff8 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -93,12 +93,24 @@ this._plotDiagram(); }, + _buildDataset: function(labels, dataset) { + // create data like this: + // [ {'x': timeStamp0, 'y': value0}, ... ,{'x': timeStampN, 'y': valueN}] + var data = []; + for(var j = 0; j < labels.length; j++) { + data[j] = {'x': labels[j], 'y': dataset[j]}; + } + return data; + }, + _plotDiagram: function () { console.log("Plotting..."); console.log(this.dataset); console.log(this.labels); var diagramDiv = this.$.diagram; + var data = this._buildDataset(this.labels, this.dataset); + var dataset = this.dataset; var datasetMin = Math.min(...dataset); var datasetMax = Math.max(...dataset); @@ -115,15 +127,14 @@ .attr('width', svgWidth) .attr('height', svgHeight); - var squares = svg.selectAll('rect') - .data(dataset) + .data(data) .enter().append('rect') .attr('width', this.squareWidth) .attr('height', this.squareWidth) .attr('x', this._spaceSquareHor) .attr('y', this._spaceSquareVert) - .attr('fill', colorScale) + .attr('fill', function(d) { return colorScale(d.y); }) .on('mouseover', this._showLabel) .on('mouseout', this._hideLabel); }, @@ -138,7 +149,7 @@ //append rect element first so that following text element is displayed above rect label.append('rect'); var labelText = label.append('text') - .text(d + ' at ' + polyElement.labels[i]) + .text(d.y + ' at ' + d.x) .style('font-size', '0.8em') .attr('fill', 'black'); //this split is necessary because the position is set relative to the size of the text element From f37fe84191efa9c660ba72c19d0880cc7b9e4bfe Mon Sep 17 00:00:00 2001 From: JE Date: Wed, 11 Jan 2017 16:58:22 +0100 Subject: [PATCH 10/30] Use custom colors Use the two custom colors marking the minimum and the maximum color. Custom colors can be passed to the component via an attribute in the HTML tag. --- activitychart-basic.html | 10 +++++----- demo/activitychart_basic_demo.html | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index ffc988ff8..ee797ceb5 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -46,15 +46,15 @@ labels: { type: Array, }, - /** Primary color of chart */ + /** Primary color of chart; formats supported are Hex, Short Hex, RGB (absolute), RGB (percentual), RGBA (absolute), RGBA (percentual), keyword (e.g. 'white'), HSL and HSLA */ primarycolor: { type: String, - value: 'rgba(0,0,0,1)', + value: '#930517', }, - /** Accent color of chart */ + /** Accent color of chart; formats supported are Hex, Short Hex, RGB (absolute), RGB (percentual), RGBA (absolute), RGBA (percentual), keyword (e.g. 'white'), HSL and HSLA */ accentcolor: { type: String, - value: 'rgba(0,0,0,1)', + value: '#F5E8BB', }, /** Number of colored dots per row (opt, default 8) */ squaresPerRow: { @@ -121,7 +121,7 @@ //so far, the appropriate color is calculated based on the value of the datum, there is no set of fixed colors var colorScale = d3.scaleLinear() .domain([datasetMin, datasetMax]) - .range(['#F5E8BB', '#930517']); + .range([this.accentcolor, this.primarycolor]); var svg = d3.select('#diagram').append('svg') .attr('width', svgWidth) diff --git a/demo/activitychart_basic_demo.html b/demo/activitychart_basic_demo.html index 2a0d47c77..497c66333 100644 --- a/demo/activitychart_basic_demo.html +++ b/demo/activitychart_basic_demo.html @@ -20,8 +20,8 @@

basic activitychart demo (custom element)

From e4b408ec336116d6de87db2d43706912099d3b34 Mon Sep 17 00:00:00 2001 From: JE Date: Thu, 12 Jan 2017 00:25:21 +0100 Subject: [PATCH 11/30] Change orientation, improve label positioning Change orientation changing the focus from rows to columns. Improve the calculation of positions and sizes for the labels to make these values more consistent among different square sizes, spacing options etc by mostly dropping hardcoded values. --- activitychart-basic.html | 40 ++++++++++++++++-------------- demo/activitychart_basic_demo.html | 6 ++--- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index ee797ceb5..ae407a6ae 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -56,8 +56,8 @@ type: String, value: '#F5E8BB', }, - /** Number of colored dots per row (opt, default 8) */ - squaresPerRow: { + /** Number of colored dots per column (opt, default 8) */ + squaresPerColumn: { type: Number, value: 8, }, @@ -115,8 +115,8 @@ var datasetMin = Math.min(...dataset); var datasetMax = Math.max(...dataset); - var svgWidth = this.squaresPerRow * this.squareSpacing, - svgHeight = Math.ceil(dataset.length / this.squaresPerRow) * this.squareSpacing; + var svgWidth = Math.ceil(dataset.length / this.squaresPerColumn) * this.squareSpacing, + svgHeight = this.squaresPerColumn * this.squareSpacing; //so far, the appropriate color is calculated based on the value of the datum, there is no set of fixed colors var colorScale = d3.scaleLinear() @@ -152,18 +152,22 @@ .text(d.y + ' at ' + d.x) .style('font-size', '0.8em') .attr('fill', 'black'); + + var labelWidth = labelText.node().getBBox().width; + var labelHeight = labelText.node().getBBox().height; + //this split is necessary because the position is set relative to the size of the text element labelText - .attr('x', polyElement._spaceLabelHor(d, i, labelText.node().getBBox().width)) - .attr('y', polyElement._spaceLabelVert(d, i)); + .attr('x', polyElement._spaceLabelHor(d, i, labelWidth)) + .attr('y', polyElement._spaceLabelVert(d, i, labelHeight)); //finish rect configuration that was appended previously label.select('rect') - .attr('x', labelText.attr('x') - 3) - .attr('y', labelText.attr('y') - 11) + .attr('x', labelText.attr('x') - 1) + .attr('y', labelText.attr('y') - 0.7 * labelHeight) // 0.7 as factor because labelHeight is bigger than actual height of text .attr('fill', 'white') - .attr('width', labelText.node().getBBox().width + 3) - .attr('height', labelText.node().getBBox().height); + .attr('width', labelWidth + 1) //ok, so that it doesn't become too short for the text because of the visual offset above + .attr('height', 0.8 * labelHeight); }, _hideLabel: function (d, i) { @@ -173,18 +177,18 @@ _spaceLabelHor: function(d, i, labelWidth) { var x = this._spaceSquareHor(d, i); //calculate the maximum x position possible so that the label won't be cut off on the edge of the SVG - var maxPosition = ((this.squareSpacing * (this.squaresPerRow - 1)) + this.squareWidth) - labelWidth; + var maxPosition = d3.select('svg').attr('width') - (this.squareSpacing - this.squareWidth) - labelWidth; if(x > maxPosition) { // label would normally be "shown" outside the SVG space - x = maxPosition + 1; + x = maxPosition; } return x; }, - _spaceLabelVert: function(d, i) { - var y = this._spaceSquareVert(d, i) - 3; - if(y < 0) { // label would normally be "shown" outside the SVG space - y = this.squareSpacing * 1.5; + _spaceLabelVert: function(d, i, labelHeight) { + var y = this._spaceSquareVert(d, i) - 2; + if(y < 0) { // label would normally be "shown" just exactly outside the SVG space + y = this.squareWidth + 0.7 * labelHeight; } return y; }, @@ -194,7 +198,7 @@ if (Polymer.dom(this).node.nodeName != "ACTIVITYCHART-BASIC") { var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; } - return polyElement.squareSpacing * (i % polyElement.squaresPerRow); + return polyElement.squareSpacing * (Math.floor(i / polyElement.squaresPerColumn)); }, _spaceSquareVert: function (d, i) { @@ -202,7 +206,7 @@ if (Polymer.dom(this).node.nodeName != "ACTIVITYCHART-BASIC") { var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; } - return polyElement.squareSpacing * (Math.floor(i / polyElement.squaresPerRow)); + return polyElement.squareSpacing * (i % polyElement.squaresPerColumn); }, /** * Normalizes the data supplied by the API to work with Plotly diff --git a/demo/activitychart_basic_demo.html b/demo/activitychart_basic_demo.html index 497c66333..807373971 100644 --- a/demo/activitychart_basic_demo.html +++ b/demo/activitychart_basic_demo.html @@ -20,8 +20,8 @@

basic activitychart demo (custom element)

From 58995ced3456e2a37dace6a70472f57ed2ea18f9 Mon Sep 17 00:00:00 2001 From: JE Date: Thu, 12 Jan 2017 10:38:45 +0100 Subject: [PATCH 12/30] Add support for custom minimum/maximum value Allow the minimum and maximum value used for the color scale to be overwritten by arguments in the HTML tag. By default, these values are derived from the dataset. --- activitychart-basic.html | 27 +++++++++++++++++++++++++-- demo/activitychart_basic_demo.html | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index ae407a6ae..2bff84550 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -71,6 +71,17 @@ type: Number, value: 20, }, + //this could also be solved by setting value to a function calculating the default min/max value from the dataset (but only if we receive the dataset as a JSON object isntead of just receiving a URL) + /** Minimum value from the dataset that is used for the color scale can be overwritten. */ + minValue: { + type: Number, + value: null + }, + /** Maximum value from the dataset that is used for the color scale can be overwritten. */ + maxValue: { + type: Number, + value: null + } }, attached: function() { @@ -112,8 +123,20 @@ var data = this._buildDataset(this.labels, this.dataset); var dataset = this.dataset; - var datasetMin = Math.min(...dataset); - var datasetMax = Math.max(...dataset); + + var datasetMin; + if(this.minValue == null) { + datasetMin = Math.min(...dataset); + } else { + datasetMin = this.minValue; + } + + var datasetMax; + if(this.maxValue == null) { + datasetMax = Math.max(...dataset); + } else { + datasetMax = this.maxValue; + } var svgWidth = Math.ceil(dataset.length / this.squaresPerColumn) * this.squareSpacing, svgHeight = this.squaresPerColumn * this.squareSpacing; diff --git a/demo/activitychart_basic_demo.html b/demo/activitychart_basic_demo.html index 807373971..b784d19cd 100644 --- a/demo/activitychart_basic_demo.html +++ b/demo/activitychart_basic_demo.html @@ -20,7 +20,7 @@

basic activitychart demo (custom element)

From f18bbc30c36d95d20996de131f7309194c107c6e Mon Sep 17 00:00:00 2001 From: JE Date: Sat, 4 Feb 2017 21:11:33 +0100 Subject: [PATCH 27/30] Refactor, decompose code and add method comments --- activitychart-basic.html | 223 +++++++++++++++++------------ demo/activitychart_basic_demo.html | 7 +- 2 files changed, 135 insertions(+), 95 deletions(-) diff --git a/activitychart-basic.html b/activitychart-basic.html index cf6e8f79a..d38ee6978 100644 --- a/activitychart-basic.html +++ b/activitychart-basic.html @@ -69,16 +69,15 @@ value: 20, }, /** This property is computed automatically based on square-size and square-spacing and cannot be overwritten. */ - spaceBetweenSquares: { + _spaceBetweenSquares: { type: Number, computed: '_computeSpaceBetweenSquares(squareSpacing, squareSize)' }, /** Number of colored dots per column. Is always derived from the size of axisLabels.y.values provided and cannot be overwritten! */ - squaresPerColumn: { + _squaresPerColumn: { type: Number, - computed: '_computeSquaresPerColumn(axisLabels)' + computed: '_deduceSquaresPerColumn(axisLabels)' }, - //this could also be solved by setting value to a function calculating the default min/max value from the dataset (but only if we receive the dataset as a JSON object isntead of just receiving a URL) /** Minimum value from the dataset that is used for the color scale can be overwritten. */ minValue: { type: Number, @@ -91,7 +90,7 @@ }, /** Labels for x and y axis. Please provide as {'x': {'title': String[, 'type': String]}, 'y': {'title': String, 'values': String[]}} - 'Type' can be 'week' to show the corresponding calendar week. If this key value pair is left out, the date is shown. + 'Type' can be 'week' to show the corresponding calendar week. If this key-value-pair is left out, simply the date is shown. The correct labels for the x axis are chosen programmatically based on the data and type.*/ axisLabels: { type: Object, @@ -115,12 +114,12 @@ type: Number, value: 2 }, - /** Set text size (in px) of label shown when hovering over a datum (opt, default: 13) */ + /** Set text size (in px) of label shown when hovering over a datum (opt, default: 14) */ labelTextSize: { type: Number, value: 14 }, - /** Customize the label background color (opt, default: white) */ + /** Customize the label background color (opt, default: 'white') */ labelBackgroundColor: { type: String, value: 'white' @@ -142,37 +141,61 @@ } }, - _computeSquaresPerColumn: function(axisLabels) { - return axisLabels.y.values.length; - }, - + /** Computes the blank space between two squares. */ _computeSpaceBetweenSquares: function(squareSpacing, squareSize) { return squareSpacing - squareSize; }, + /** Deduces the number of squares to be shown per column from the number of y labels provided. */ + _deduceSquaresPerColumn: function(axisLabels) { + return axisLabels.y.values.length; + }, + attached: function() { if(this.dataurl) { this.$.ajaxCaller.generateRequest(); } else { - this._plotDiagram(); + this._createDiagram(); } }, + /** * Transforms the transmitted data into a plot * * @param {Object} data The data to be plotted */ - handleResponse: function (data) { + handleResponse: function(data) { var goodData = this._normalizeJSON(data.detail.response); this.dataset = goodData[0]["y"]; this.labels = goodData[0]["x"]; - this._plotDiagram(); + this._createDiagram(); }, - _buildDataset: function(labels, dataset) { - // create data like this: - // [ {'x': timeStamp0, 'y': value0}, ... ,{'x': timeStampN, 'y': valueN}] + /** Creates the activity diagram including the axes based on the data. */ + _createDiagram: function() { + this.svg = Plotly.d3.select(this.$.diagram).append('svg'); + //let other D3 functions later use this.svg in order to restrict D3's selection process to the local DOM of this component! + + this.data = this._parseData(this.labels, this.dataset); + + this._plotYAxis(); + this._plotXAxis(); + this._plotVisualization(); + + // set svg dimensions + this.svg + .attr('height', this._getBounds('y-axis').height + this._spaceBetweenSquares + this._getBounds('x-axis').height) + .attr('width', this._getBounds('y-axis').width + this._spaceBetweenSquares + this._getBounds('x-axis').width); + }, + + /** + * Parses the labels and the data into a more D3-friendly format: + * [ {'x': timeStamp_0, 'y': value_0}, ... ,{'x': timeStamp_N, 'y': value_N}] + * + * @return {Object} + */ + _parseData: function(labels, dataset) { var data = []; for(var j = 0; j < labels.length; j++) { data[j] = {'x': labels[j], 'y': dataset[j]}; @@ -180,32 +203,15 @@ return data; }, - _plotDiagram: function () { - this.diagramDiv = this.$.diagram; //hand this diagram wrapper to D3 later on to limit D3's selection process to the local DOM - - var data = this._buildDataset(this.labels, this.dataset); - - if(this.minValue == null) { - this.minValue = Math.min(...this.dataset); - } - if(this.maxValue == null) { - this.maxValue = Math.max(...this.dataset); - } - - //so far, the appropriate color is calculated based on the value of the datum, there is no set of fixed colors - var colorScale = Plotly.d3.scale.linear() - .domain([this.minValue, this.maxValue]) - .range([this.accentcolor, this.primarycolor]); - - var svg = Plotly.d3.select(this.diagramDiv).append('svg'); - - //create y axis - var yAxis = svg.append('g'); + /** Creates the Y axis of the diagram */ + _plotYAxis: function() { + var yAxis = this.svg.append('g') + .attr('id', 'y-axis'); var yAxisTitle = yAxis.append('text') .text(this.axisLabels.y.title) .style('font-size', this.axisTitleTextSize) - .attr('x', 0) //make this customizable as some kind of margin maybe? + .attr('x', 0) .attr('y', this.axisTitleTextSize); var yLabelValues = this.axisLabels.y.values; @@ -219,25 +225,24 @@ .attr('x', 0) .attr('y', this._spaceSquareVert.bind(this)); - yAxisLabels.attr('transform', 'translate(0, ' + (3 * this.axisTitleTextSize) + ')'); - - //calculate dimensions of the actual visualization (without the axes) - var visualizationHeight = (this.squaresPerColumn * this.squareSpacing) + this.spaceBetweenSquares; - var visualizationWidth = (Math.ceil(this.dataset.length / this.squaresPerColumn) * this.squareSpacing) + this.spaceBetweenSquares; + yAxisLabels.attr('transform', 'translate(0, ' + (2.5 * this.axisTitleTextSize) + ')'); + }, - //create x axis - var xAxis = svg.append('g'); + /** Creates the X axis of the diagram */ + _plotXAxis: function() { + var xAxis = this.svg.append('g') + .attr('id', 'x-axis'); var xLabelValues = []; - for(var i = 0; i < data.length; i++) { - if((i % this.squaresPerColumn == 0) && ((i / this.squaresPerColumn) % this.xAxisLabelFrequency == 0)) { - xLabelValues.push(data[i]); + for(var i = 0; i < this.data.length; i++) { + if((i % this._squaresPerColumn == 0) && ((i / this._squaresPerColumn) % this.xAxisLabelFrequency == 0)) { + xLabelValues.push(this.data[i]); } } - var xAxisLabels = xAxis.append('g'); - var xAxisLabelsTexts = xAxisLabels.selectAll('text') + //add x axis labels + xAxis.append('g').selectAll('text') .data(xLabelValues) .enter().append('text') .text(function(d) { @@ -251,42 +256,67 @@ .attr('transform', this._transformXAxisLabels.bind(this)) .attr('alignment-baseline', 'after-edge'); - var xAxisTitle = xAxis.append('text') + //add title + xAxis.append('text') .text(this.axisLabels.x.title) .style('font-size', this.axisTitleTextSize) - .attr('x', visualizationWidth - this.spaceBetweenSquares) + .attr('x', this._getVisualizationWidth() - this._spaceBetweenSquares) .attr('y', this.axisTitleTextSize) .attr('alignment-baseline', 'after-edge'); - var yAxisBounds = yAxis.node().getBBox(); - var xAxisBounds = xAxis.node().getBBox(); + xAxis.attr('transform', 'translate(' + + (this._getBounds('y-axis').width + this._spaceBetweenSquares) + + ', ' + + (this._getBounds('y-axis').height + this._spaceBetweenSquares) + + ')'); + }, - xAxis.attr('transform', 'translate(' + (yAxisBounds.width + this.spaceBetweenSquares) + ', ' + (yAxisBounds.height + this.spaceBetweenSquares) + ')'); + /** Plots the actual activity visualization consisting solely of squares */ + _plotVisualization: function() { + if(this.minValue == null) { + this.minValue = Math.min(...this.dataset); + } + if(this.maxValue == null) { + this.maxValue = Math.max(...this.dataset); + } + + //the appropriate color is calculated based on the value of the datum, there is no set of fixed colors + var colorScale = Plotly.d3.scale.linear() + .domain([this.minValue, this.maxValue]) + .range([this.accentcolor, this.primarycolor]); - //create actual activity visualization - var visualization = svg.append('g') - .attr('transform', 'translate(' + (yAxisBounds.width + this.spaceBetweenSquares) + ', ' + (yAxisBounds.height - visualizationHeight + 2 * this.spaceBetweenSquares) + ')') + var visualization = this.svg.append('g') + .attr('transform', 'translate(' + + (this._getBounds('y-axis').width + this._spaceBetweenSquares) + + ', ' + + (this._getBounds('y-axis').height - this._getVisualizationHeight() + 2 * this._spaceBetweenSquares) + + ')') .attr('id', 'visualization'); var squares = visualization.selectAll('rect') - .data(data) + .data(this.data) .enter().append('rect') .attr('width', this.squareSize) .attr('height', this.squareSize) .attr('x', this._spaceSquareHor.bind(this)) .attr('y', this._spaceSquareVert.bind(this)) .attr('fill', function(d) { return colorScale(d.y); }) - .on('mouseover', this._showLabel.bind(this)) - .on('mouseout', this._hideLabel.bind(this)); + .on('mouseover', this._appendLabel.bind(this)) + .on('mouseout', this._deleteLabel.bind(this)); + }, - // set svg dimensions - svg - .attr('height', yAxisBounds.height + this.spaceBetweenSquares + xAxisBounds.height) - .attr('width', yAxisBounds.width + this.spaceBetweenSquares + xAxisBounds.width); + /** Returns the bounding box of a group element, useful for accessing a group's width and height */ + _getBounds: function(id) { + return this.svg.select('#' + id).node().getBBox(); }, + _convertToLocaleString: function(timestamp) { + return (new Date(timestamp * 1000)).toLocaleString(); + }, + + /** Returns a 'translate(...)rotate(...)' String that helps position the labels of the X axis right and rotates them if they are dates. */ _transformXAxisLabels: function(d, i) { - var xTranslation = this._spaceSquareHor(d, (i * this.xAxisLabelFrequency * this.squaresPerColumn)); + var xTranslation = this._spaceSquareHor(d, (i * this.xAxisLabelFrequency * this._squaresPerColumn)); var yTranslation; var rotation; @@ -301,8 +331,7 @@ return 'translate(' + xTranslation + ', ' + yTranslation + ')rotate(' + rotation + ')'; }, - //calculate and return calendar week from the given time stamp - //after ISO 8601: week starts on Monday, 1st week of the year = week with 1st Thursday of the year + /** Returns the calendar week the given time stamp falls into; conforms to ISO 8601: Week starts on Monday, 1st week of the year is the week with the 1st Thursday of the year. */ _calculateCalendarWeek: function(timeStamp) { var millisecondsInASecond = 1000; var millisecondsInAWeek = 604800000; @@ -324,23 +353,32 @@ return Math.ceil((firstThursday - thisDay) / millisecondsInAWeek) + 1; }, - _showLabel: function (d, i) { - //using a group element 'label' with an appended text element and a rect element as background to display a label + /** Calculates width of the actual visualization (without the axes) */ + _getVisualizationWidth: function() { + return (Math.ceil(this.dataset.length / this._squaresPerColumn) * this.squareSpacing) + this._spaceBetweenSquares; + }, + + /** Calculates height of the actual visualization (without the axes) */ + _getVisualizationHeight: function() { + return (this._squaresPerColumn * this.squareSpacing) + this._spaceBetweenSquares; + }, - var label = Plotly.d3.select(this.diagramDiv).select('#visualization').append('g') + /** Appends a label showing the detailed activity count and date of the activity square that the mouse hovers over. */ + _appendLabel: function(d, i) { + //use a group element 'label-...' with an appended text element and a rect element as background + var label = this.svg.select('#visualization').append('g') .attr('id', 'label-' + i); var labelText = label.append('text') .text(d.y + this.labelSeparator + this._convertToLocaleString(d.x)) .style('font-size', this.labelTextSize) - .attr('fill', 'black') .attr('alignment-baseline', 'after-edge'); - var textWidth = labelText.node().getBBox().width; + var labelTextWidth = labelText.node().getBBox().width; //position is set relative to the size of the text element labelText - .attr('x', this._spaceLabelHor(d, i, textWidth)) + .attr('x', this._spaceLabelHor(d, i, labelTextWidth)) .attr('y', this._spaceLabelVert(d, i)); //insert rect as background @@ -348,18 +386,20 @@ .attr('x', labelText.attr('x') - this.labelPadding) .attr('y', labelText.attr('y') - this.labelTextSize - this.labelPadding) .attr('fill', this.labelBackgroundColor) - .attr('width', textWidth + (2 * this.labelPadding)) + .attr('width', labelTextWidth + (2 * this.labelPadding)) .attr('height', this.labelTextSize + (2 * this.labelPadding)); }, - _hideLabel: function (d, i) { - Plotly.d3.select(this.diagramDiv).select('#label-' + i).remove(); + /** Deletes the label once the mouse doesn't hover over the corresponding square anymore */ + _deleteLabel: function(d, i) { + this.svg.select('#label-' + i).remove(); }, - _spaceLabelHor: function(d, i, textWidth) { + /** Returns the x position of a label considering the corresponding square's position, the label's width as well as the bounds of the SVG */ + _spaceLabelHor: function(d, i, labelTextWidth) { var x = this._spaceSquareHor(d, i); //calculate the maximum x position possible so that the label won't be cut off on the edge of the SVG - var maxPosition = Plotly.d3.select(this.diagramDiv).select('#visualization').node().getBBox().width - textWidth - this.labelPadding; + var maxPosition = this._getBounds('visualization').width - labelTextWidth - this.labelPadding; if(x > maxPosition) { // label would normally be "shown" outside the SVG space x = maxPosition; @@ -367,28 +407,33 @@ return x; }, + /** Returns the y position of a label considering the corresponding square's position, the label's height as well as the bounds of the SVG */ _spaceLabelVert: function(d, i) { var y = this._spaceSquareVert(d, i) - this.labelPadding; - if(y <= this.labelTextSize + this.labelPadding) { // there wouldn't enough space above the square to show its label - y = this.squareSize + this.labelTextSize + this.labelPadding; + var maxPosition = this.labelTextSize + this.labelPadding; + if(y <= maxPosition) { // there wouldn't be enough space above the square to show its label + y = this.squareSize + maxPosition; } return y; }, - _spaceSquareHor: function (d, i) { - return this.squareSpacing * (Math.floor(i / this.squaresPerColumn)); + /** Returns the x position where the square should be placed */ + _spaceSquareHor: function(d, i) { + return this.squareSpacing * (Math.floor(i / this._squaresPerColumn)); }, - _spaceSquareVert: function (d, i) { - return this.squareSpacing * (i % this.squaresPerColumn); + /** Returns the y position where the square should be placed */ + _spaceSquareVert: function(d, i) { + return this.squareSpacing * (i % this._squaresPerColumn); }, + /** * Normalizes the data supplied by the API to work with Plotly * * @param {Object} data The data (as JSON) to be normalized * @return {Array} The normalized data */ - _normalizeJSON: function (data) { + _normalizeJSON: function(data) { var x = []; var y = []; for (entry in data) { @@ -403,10 +448,6 @@ var plotlyData = []; plotlyData.push(diagramData); return plotlyData; - }, - - _convertToLocaleString: function(timestamp) { - return (new Date(timestamp * 1000)).toLocaleString(); } }); }); diff --git a/demo/activitychart_basic_demo.html b/demo/activitychart_basic_demo.html index ffaf78e5d..3310a6f3e 100644 --- a/demo/activitychart_basic_demo.html +++ b/demo/activitychart_basic_demo.html @@ -23,16 +23,15 @@

basic activitychart demo (custom element)