diff --git a/src/app/d3Bindings/eventDistribution/_eventDistribution.scss b/src/app/d3Bindings/eventDistribution/_eventDistribution.scss index 388b4936..6d4de3a5 100644 --- a/src/app/d3Bindings/eventDistribution/_eventDistribution.scss +++ b/src/app/d3Bindings/eventDistribution/_eventDistribution.scss @@ -1,6 +1,6 @@ $mini-backgrounds: darksalmon darkolivegreen slategray #8f7f6f #7a6f8f #8f6f8f; -event-distribution-overview { +event-distribution-overview, event-distribution-detail { // purple only for debugging background-color: purple; @@ -9,6 +9,7 @@ event-distribution-overview { & svg { width: 100%; + fill: black; } & .chart { @@ -16,12 +17,12 @@ event-distribution-overview { background-color: #ffffff; } - & .mini { - fill: black; + .laneLines { + stroke: lightgray; + } + + .laneLinesGroup { - & text { - font: 12px sans-serif; - } } .brush .extent { @@ -42,4 +43,23 @@ event-distribution-overview { } +event-distribution-overview { + & .mini { + + } + + & text { + font: 12px sans-serif; + } +} + +event-distribution-detail { + & .main { + + } + + & text { + font: 14px sans-serif; + } +} diff --git a/src/app/d3Bindings/eventDistribution/distributionDetail.js b/src/app/d3Bindings/eventDistribution/distributionDetail.js index 6dfccbde..1b3caf40 100644 --- a/src/app/d3Bindings/eventDistribution/distributionDetail.js +++ b/src/app/d3Bindings/eventDistribution/distributionDetail.js @@ -24,27 +24,50 @@ angular laneLinesGroup, laneLabelsGroup, mainItemsGroup, + laneLabelMarginRight = 5, + xAxisHeight = 30, margin = { top: 5, right: 20, - bottom: 5, + bottom: 5 + xAxisHeight, left: 120 }, // these are initial values only // this is the width and height of the main group mainWidth = 1000, - mainHeight = 256; + mainHeight = 256, + laneHeight = 120; // exports this.updateData = updateData; + this.updateExtent = updateExtent; + this.items = []; + this.lanes = []; + this.minimum = null; + this.maximum = null; + this.visibleExtent = null; // init create(); // exported functions - function updateData() { + function updateData(data) { + updateDataVariables(data); + updateDimensions(); + + updateScales(); + + updateMain(); + } + + function updateExtent(extent) { + that.visibleExtent = extent; + + updateScales(); + + extentUpdateMain(); } // other functions @@ -59,6 +82,12 @@ angular .classed("chart", true); } + function updateDimensions() { + mainWidth = calculateMainWidth(); + mainHeight = Math.max(getLaneLength() * laneHeight, laneHeight); + chart.style("height", svgHeight()); + } + function createMain() { // create main surface main = chart.append("g") @@ -75,6 +104,97 @@ angular xAxis = new TimeAxis(main, xScale, {y: mainHeight}) } + + function updateDataVariables(data) { + // public field - share the reference + that.items = data.items || []; + that.lanes = data.lanes || []; + that.maximum = data.maximum; + that.minimum = data.minimum; + } + + function updateScales() { + xScale = d3.time.scale() + .domain(that.visibleExtent || [that.minimum, that.maximum]) + .range([0, mainWidth]); + + yScale = d3.scale.linear() + .domain([0, getLaneLength()]) + .range([0, mainHeight]); + } + + + function updateMain() { + + // separator lines between categories + function getSeparatorLineY(d, i) { + return yScale(i); + } + laneLinesGroup.selectAll() + .data(that.lanes) + .enter() + .append("line") + .attr({ + x1: 0, + y1: getSeparatorLineY, + x2: mainWidth, + y2: getSeparatorLineY, + class: "laneLines" + }); + + // lane labels + laneLabelsGroup.selectAll() + .data(that.lanes) + .enter() + .append("text") + .text(id) + .attr({ + x: -laneLabelMarginRight, + y: function (d, i) { + // 0.5 shifts it halfway into lane + return yScale(i + 0.5); + }, + dy: ".5ex", + "text-anchor": "end", + class: "laneText" + }); + + extentUpdateMain(); + } + + /** + * Called when the extent is updated to repaint rects + */ + function extentUpdateMain() { + + + // finally update the axis + if (data && data.items.length > 0) { + xAxis.update(xScale, [0, mainHeight]); + } + + } + + function isRectVisible(d) { + return dataFunctions.getLow(d) >= that.visibleExtent + || dataFunctions.getHigh() <= that.visibleExtent; + } + + function getLaneLength() { + return that.lanes && that.lanes.length || 0; + } + + function calculateMainWidth() { + return chart.node().getBoundingClientRect().width - margin.left - margin.right; + } + + function svgHeight() { + return mainHeight + margin.top + margin.bottom; + } + + function id(a) { + return a; + } } } ] @@ -86,12 +206,12 @@ angular // directive definition object return { restrict: "EA", - scope: {}, + scope: false, require: "^^eventDistribution", + controller: "distributionController", link: function ($scope, $element, attributes, controller, transcludeFunction) { var element = $element[0]; - - controller.detail = new DistributionDetail(element, {}, controller.dataFunctions); + controller.detail = new DistributionDetail(element, controller.data, controller.options.functions); } }; } diff --git a/src/app/d3Bindings/eventDistribution/distributionOverview.js b/src/app/d3Bindings/eventDistribution/distributionOverview.js index 6020c55e..b202631d 100644 --- a/src/app/d3Bindings/eventDistribution/distributionOverview.js +++ b/src/app/d3Bindings/eventDistribution/distributionOverview.js @@ -18,20 +18,20 @@ angular chart, mini, xAxis, - miniX, - miniY, + xScale, + yScale, // this default value will be overwritten almost immediately miniWidth = 1000, // this default value will be overwritten almost immediately miniHeight = 200, - xAxisHeight = 60, + xAxisHeight = 30, margin = { top: 5, right: 20, bottom: 5 + xAxisHeight, left: 120 }, - laneHeight = 80, + laneHeight = 60, lanePaddingDomain = 0.125, labelRectPadding = 5, container = d3.select(target), @@ -68,7 +68,6 @@ angular that.minimum = null; // init - console.debug("DistributionOverview:created", data); create(); // public functions @@ -81,7 +80,7 @@ angular updateDimensions(); mini = createMini(chart); - xAxis = new TimeAxis(mini, miniX, {y: miniHeight}); + xAxis = new TimeAxis(mini, xScale, {y: miniHeight}); } function updateData(data) { @@ -96,7 +95,7 @@ angular if (data && data.items.length > 0) { display(); - xAxis.update(miniX, [0, miniHeight]); + xAxis.update(xScale, [0, miniHeight]); } } @@ -161,7 +160,7 @@ angular .attr({ x: 0, // TODO: -m[1] === -15 y: function (d) { - return miniY(getCategoryIndex(d) + 0.5) + return yScale(getCategoryIndex(d) + 0.5) }, dy: ".5ex" }); @@ -169,8 +168,8 @@ angular // create interactive brush that.brush = d3.svg.brush() - // miniX is undefined at this point, it is updated later when data is added - .x(miniX) + // xScale is undefined at this point, it is updated later when data is added + .x(xScale) .on("brush", that.display); // create surface for the brush @@ -189,9 +188,9 @@ angular function updateDataVariables(data) { // public field - share the reference that.items = data.items || []; - that.lanes = d3.set(that.items.map(functions.getCategory)).values(); - that.maximum = Math.max.apply(null, that.items.map(functions.getHigh, functions)); - that.minimum = Math.min.apply(null, that.items.map(functions.getLow, functions)); + that.lanes = data.lanes || []; + that.maximum = data.maximum; + that.minimum = data.minimum; } /** @@ -199,15 +198,15 @@ angular */ function updateScales() { // a normal linear scale also works - miniX = d3.time.scale() + xScale = d3.time.scale() .domain([that.minimum, that.maximum]) .range([0, miniWidth]); - miniY = d3.scale.linear() + yScale = d3.scale.linear() .domain([0, that.getLaneLength()]) .range([0, miniHeight]); // update the brush - that.brush.x(miniX); + that.brush.x(xScale); } /** @@ -217,7 +216,7 @@ angular function updateMini(mini) { // separator lines between categories function getSeparatorLineY(d, i) { - return miniY(i); + return yScale(i); } mini.select(".laneLinesGroup") @@ -230,7 +229,6 @@ angular y1: getSeparatorLineY, x2: miniWidth, y2: getSeparatorLineY, - stroke: "lightgray", class: "laneLines" }); @@ -245,7 +243,7 @@ angular x: -labelRectPadding, y: function (d, i) { // 0.5 shifts it halfway into lane - return miniY(i + 0.5); + return yScale(i + 0.5); }, dy: ".5ex", "text-anchor": "end", @@ -258,15 +256,15 @@ angular return "miniItem" + getCategoryIndex(d); }, x: function (d) { - return miniX(functions.getLow(d)); + return xScale(functions.getLow(d)); }, y: function (d) { - return miniY(getCategoryIndex(d) + lanePaddingDomain); + return yScale(getCategoryIndex(d) + lanePaddingDomain); }, width: function (d) { - return miniX(functions.getHigh(d)) - miniX(functions.getLow(d)); + return xScale(functions.getHigh(d)) - xScale(functions.getLow(d)); }, - height: miniY(1.0 - (2 * lanePaddingDomain)) + height: yScale(1.0 - (2 * lanePaddingDomain)) }; mini.select(".miniItemsGroup") .selectAll() @@ -288,7 +286,6 @@ angular /** * Updates the brush's extent (positions of minimum and maximum) * and also updates the bounds as data (for binding). - * */ function display() { var brushExtent = that.brush.extent(); @@ -331,65 +328,12 @@ angular // directive definition object return { restrict: "EA", - scope: { - data: "=", - options: "=" - }, - // controller: "distributionController", + scope: false, + controller: "distributionController", require:"^^eventDistribution", link: function ($scope, $element, attributes, controller, transcludeFunction) { - var element = $element[0]; - $scope.options = $scope.options || {}; - - // TODO: refactor the functions, they should not be data specific in this component - var instance = new DistributionOverview(element, {items: $scope.data}, { - getId: function (d) { - return d.audioId; - }, - getCategory: function (d) { - return d.siteId; - }, - getLow: function (d) { - if ((typeof d.recordedDate) === "string") { - d.recordedDate = new Date(d.recordedDate); - d.minimumMilliseconds = d.recordedDate.getTime(); - } - return d.minimumMilliseconds; - }, - getHigh: function (d) { - if ((typeof d.durationSeconds) === "string") { - d.durationSeconds = Number(d.durationSeconds); - d.durationMilliseconds = d.durationSeconds * 1000; - } - return this.getLow(d) + d.durationMilliseconds; - }, - getText: function (d) { - return d.audioId; - }, - extentUpdate: function (newExtent) { - function update() { - $scope.options.overviewExtent = newExtent; - } - - if (!$scope.$root.$$phase) { - $scope.$apply(update); - } - else { - $scope.$eval(update); - } - } - }); - - console.debug("test", $scope.test); - - // only watches changes to object reference - $scope.$watch(function () { - return $scope.data; - }, function (newValue, oldValue) { - instance.updateData({items: $scope.data}); - }); - + controller.overview = new DistributionOverview(element, controller.data, controller.options.functions); } } } diff --git a/src/app/d3Bindings/eventDistribution/eventDistributionController.js b/src/app/d3Bindings/eventDistribution/eventDistributionController.js index de3f41c2..d4cffc90 100644 --- a/src/app/d3Bindings/eventDistribution/eventDistributionController.js +++ b/src/app/d3Bindings/eventDistribution/eventDistributionController.js @@ -4,20 +4,82 @@ angular "distributionController", [ "$scope", - function distributionController($scope, $element, $attrs) { + "$element", + "$attrs", + "d3", + function distributionController($scope, $element, $attrs, d3) { console.debug("event distribution controller:init"); + var that = this; $scope.test = "hello world"; this.test = function() { alert("hello world2"); + }; + + this.data = {}; + // object reference! + this.options = $scope.options || {}; + this.options.functions = this.options.functions || {}; + this.detail = null; + this.overview = null; + + this.options.functions.extentUpdate = function (newExtent) { + function update() { + // object reference! + that.options.overviewExtent = newExtent; + } + + that.detail.updateExtent(newExtent); + + if (!$scope.$root.$$phase) { + $scope.$apply(update); + } + else { + $scope.$eval(update); + } + }; + + + // only watches changes to object reference + $scope.$watch(function () { + return $scope.data; + }, function (newValue, oldValue) { + if (tryUpdateDataVariables(that.data, newValue, that.options.functions)) { + that.overview.updateData(that.data); + that.detail.updateData(that.data); + } + }); + + function tryUpdateDataVariables(data, newValue, functions) { + // public field - share the reference + if (!newValue) { + data.items = []; + data.lanes = []; + data.maximum = null; + data.minimum = null; + return false; + } + else { + data.items = newValue || []; + data.lanes = d3.set(data.items.map(functions.getCategory)).values(); + data.maximum = Math.max.apply(null, data.items.map(functions.getHigh, functions)); + data.minimum = Math.min.apply(null, data.items.map(functions.getLow, functions)); + return true; + } } + + } ]) .directive( "eventDistribution", function() { return { + scope: { + data: "=", + options: "=" + }, controller: "distributionController" } } diff --git a/src/app/d3Bindings/widgets/timeAxis.js b/src/app/d3Bindings/widgets/timeAxis.js index 16e57742..037b1312 100644 --- a/src/app/d3Bindings/widgets/timeAxis.js +++ b/src/app/d3Bindings/widgets/timeAxis.js @@ -29,7 +29,7 @@ angular // d3 should automatically work out the tick interval //.ticks(d3.time.month, 1) // TODO: provide a dynamic/multiscale set of time formats - .tickFormat(d3.time.format("%y-%m")) + //.tickFormat(d3.time.format("%y-%m")) .tickSize(options.tickSize || defaultTickSize) .tickPadding(options.tickPadding || defaultTickPadding); diff --git a/src/app/visualize/visualize.js b/src/app/visualize/visualize.js index 7ac6c387..8303cfe7 100644 --- a/src/app/visualize/visualize.js +++ b/src/app/visualize/visualize.js @@ -5,19 +5,43 @@ angular [ "$scope", "$http", - function($scope, $http) { + function ($scope, $http) { // testing data $scope.recordingData = []; - $http.get("assets/temp/dummyData.json").then(function(response) { + $http.get("assets/temp/dummyData.json").then(function (response) { $scope.recordingData = response.data; - }, function() { - console.error("loading dummy data failed", arguments); + }, function () { + console.error("loading dummy data failed", arguments); }); - $scope.overviewOptions = {}; - - + $scope.distributionOptions = { + functions: { + getId: function (d) { + return d.audioId; + }, + getCategory: function (d) { + return d.siteName; + }, + getLow: function (d) { + if ((typeof d.recordedDate) === "string") { + d.recordedDate = new Date(d.recordedDate); + d.minimumMilliseconds = d.recordedDate.getTime(); + } + return d.minimumMilliseconds; + }, + getHigh: function (d) { + if ((typeof d.durationSeconds) === "string") { + d.durationSeconds = Number(d.durationSeconds); + d.durationMilliseconds = d.durationSeconds * 1000; + } + return this.getLow(d) + d.durationMilliseconds; + }, + getText: function (d) { + return d.audioId; + } + } + }; // gridlines @@ -31,7 +55,7 @@ angular min: 0, step: 1000, height: 256, - labelFormatter: function(value, index, min, max) { + labelFormatter: function (value, index, min, max) { return (value / 1000).toFixed(1); }, title: "Frequency (KHz)" @@ -44,7 +68,7 @@ angular min: 0, step: 1, width: 1440, - labelFormatter: function(value, index, min, max) { + labelFormatter: function (value, index, min, max) { // show 'absolute' time.... i.e. seconds of the minute var offset = (value % 60); diff --git a/src/app/visualize/visualize.tpl.html b/src/app/visualize/visualize.tpl.html index 39aaae4b..aa684d9d 100644 --- a/src/app/visualize/visualize.tpl.html +++ b/src/app/visualize/visualize.tpl.html @@ -4,11 +4,11 @@