New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Activity chart #14
Changes from 15 commits
db88e95
69a01b6
da3fe2a
6ccb6e2
018b182
1747cad
af4c3fd
7613df8
3bdcfac
f37fe84
e4b408e
58995ce
8a8e155
50b04ca
410a22f
2f04302
ff9c404
fdad5bf
6082b13
5257e4a
349387b
cf43575
690c8dc
816a9b9
9c7163a
68ca7a4
2074174
785a759
f18bbc3
a59e687
3216b17
b726e20
027045b
1230a84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
*bower_components/* | ||
.DS_Store |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
<link rel="import" href="../polymer/polymer.html"> | ||
<link rel="import" href="../iron-ajax/iron-ajax.html"> | ||
<link rel="import" href="d3-import.html"> | ||
|
||
|
||
<!-- | ||
`activitychart-basic` | ||
This component shows an activity history, similar to the colored github chart | ||
|
||
@demo demo/activitychart_basic_demo.html | ||
--> | ||
<dom-module id="activitychart-basic"> | ||
<template> | ||
<style> | ||
:host { | ||
display: block; | ||
} | ||
</style> | ||
|
||
<iron-ajax | ||
id="ajaxCaller" | ||
url="[[dataurl]]" | ||
handle-as="json" | ||
on-response="handleResponse" | ||
debounce-duration="300"> | ||
</iron-ajax> | ||
<div style="display:none;">{{dataset}}</div> | ||
<div id="diagram"></div> | ||
</template> | ||
|
||
<script> | ||
Polymer({ | ||
is: 'activitychart-basic', | ||
|
||
properties: { | ||
/** 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; 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: '#930517', | ||
}, | ||
/** 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: '#F5E8BB', | ||
}, | ||
/** Number of colored dots per column (opt, default 8) */ | ||
squaresPerColumn: { | ||
type: Number, | ||
value: 8, | ||
}, | ||
/** Width of square (opt, default: 15) */ | ||
squareWidth: { | ||
type: Number, | ||
value: 15, | ||
}, | ||
/** Spacing of squares (opt, default: 20) */ | ||
squareSpacing: { | ||
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() { | ||
if(this.dataurl != "") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the |
||
this.$.ajaxCaller.generateRequest(); | ||
} | ||
else { | ||
this._plotDiagram(); | ||
} | ||
}, | ||
/** | ||
* Transforms the transmitted data into a plot | ||
* | ||
* @param {Object} data The data to be plotted | ||
*/ | ||
handleResponse: function (data) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe make that private? |
||
var goodData = this._normalizeJSON(data.detail.response); | ||
this.dataset = goodData[0]["y"]; | ||
this.labels = goodData[0]["x"]; | ||
this._plotDiagram(); | ||
}, | ||
|
||
_buildDataset: function(labels, dataset) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please document |
||
// 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 () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please document |
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we move this line above |
||
|
||
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; | ||
|
||
//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([this.accentcolor, this.primarycolor]); | ||
|
||
var svg = d3.select('#diagram').append('svg') | ||
.attr('width', svgWidth) | ||
.attr('height', svgHeight); | ||
|
||
var squares = svg.selectAll('rect') | ||
.data(data) | ||
.enter().append('rect') | ||
.attr('width', this.squareWidth) | ||
.attr('height', this.squareWidth) | ||
.attr('x', this._spaceSquareHor) | ||
.attr('y', this._spaceSquareVert) | ||
.attr('fill', function(d) { return colorScale(d.y); }) | ||
.on('mouseover', this._showLabel) | ||
.on('mouseout', this._hideLabel); | ||
}, | ||
|
||
_showLabel: function (d, i) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. even though this method is private, we might wanna use more meaningful names than d, i There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm against that here. It's a D3 thing to use precisely this signature; d being the datum and i being its index. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, I see. Sorry for the confusion! |
||
//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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there maybe a better way to traverse the dom than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sure there is; Jan made a remark about this earlier. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's discuss / investigate in the next team meeting |
||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. like this approach |
||
.text(d.y + ' at ' + d.x) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, changed this in cf43575. |
||
.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, labelWidth)) | ||
.attr('y', polyElement._spaceLabelVert(d, i, labelHeight)); | ||
|
||
//finish rect configuration that was appended previously | ||
label.select('rect') | ||
.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', 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) { | ||
d3.select('#label-' + i).remove(); | ||
}, | ||
|
||
_spaceLabelHor: function(d, i, labelWidth) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe rename d, i to something more meaningful |
||
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 = d3.select('svg').attr('width') - (this.squareSpacing - this.squareWidth) - labelWidth; | ||
|
||
if(x > maxPosition) { // label would normally be "shown" outside the SVG space | ||
x = maxPosition; | ||
} | ||
return x; | ||
}, | ||
|
||
_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; | ||
}, | ||
|
||
_spaceSquareHor: function (d, i) { | ||
var polyElement = this; | ||
if (Polymer.dom(this).node.nodeName != "ACTIVITYCHART-BASIC") { | ||
var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; | ||
} | ||
return polyElement.squareSpacing * (Math.floor(i / polyElement.squaresPerColumn)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we should use a pre-commit hook for beautify.js |
||
}, | ||
|
||
_spaceSquareVert: function (d, i) { | ||
var polyElement = this; | ||
if (Polymer.dom(this).node.nodeName != "ACTIVITYCHART-BASIC") { | ||
var polyElement = Polymer.dom(this).parentNode.parentNode.parentNode; | ||
} | ||
return polyElement.squareSpacing * (i % polyElement.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) { | ||
var x = []; | ||
var y = []; | ||
for (entry in data) { | ||
//compute date string | ||
//x.push(entry); | ||
x.push((new Date(entry*1000)).toLocaleString()); | ||
y.push(data[entry]); | ||
} | ||
var diagramData = {"x":x}; | ||
diagramData["y"] = y; | ||
diagramData["type"] = "bar"; | ||
diagramData["marker"] = {color:this.primarycolor}; | ||
var plotlyData = []; | ||
plotlyData.push(diagramData); | ||
return plotlyData; | ||
} | ||
}); | ||
</script> | ||
</dom-module> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
<!-- all-imports.html --> | ||
<link rel="import" href="barchart-basic.html"> | ||
<link rel="import" href="piechart-basic.html"> | ||
<link rel="import" href="activitychart-basic.html"> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<script src="../d3/d3.min.js"></script> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes"> | ||
|
||
<title>activitychart-basic demo</title> | ||
|
||
<script src="../../webcomponentsjs/webcomponents-lite.js"></script> | ||
|
||
<link rel="import" href="../../iron-demo-helpers/demo-pages-shared-styles.html"> | ||
<link rel="import" href="../../iron-demo-helpers/demo-snippet.html"> | ||
<link rel="import" href="../activitychart-basic.html"> | ||
|
||
<style is="custom-style" include="demo-pages-shared-styles"> | ||
div.vertical-section-container { | ||
max-width: 1500px; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems to not be working on Mac w/ Firefox though, couldnt find a reason why... see #25 |
||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div class="vertical-section-container centered"> | ||
<h3>basic activitychart demo (custom element)</h3> | ||
<demo-snippet> | ||
<template> | ||
<activitychart-basic primarycolor="#4a148c" accentcolor="#ffcc80" dataurl="https://open.hpi.de/api/v2/stats/course/timebased.json?metric=CourseActivityTimebased&course_id=7793dd60-9a2f-4f01-828a-0dce340d8acd&start_date=2016-09-05T24:00:00.000Z&end_date=2016-10-31T17:00:00.000Z" squares-per-column="24" min-value='1000'></activitychart-basic> | ||
<!-- <activitychart-basic primarycolor="#4a148c" accentcolor="#ffcc80" 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]' | ||
labels=' | ||
["5.9.2016, 22:00:00", "5.9.2016, 23:00:00", "6.9.2016, 00:00:00", "6.9.2016, 01:00:00", "6.9.2016, 02:00:00", "6.9.2016, 03:00:00", "6.9.2016, 04:00:00", "6.9.2016, 05:00:00", | ||
"6.9.2016, 06:00:00", "6.9.2016, 07:00:00", "6.9.2016, 08:00:00", "6.9.2016, 09:00:00", "6.9.2016, 10:00:00", "6.9.2016, 11:00:00", "6.9.2016, 12:00:00", "6.9.2016, 13:00:00", | ||
"6.9.2016, 14:00:00", "6.9.2016, 15:00:00", "6.9.2016, 16:00:00", "6.9.2016, 17:00:00", "6.9.2016, 18:00:00", "6.9.2016, 19:00:00", "6.9.2016, 20:00:00", "6.9.2016, 21:00:00", | ||
"6.9.2016, 22:00:00", "6.9.2016, 23:00:00", "7.9.2016, 00:00:00", "7.9.2016, 01:00:00", "7.9.2016, 02:00:00", "7.9.2016, 03:00:00", "7.9.2016, 04:00:00", "7.9.2016, 05:00:00", | ||
"7.9.2016, 06:00:00", "7.9.2016, 07:00:00", "7.9.2016, 08:00:00", "7.9.2016, 09:00:00", "7.9.2016, 10:00:00", "7.9.2016, 11:00:00", "7.9.2016, 12:00:00", "7.9.2016, 13:00:00", | ||
"7.9.2016, 14:00:00", "7.9.2016, 15:00:00", "7.9.2016, 16:00:00", "7.9.2016, 17:00:00", "7.9.2016, 18:00:00", "7.9.2016, 19:00:00", "7.9.2016, 20:00:00", "7.9.2016, 21:00:00"]'></activitychart-basic> --> | ||
</template> | ||
</demo-snippet> | ||
</div> | ||
</body> | ||
</html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that bloody issue...
see #26