From 456b2316667ab2de6f4e95cd418d2da3bb93f0f6 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Wed, 10 Sep 2014 19:28:04 +1000 Subject: [PATCH] Added Terrain view. Completed dot view. --- src/app/app.js | 1 + .../d3Bindings/calendarView/calendarView.js | 2 +- src/app/d3Bindings/d3TestPage.tpl.html | 6 +- src/app/d3Bindings/dotView/_dotView.scss | 12 +- src/app/d3Bindings/dotView/dotView.js | 264 +++++++++++------- src/app/d3Bindings/dotView/dotView.tpl.html | 2 +- .../d3Bindings/terrainView/_terrainView.scss | 37 +++ src/app/d3Bindings/terrainView/terrainView.js | 217 ++++++++++++++ .../terrainView/terrainView.tpl.html | 1 + .../timelineView/_timelineView.scss | 14 +- .../d3Bindings/timelineView/timelineView.js | 2 +- .../timelineViewTemplate.tpl.html | 2 +- src/sass/application.tpl.scss | 1 + 13 files changed, 444 insertions(+), 117 deletions(-) create mode 100644 src/app/d3Bindings/terrainView/_terrainView.scss create mode 100644 src/app/d3Bindings/terrainView/terrainView.js create mode 100644 src/app/d3Bindings/terrainView/terrainView.tpl.html diff --git a/src/app/app.js b/src/app/app.js index 0d49407b..566eaca0 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -90,6 +90,7 @@ var app = angular.module('baw', 'bawApp.d3.calendarView', 'bawApp.d3.timelineView', 'bawApp.d3.dotView', + 'bawApp.d3.terrainView', 'bawApp.accounts', 'bawApp.annotationViewer', diff --git a/src/app/d3Bindings/calendarView/calendarView.js b/src/app/d3Bindings/calendarView/calendarView.js index 624d279d..56068f57 100644 --- a/src/app/d3Bindings/calendarView/calendarView.js +++ b/src/app/d3Bindings/calendarView/calendarView.js @@ -32,7 +32,7 @@ angular.module("bawApp.d3.calendarView", ["bawApp.d3"]) for(var i = 0;i firstYear) { diff --git a/src/app/d3Bindings/d3TestPage.tpl.html b/src/app/d3Bindings/d3TestPage.tpl.html index c8bc1973..5327a045 100644 --- a/src/app/d3Bindings/d3TestPage.tpl.html +++ b/src/app/d3Bindings/d3TestPage.tpl.html @@ -9,8 +9,12 @@

Timeline view

-

Dot view

+

Dot view Shows audio recordings per hour in a day for each year

+

Terrain view Zoomable with mouse wheel and pannable by dragging

+
+ +
\ No newline at end of file diff --git a/src/app/d3Bindings/dotView/_dotView.scss b/src/app/d3Bindings/dotView/_dotView.scss index 850218e7..33140d5f 100644 --- a/src/app/d3Bindings/dotView/_dotView.scss +++ b/src/app/d3Bindings/dotView/_dotView.scss @@ -1,7 +1,7 @@ -body{font-family: Arial, sans-serif;font-size:10px;} -.axis path,.axis line {fill: none;stroke:#b6b6b6;shape-rendering: crispEdges;} +#audioRecordingDots{font-family: Arial, sans-serif;font-size:10px;} +#audioRecordingDots .axis path,#audioRecordingDots .axis line {fill: none;stroke:#b6b6b6;shape-rendering: crispEdges;} /*.tick line{fill:none;stroke:none;}*/ -.tick text{fill:#999;} -g.journal.active{cursor:pointer;} -text.label{font-size:12px;font-weight:bold;cursor:pointer;} -text.value{font-size:12px;font-weight:bold;} \ No newline at end of file +#audioRecordingDots .tick text{fill:#999;} +#audioRecordingDots g.journal.active{cursor:pointer;} +#audioRecordingDots text.label{font-size:12px;font-weight:bold;cursor:pointer;} +#audioRecordingDots text.value{font-size:12px;font-weight:bold;} \ No newline at end of file diff --git a/src/app/d3Bindings/dotView/dotView.js b/src/app/d3Bindings/dotView/dotView.js index 1c05bc46..368e5ff1 100644 --- a/src/app/d3Bindings/dotView/dotView.js +++ b/src/app/d3Bindings/dotView/dotView.js @@ -6,110 +6,177 @@ angular.module("bawApp.d3.dotView", ["bawApp.d3"]) .directive("bawDotView", ["d3", "moment", function (d3, moment) { - var dataStore = []; - - // d3 functions - function truncate(str, maxLength, suffix) { - if(str.length > maxLength) { - str = str.substring(0, maxLength + 1); - str = str.substring(0, Math.min(str.length, str.lastIndexOf(" "))); - str = str + suffix; - } - return str; - } - - var margin = {top: 20, right: 200, bottom: 0, left: 20}, - width = 800, - height = 650; - - var start_year = 1970, - end_year = 2013; - - var c = d3.scale.category20c(); + function DotViewDetails(elementId, jsonResponse) { + var that = this; + that.elementId = elementId; + + that.items = []; + + // build data structure + angular.forEach(jsonResponse.data, function (value, key) { + // {"hoursOfDay": [[0,3],[1, 2], [2,6], [5, 1], ... [23, 1]], "year": 2012} + + // get start and end in +10 timezone + var start = moment(value.recordedDate).zone('+10:00'); + var end = moment(value.recordedDate).add('seconds', value.durationSeconds).zone('+10:00'); + + var startYear = start.year(); + var startHour = start.hour(); + var endHour = end.hour(); + + var minHour = Math.min(startHour, endHour); + var maxHour = Math.max(startHour, endHour); + for (var i = minHour; i <= maxHour; i++) { + var hour = i; + var foundYear = false; + angular.forEach(that.items, function (valueItem, keyItem) { + if (valueItem.year === startYear) { + foundYear = true; + var foundHour = false; + angular.forEach(valueItem.hoursOfDay, function (valueHours, keyHours) { + var existingHour = valueHours[0]; + if (hour === existingHour) { + foundHour = true; + // increment audioRecordingCount + found = true; + valueHours[1] += 1; + } + }); + + if (!foundHour) { + // add hour and count if it does not exist + valueItem.hoursOfDay.push([hour, 1]) + } + } + }); + + if (!foundYear) { + that.items.push({"year": startYear, "hoursOfDay": [ + [hour, 1] + ]}) + } + } + }); + + that.truncate = function truncate(str, maxLength, suffix) { + if (str.length > maxLength) { + str = str.substring(0, maxLength + 1); + str = str.substring(0, Math.min(str.length, str.lastIndexOf(" "))); + str = str + suffix; + } + return str; + }; + + that.mouseover = function mouseover(p) { + var g = d3.select(this).node().parentNode; + d3.select(g).selectAll("circle").style("display", "none"); + d3.select(g).selectAll("text.value").style("display", "block"); + }; - var x = d3.scale.linear() - .range([0, width]); + that.mouseout = function mouseout(p) { + var g = d3.select(this).node().parentNode; + d3.select(g).selectAll("circle").style("display", "block"); + d3.select(g).selectAll("text.value").style("display", "none"); + }; - var xAxis = d3.svg.axis() - .scale(x) - .orient("top"); + var margin = {top: 20, right: 200, bottom: 0, left: 20}, + width = 800, + height = 650; - var formatYears = d3.format("0000"); - xAxis.tickFormat(formatYears); + var firstHour = 0, + lastHour = 23; - var create = function create(data) { - var svg = d3.select("#audioRecordingDots").append("svg") - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .style("margin-left", margin.left + "px") - .append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + var c = d3.scale.category20c(); - x.domain([start_year, end_year]); - var xScale = d3.scale.linear() - .domain([start_year, end_year]) + var x = d3.scale.linear() .range([0, width]); - svg.append("g") - .attr("class", "x axis") - .attr("transform", "translate(0," + 0 + ")") - .call(xAxis); - - for (var j = 0; j < data.length; j++) { - var g = svg.append("g").attr("class","journal"); - - var circles = g.selectAll("circle") - .data(data[j]['articles']) - .enter() - .append("circle"); - - var text = g.selectAll("text") - .data(data[j]['articles']) - .enter() - .append("text"); - - var rScale = d3.scale.linear() - .domain([0, d3.max(data[j]['articles'], function(d) { return d[1]; })]) - .range([2, 9]); - - circles - .attr("cx", function(d, i) { return xScale(d[0]); }) - .attr("cy", j*20+20) - .attr("r", function(d) { return rScale(d[1]); }) - .style("fill", function(d) { return c(j); }); - - text - .attr("y", j*20+25) - .attr("x",function(d, i) { return xScale(d[0])-5; }) - .attr("class","value") - .text(function(d){ return d[1]; }) - .style("fill", function(d) { return c(j); }) - .style("display","none"); - - g.append("text") - .attr("y", j*20+25) - .attr("x",width+20) - .attr("class","label") - .text(truncate(data[j]['name'],30,"...")) - .style("fill", function(d) { return c(j); }) - .on("mouseover", mouseover) - .on("mouseout", mouseout); - } - - function mouseover(p) { - var g = d3.select(this).node().parentNode; - d3.select(g).selectAll("circle").style("display","none"); - d3.select(g).selectAll("text.value").style("display","block"); - } - - function mouseout(p) { - var g = d3.select(this).node().parentNode; - d3.select(g).selectAll("circle").style("display","block"); - d3.select(g).selectAll("text.value").style("display","none"); - } - }; - - + var xAxis = d3.svg.axis() + .scale(x) + .orient("top"); + + var formatYears = d3.format("00"); + xAxis.tickFormat(formatYears); + + that.createView = function createView(data) { + var svg = d3.select("#audioRecordingDots").append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .style("margin-left", margin.left + "px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + x.domain([firstHour, lastHour]); + var xScale = d3.scale.linear() + .domain([firstHour, lastHour]) + .range([0, width]); + + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + 0 + ")") + .call(xAxis); + + for (var j = 0; j < data.length; j++) { + var dataIndex = j; + var g = svg.append("g").attr("class", "journal"); + + var circles = g.selectAll("circle") + .data(data[j]['hoursOfDay']) + .enter() + .append("circle"); + + var text = g.selectAll("text") + .data(data[j]['hoursOfDay']) + .enter() + .append("text"); + + var rScale = d3.scale.linear() + .domain([0, d3.max(data[j]['hoursOfDay'], function (d) { + return d[1]; + })]) + .range([2, 9]); + + circles + .attr("cx", function (d, i) { + return xScale(d[0]); + }) + .attr("cy", j * 20 + 20) + .attr("r", function (d) { + return rScale(d[1]); + }) + .style("fill", function (d) { + return c(dataIndex); + }); + + text + .attr("y", j * 20 + 25) + .attr("x", function (d, i) { + return xScale(d[0]) - 5; + }) + .attr("class", "value") + .text(function (d) { + return d[1]; + }) + .style("fill", function (d) { + return c(dataIndex); + }) + .style("display", "none"); + + g.append("text") + .attr("y", j * 20 + 25) + .attr("x", width + 20) + .attr("class", "label") + .text(that.truncate(data[j]['year'], 30, "...")) + .style("fill", function (d) { + return c(dataIndex); + }) + .on("mouseover", that.mouseover) + .on("mouseout", that.mouseout); + } + }; + + that.createView(that.items); + } return { restrict: "EA", @@ -131,8 +198,7 @@ angular.module("bawApp.d3.dotView", ["bawApp.d3"]) return $scope.data; }, function (newValue, oldValue) { if (newValue) { - //updateCatalogueData(newValue.data); - create(dataStore); + $scope.details = new DotViewDetails('audioRecordingDots', newValue); } }); diff --git a/src/app/d3Bindings/dotView/dotView.tpl.html b/src/app/d3Bindings/dotView/dotView.tpl.html index 052ed8ab..d0ad24d6 100644 --- a/src/app/d3Bindings/dotView/dotView.tpl.html +++ b/src/app/d3Bindings/dotView/dotView.tpl.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/src/app/d3Bindings/terrainView/_terrainView.scss b/src/app/d3Bindings/terrainView/_terrainView.scss new file mode 100644 index 00000000..db56aea3 --- /dev/null +++ b/src/app/d3Bindings/terrainView/_terrainView.scss @@ -0,0 +1,37 @@ +#audioRecordingTerrain svg { + font-size: 10px; +} + +#audioRecordingTerrain .axis { + shape-rendering: crispEdges; +} + +#audioRecordingTerrain .axis path, #audioRecordingTerrain .axis line { + fill: none; + stroke-width: .5px; +} + +#audioRecordingTerrain .x.axis path { + stroke: #000; +} + +#audioRecordingTerrain .x.axis line { + stroke: #fff; + stroke-opacity: .5; +} + +#audioRecordingTerrain .y.axis line { + stroke: #ddd; +} + +#audioRecordingTerrain path.line { + fill: none; + stroke: #000; + stroke-width: .5px; +} + +#audioRecordingTerrain rect.pane { + cursor: move; + fill: none; + pointer-events: all; +} \ No newline at end of file diff --git a/src/app/d3Bindings/terrainView/terrainView.js b/src/app/d3Bindings/terrainView/terrainView.js new file mode 100644 index 00000000..2ed7bd69 --- /dev/null +++ b/src/app/d3Bindings/terrainView/terrainView.js @@ -0,0 +1,217 @@ +/** + * A d3 Terrain View directive + * Created by Mark on 10/09/2014. + * Based on http://neuralengr.com/asifr/journals/journals_dbs.html + */ +angular.module("bawApp.d3.terrainView", ["bawApp.d3"]) + .directive("bawTerrainView", ["d3", "moment", function (d3, moment) { + + function TerrainViewDetails(elementId, jsonResponse) { + var that = this; + that.elementId = elementId; + + that.items = {}; + + // build data structure + angular.forEach(jsonResponse.data, function (value, key) { + // minute resolution + // {"datetime": "2014-09-10 15:00:00", "value": 5} + + // get start and end in +10 timezone + var start = moment(value.recordedDate).zone('+10:00'); + var end = moment(value.recordedDate).add('seconds', value.durationSeconds).zone('+10:00'); + + var momentFormatString = 'YYYY-MM-DD HH:00:00'; + + // loop from start to end of recording, adding a minute each time. + var diff = end.diff(start.clone().startOf('hour'), 'hours'); + for(var step = 0;step<=diff;step++){ + + var current = start.clone().startOf('hour').add('hour', step); + var currentFormatted = current.format(momentFormatString); + if(!that.items[currentFormatted]){ + that.items[currentFormatted] = 1; + } else { + that.items[currentFormatted] += 1; + } + } + }); + + var m = [79, 80, 160, 79], + w = 1280 - m[1] - m[3], + h = 800 - m[0] - m[2], + parse = d3.time.format("%Y-%m-%d %H:%M:%S").parse, + format = d3.time.format("%Y"); + + // Scales. Note the inverted domain for the y-scale: bigger is up! + that.x = d3.time.scale().range([0, w]); + var y = d3.scale.linear().range([h, 0]), + xAxis = d3.svg.axis().scale(that.x).orient("bottom").tickSize(-h, 0).tickPadding(6), + yAxis = d3.svg.axis().scale(y).orient("right").tickSize(-w).tickPadding(6); + + that.createView = function createView(dataObject) { + + // Parse dates and numbers. + var data = []; + angular.forEach(dataObject, function (value, key) { + // d.datetime is already a Moment + data.push({ + datetime: parse(key), + value: +value + }); + }); + + // An area generator. + var area = d3.svg.area() + .interpolate("step-after") + .x(function (d) { + return that.x(d.datetime); + }) + .y0(y(0)) + .y1(function (d) { + return y(d.value); + }); + + // A line generator. + var line = d3.svg.line() + .interpolate("step-after") + .x(function (d) { + return that.x(d.datetime); + }) + .y(function (d) { + return y(d.value); + }); + + var svg = d3.select("#audioRecordingTerrain").append("svg:svg") + .attr("width", w + m[1] + m[3]) + .attr("height", h + m[0] + m[2]) + .append("svg:g") + .attr("transform", "translate(" + m[3] + "," + m[0] + ")"); + + var gradient = svg.append("svg:defs").append("svg:linearGradient") + .attr("id", "gradient") + .attr("x2", "0%") + .attr("y2", "100%"); + + gradient.append("svg:stop") + .attr("offset", "0%") + .attr("stop-color", "#fff") + .attr("stop-opacity", .5); + + gradient.append("svg:stop") + .attr("offset", "100%") + .attr("stop-color", "#999") + .attr("stop-opacity", 1); + + svg.append("svg:clipPath") + .attr("id", "clip") + .append("svg:rect") + .attr("x", that.x(0)) + .attr("y", y(1)) + .attr("width", that.x(1) - that.x(0)) + .attr("height", y(0) - y(1)); + + svg.append("svg:g") + .attr("class", "y axis") + .attr("transform", "translate(" + w + ",0)"); + + svg.append("svg:path") + .attr("class", "area") + .attr("clip-path", "url(#clip)") + .style("fill", "url(#gradient)"); + + svg.append("svg:g") + .attr("class", "x axis") + .attr("transform", "translate(0," + h + ")"); + + svg.append("svg:path") + .attr("class", "line") + .attr("clip-path", "url(#clip)"); + + svg.append("svg:rect") + .attr("class", "pane") + .attr("width", w) + .attr("height", h) + .call(d3.behavior.zoom().on("zoom", zoom)); + + function update(data) { + + // Compute the maximum price. + that.x.domain([d3.max(data, function (d) { + return d.datetime; + }), d3.max(data, function (d) { + return d.datetime; + })]); + y.domain([0, d3.max(data, function (d) { + return d.value; + })]); + + // Bind the data to our path elements. + svg.select("path.area").data([data]); + svg.select("path.line").data([data]); + + draw(); + } + + function draw() { + svg.select("g.x.axis").call(xAxis); + svg.select("g.y.axis").call(yAxis); + svg.select("path.area").attr("d", area); + svg.select("path.line").attr("d", line); + d3.select("#footer span").text("U.S. Commercial Flights, " + that.x.domain().map(format).join("-")); + } + + function zoom() { + d3.event.transform(x); // TODO d3.behavior.zoom should support extents + draw(); + } + + update(data); + }; + + that.createView(that.items); + } + + + return { + restrict: "EA", + scope: { + data: "=" + }, + templateUrl: "d3Bindings/terrainView/terrainView.tpl.html", + link: function ($scope, $element, attributes, controller, transcludeFunction) { + + // use this function to bind DOM events to angular scope + // or d3 events to angular scope. + // you can use the jQuery / d3 objects here (use the injected d3 instance) + + // where possible avoid jQuery + var element = $element[0]; + + // watch for changes on scope data + $scope.$watch(function () { + return $scope.data; + }, function (newValue, oldValue) { + if (newValue) { + //createView(); + $scope.details = new TerrainViewDetails('audioRecordingTerrain', newValue); + } + }); + + }, + controller: "bawTerrainViewController" + }; + }]) + .controller("bawTerrainViewController", ["$scope", "$element", "$attrs", + function ($scope, $element, $attrs) { + // The controller should host functionality native to angular + // e.g. + // - functions for button clicks + // - API calls (not relevant in this case) + // - scope modification + // - iteraction with other services/providers + // IT SHOULD NOT contain any reference to the d3 or jQuery objects + + //$scope.example = "Hello world!"; + + }]); \ No newline at end of file diff --git a/src/app/d3Bindings/terrainView/terrainView.tpl.html b/src/app/d3Bindings/terrainView/terrainView.tpl.html new file mode 100644 index 00000000..6ff84007 --- /dev/null +++ b/src/app/d3Bindings/terrainView/terrainView.tpl.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/app/d3Bindings/timelineView/_timelineView.scss b/src/app/d3Bindings/timelineView/_timelineView.scss index b53f20e2..eed6bcc2 100644 --- a/src/app/d3Bindings/timelineView/_timelineView.scss +++ b/src/app/d3Bindings/timelineView/_timelineView.scss @@ -1,33 +1,33 @@ -.chart { +#audioRecordingTimeline .chart { shape-rendering: crispEdges; } -.mini text { +#audioRecordingTimeline .mini text { font: 9px sans-serif; } -.main text { +#audioRecordingTimeline .main text { font: 12px sans-serif; } -.miniItem0 { +#audioRecordingTimeline .miniItem0 { fill: darksalmon; stroke-width: 6; } -.miniItem1 { +#audioRecordingTimeline .miniItem1 { fill: darkolivegreen; fill-opacity: .7; stroke-width: 6; } -.miniItem2 { +#audioRecordingTimeline .miniItem2 { fill: slategray; fill-opacity: .7; stroke-width: 6; } -.brush .extent { +#audioRecordingTimeline .brush .extent { stroke: gray; fill: dodgerblue; fill-opacity: .365; diff --git a/src/app/d3Bindings/timelineView/timelineView.js b/src/app/d3Bindings/timelineView/timelineView.js index 65ff348a..f3fd75e5 100644 --- a/src/app/d3Bindings/timelineView/timelineView.js +++ b/src/app/d3Bindings/timelineView/timelineView.js @@ -324,7 +324,7 @@ angular.module("bawApp.d3.timelineView", ["bawApp.d3"]) }, function (newValue, oldValue) { if (newValue) { - $scope.details = new Details('audioRecordingTimelineContainer', newValue); + $scope.details = new Details('audioRecordingTimeline', newValue); } }); diff --git a/src/app/d3Bindings/timelineView/timelineViewTemplate.tpl.html b/src/app/d3Bindings/timelineView/timelineViewTemplate.tpl.html index 7e6898c2..3d067494 100644 --- a/src/app/d3Bindings/timelineView/timelineViewTemplate.tpl.html +++ b/src/app/d3Bindings/timelineView/timelineViewTemplate.tpl.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/src/sass/application.tpl.scss b/src/sass/application.tpl.scss index b5111360..f76d46dd 100644 --- a/src/sass/application.tpl.scss +++ b/src/sass/application.tpl.scss @@ -39,6 +39,7 @@ $DEBUG: '<%= build_configs.current.key === "development" %>' == 'true'; @import "../app/d3Bindings/calendarView/calendarView"; @import "../app/d3Bindings/timelineView/timelineView"; @import "../app/d3Bindings/dotView/dotView"; +@import "../app/d3Bindings/terrainView/terrainView"; @import "../app/home/home"; @import "../app/listen/listen"; @import "../app/login/login_control";