From cc7eb9ccd9034db09c87bb6001c974d6e2dcf089 Mon Sep 17 00:00:00 2001
From: Markus Grigull
Date: Thu, 22 Oct 2015 10:13:24 -0400
Subject: [PATCH] Add d3 library and d3-gauge component
---
app/components/d3-gauge.js | 257 ++++++++++++++++++
app/controllers/lab-mashup.js | 2 +
app/templates/components/d3-gauge.hbs | 1 +
app/templates/lab-mashup.hbs | 2 +
bower.json | 3 +-
ember-cli-build.js | 1 +
tests/integration/components/d3-gauge-test.js | 26 ++
7 files changed, 291 insertions(+), 1 deletion(-)
create mode 100644 app/components/d3-gauge.js
create mode 100644 app/templates/components/d3-gauge.hbs
create mode 100644 tests/integration/components/d3-gauge-test.js
diff --git a/app/components/d3-gauge.js b/app/components/d3-gauge.js
new file mode 100644
index 0000000..28fb1f7
--- /dev/null
+++ b/app/components/d3-gauge.js
@@ -0,0 +1,257 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: 'span',
+ classNames: ['gauge'],
+
+ size: 120,
+ minValue: 0,
+ maxValue: 100,
+ value: 0,
+ minorTicks: 2,
+ majorTicks: 5,
+ label: '',
+ greenColor: '#109618',
+ yellowColor: '#FF9900',
+ redColor: '#DC3912',
+ greenZones: [],
+ yellowZones: [],
+ redZones: [],
+
+ didInsertElement: function() {
+ this._drawGauge();
+ },
+
+ _drawGauge: function() {
+ // calculate dimensions
+ var cx = this.size / 2;
+ var radius = this.size / 2 * 0.97;
+ var labelFontSize = Math.round(this.size / 9);
+ var fontSize = Math.round(this.size / 16);
+ var range = this.maxValue - this.minValue;
+ var majorDelta = range / (this.majorTicks - 1);
+ var minorDelta = majorDelta / this.minorTicks;
+ var midValue = (this.minValue + this.maxValue) / 2;
+ var pointerFontSize = Math.round(this.size / 10);
+
+ // create body element
+ var body = d3.select('#' + this.elementId).append("svg:svg");
+ this.set('svgBody', body);
+
+ // base circles
+ body.append("svg:circle")
+ .attr("cx", cx)
+ .attr("cy", cx)
+ .attr("r", radius)
+ .style("fill", "#ccc")
+ .style("stroke", "#000")
+ .style("stroke-width", "0.5px");
+
+ body.append("svg:circle")
+ .attr("cx", cx)
+ .attr("cy", cx)
+ .attr("r", radius * 0.9)
+ .style("fill", "#fff")
+ .style("stroke", "#e0e0e0")
+ .style("stroke-width", "2px");
+
+ // color zones
+ for (var index in this.greenZones) {
+ this.drawBand(this.greenZones[index].from, this.greenZones[index].to, this.greenColor);
+ }
+
+ for (var index in this.yellowZones) {
+ this.drawBand(this.yellowZones[index].from, this.yellowZones[index].to, this.yellowColor);
+ }
+
+ for (var index in this.redZones) {
+ var zone = this.redZones[index];
+ console.log(zone);
+ this.drawBand(this.redZones[index].from, this.redZones[index].to, this.redColor);
+ }
+
+ // label
+ if (this.label) {
+ body.append("svg:text")
+ .attr("x", cx)
+ .attr("y", cx / 2 + labelFontSize / 2)
+ .attr("dy", labelFontSize / 2)
+ .attr("text-anchor", "middle")
+ .text(this.label)
+ .style("font-size", labelFontSize + "px")
+ .style("fill", "#333")
+ .style("stroke-width", "0px");
+ }
+
+ // ticks
+ for (var major = this.minValue; major <= this.maxValue; major += majorDelta) {
+ for (var minor = major + minorDelta; minor < Math.min(major + majorDelta, this.maxValue); minor += minorDelta) {
+ var p1 = this.valueToPoint(minor, 0.75);
+ var p2 = this.valueToPoint(minor, 0.85);
+
+ body.append("svg:line")
+ .attr("x1", p1.x)
+ .attr("y1", p1.y)
+ .attr("x2", p2.x)
+ .attr("y2", p2.y)
+ .style("stroke", "#666")
+ .style("stroke-width", "1px");
+ }
+
+ var p1 = this.valueToPoint(major, 0.7);
+ var p2 = this.valueToPoint(major, 0.85);
+
+ body.append("svg:line")
+ .attr("x1", p1.x)
+ .attr("y1", p1.y)
+ .attr("x2", p2.x)
+ .attr("y2", p2.y)
+ .style("stroke", "#333")
+ .style("stroke-width", "2px");
+
+ if (major == this.minValue || major == this.maxValue) {
+ var point = this.valueToPoint(major, 0.63);
+
+ body.append("svg:text")
+ .attr("x", point.x)
+ .attr("y", point.y)
+ .attr("dy", fontSize / 3)
+ .attr("text-anchor", major == this.minValue ? "start" : "end")
+ .text(major)
+ .style("font-size", fontSize + "px")
+ .style("fill", "#333")
+ .style("stroke-width", "0px");
+ }
+ }
+
+ // pointer
+ var pointerContainer = body.append("svg:g").attr("class", "pointerContainer");
+ var pointerPath = this.buildPointerPath(midValue);
+ var pointerLine = d3.svg.line()
+ .x(function(d) { return d.x })
+ .y(function(d) { return d.y })
+ .interpolate("basis");
+
+ pointerContainer.selectAll("path")
+ .data([pointerPath])
+ .enter()
+ .append("svg:path")
+ .attr("d", pointerLine)
+ .style("fill", "#dc3912")
+ .style("stroke", "#c63310")
+ .style("fill-opacity", 0.7);
+
+ pointerContainer.append("svg:circle")
+ .attr("cx", cx)
+ .attr("cy", cx)
+ .attr("r", radius * 0.12)
+ .style("fill", "#4684EE")
+ .style("stroke", "#666")
+ .style("opacity", 1);
+
+ pointerContainer.selectAll("text")
+ .data([midValue])
+ .enter()
+ .append("svg:text")
+ .attr("x", cx)
+ .attr("y", this.size - cx / 4 - pointerFontSize)
+ .attr("dy", pointerFontSize / 2)
+ .attr("text-anchor", "middle")
+ .style("font-size", pointerFontSize + "px")
+ .style("fill", "#000")
+ .style("stroke-width", "0px");
+
+ this._redraw(this.value, 0);
+ },
+
+ _redraw: function(value, transitionDuration) {
+ var pointerContainer = this.svgBody.select(".pointerContainer");
+ pointerContainer.selectAll("text").text(Math.round(value));
+
+ var pointer = pointerContainer.selectAll("path");
+ var _this = this;
+
+ pointer.transition()
+ .duration(transitionDuration)
+ .attrTween("transform", function() {
+ var pointerValue = value;
+ if (value > _this.maxValue) {
+ pointerValue = _this.maxValue + 0.02 * (_this.maxValue - _this.minValue);
+ } else if (value < _this.minValue) {
+ pointerValue = _this.minValue - 0.02 * (_this.maxValue - _this.minValue);
+ }
+
+ var targetRotation = _this.valueToDegrees(pointerValue) - 90;
+ var currentRotation = _this._currentRotation || targetRotation;
+ _this._currentRotation = targetRotation;
+
+ return function(step) {
+ var rotation = currentRotation + (targetRotation - currentRotation) * step;
+ return "translate(" + (_this.size / 2) + ", " + (_this.size / 2) + ") rotate(" + rotation + ")";
+ }
+ });
+ }.observes('value'),
+
+ drawBand: function(start, end, color) {
+ if (0 >= end - start) {
+ return;
+ }
+
+ var _this = this;
+
+ this.svgBody.append("svg:path")
+ .style("fill", color)
+ .attr("d", d3.svg.arc()
+ .startAngle(this.valueToRadians(start))
+ .endAngle(this.valueToRadians(end))
+ .innerRadius(0.65 * (this.size / 2 * 0.97))
+ .outerRadius(0.85 * (this.size / 2 * 0.97)))
+ .attr("transform", function() {
+ return "translate(" + (_this.size / 2) + ", " + (_this.size / 2) + ") rotate(270)";
+ });
+ },
+
+ buildPointerPath: function(value) {
+ var delta = (this.maxValue - this.minValue) / 13;
+ var head = this.valueToPoint(value, 0.85);
+ var head1 = this.valueToPoint(value - delta, 0.12);
+ var head2 = this.valueToPoint(value + delta, 0.12);
+
+ var cx = this.size / 2;
+ head.x -= cx;
+ head.y -= cx;
+ head1.x -= cx;
+ head1.y -= cx;
+ head2.x -= cx;
+ head2.y -= cx;
+
+ var tailValue = value - ((this.maxValue - this.minValue) * (1/(270/360)) / 2);
+ var tail = this.valueToPoint(tailValue, 0.28);
+ var tail1 = this.valueToPoint(tailValue - delta, 0.12);
+ var tail2 = this.valueToPoint(tailValue + delta, 0.12);
+
+ tail.x -= cx;
+ tail.y -= cx;
+ tail1.x -= cx;
+ tail1.y -= cx;
+ tail2.x -= cx;
+ tail2.y -= cx;
+
+ return [head, head1, tail2, tail, tail1, head2, head];
+ },
+
+ valueToPoint: function(value, factor) {
+ return {
+ x: (this.size / 2) - (this.size / 2 * 0.97) * factor * Math.cos(this.valueToRadians(value)),
+ y: (this.size / 2) - (this.size / 2 * 0.97) * factor * Math.sin(this.valueToRadians(value))
+ };
+ },
+
+ valueToRadians: function(value) {
+ return this.valueToDegrees(value) * Math.PI / 180;
+ },
+
+ valueToDegrees: function(value) {
+ return value / (this.maxValue - this.minValue) * 270 - (this.minValue / (this.maxValue - this.minValue) * 270 + 45);
+ }
+});
diff --git a/app/controllers/lab-mashup.js b/app/controllers/lab-mashup.js
index cb6c094..2c7233a 100644
--- a/app/controllers/lab-mashup.js
+++ b/app/controllers/lab-mashup.js
@@ -2,6 +2,8 @@ import Ember from 'ember';
export default Ember.Controller.extend({
state: 1,
+ redZone: [{from: 50, to: 60}],
+ yellowZone: [{from: 40, to: 50}],
dataSetOne: [
{
diff --git a/app/templates/components/d3-gauge.hbs b/app/templates/components/d3-gauge.hbs
new file mode 100644
index 0000000..889d9ee
--- /dev/null
+++ b/app/templates/components/d3-gauge.hbs
@@ -0,0 +1 @@
+{{yield}}
diff --git a/app/templates/lab-mashup.hbs b/app/templates/lab-mashup.hbs
index 216973f..26cb868 100644
--- a/app/templates/lab-mashup.hbs
+++ b/app/templates/lab-mashup.hbs
@@ -32,6 +32,8 @@
Electric power capacity: 8458 MW
+
+ {{d3-gauge label="Frequency" value=23 maxValue=60 minorTicks=4 redZones=redZone yellowZones=yellowZone}}
diff --git a/bower.json b/bower.json
index 3b4db98..f339e8d 100644
--- a/bower.json
+++ b/bower.json
@@ -17,7 +17,8 @@
"Faker": "~3.0.0",
"bootstrap": "~3.3.2",
"flot": "*",
- "normalize.css": "3.0.3"
+ "normalize.css": "3.0.3",
+ "d3": "~3.5.6"
},
"resolutions": {
"ember": "^2.0.0"
diff --git a/ember-cli-build.js b/ember-cli-build.js
index a8d7780..79ba103 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -20,6 +20,7 @@ module.exports = function(defaults) {
// along with the exports of each module as its value.
app.import('bower_components/flot/jquery.flot.time.js');
+ app.import('bower_components/d3/d3.js');
return app.toTree();
};
diff --git a/tests/integration/components/d3-gauge-test.js b/tests/integration/components/d3-gauge-test.js
new file mode 100644
index 0000000..05120e4
--- /dev/null
+++ b/tests/integration/components/d3-gauge-test.js
@@ -0,0 +1,26 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('d3-gauge', 'Integration | Component | d3 gauge', {
+ integration: true
+});
+
+test('it renders', function(assert) {
+ assert.expect(2);
+
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.on('myAction', function(val) { ... });
+
+ this.render(hbs`{{d3-gauge}}`);
+
+ assert.equal(this.$().text().trim(), '');
+
+ // Template block usage:
+ this.render(hbs`
+ {{#d3-gauge}}
+ template block text
+ {{/d3-gauge}}
+ `);
+
+ assert.equal(this.$().text().trim(), 'template block text');
+});
|