diff --git a/app/icons/fuel.svg b/app/icons/fuel.svg new file mode 100755 index 00000000..69ff2074 --- /dev/null +++ b/app/icons/fuel.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/js/controllers/controller-outfit.js b/app/js/controllers/controller-outfit.js index d0e480ee..b51880ce 100755 --- a/app/js/controllers/controller-outfit.js +++ b/app/js/controllers/controller-outfit.js @@ -1,4 +1,4 @@ -angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$state', '$stateParams', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', function ($rootScope, $scope, $state, $p, Ships, Ship, Components, Serializer, Persist) { +angular.module('app').controller('OutfitController', ['$window','$rootScope','$scope', '$state', '$stateParams', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', function ($window, $rootScope, $scope, $state, $p, Ships, Ship, Components, Serializer, Persist) { var data = Ships[$p.shipId]; // Retrieve the basic ship properties, slots and defaults var ship = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship instance @@ -26,16 +26,22 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s $scope.selectedSlot = null; $scope.savedCode = Persist.getBuild(ship.id, $scope.buildName); $scope.canSave = Persist.isEnabled(); + $scope.fuel = 0; $scope.jrSeries = { - xMin: ship.unladenMass, - xMax: ship.ladenMass, - func: ship.jumpRangeWithMass.bind(ship) + xMin: 0, + xMax: ship.cargoCapacity, + // Slightly higher than actual based bacuse components are excluded + yMax: ship.jumpRangeWithMass(ship.unladenMass), + yMin: 0, + func: function(cargo) { // X Axis is Cargo + return ship.jumpRangeWithMass(ship.unladenMass + $scope.fuel + cargo, $scope.fuel); + } }; $scope.jrChart = { labels: { xAxis: { - title:'Ship Mass', + title:'Cargo', unit: 'T' }, yAxis: { @@ -156,11 +162,17 @@ angular.module('app').controller('OutfitController', ['$rootScope','$scope', '$s ship.updateTotals(); }; + $scope.fuelChange = function (fuel) { + $scope.fuel = fuel; + angular.element($window).triggerHandler('render'); + } + // Utilify functions function updateState() { $state.go('outfit', {shipId: ship.id, code: $scope.code, bn: $scope.buildName}, {location:'replace', notify:false}); - $scope.jrSeries.xMin = ship.unladenMass; - $scope.jrSeries.xMax = ship.ladenMass; + $scope.jrSeries.xMax = ship.cargoCapacity; + $scope.jrSeries.yMax = ship.jumpRangeWithMass(ship.unladenMass); + $scope.jrSeries.mass = ship.unladenMass; } // Hide any open menu/slot/etc if escape key is pressed diff --git a/app/js/directives/directive-area-chart.js b/app/js/directives/directive-area-chart.js index 0d92d1ae..918a2a0e 100755 --- a/app/js/directives/directive-area-chart.js +++ b/app/js/directives/directive-area-chart.js @@ -5,9 +5,7 @@ angular.module('app').directive('areaChart', ['$window', function ($window) { restrict: 'A', scope:{ config: '=', - series: '=', - height: '=', - width: '=' + series: '=' }, link: function(scope, element) { var series = scope.series, @@ -15,15 +13,17 @@ angular.module('app').directive('areaChart', ['$window', function ($window) { labels = config.labels, margin = {top: 15, right: 15, bottom: 35, left: 50}, fmt = d3.format('.3r'), - fmtLong = d3.format('.2f'); + fmtLong = d3.format('.2f'), + // Define Axes + xAxis = d3.svg.axis().outerTickSize(0).orient("bottom").tickFormat(d3.format('.2r')), + yAxis = d3.svg.axis().outerTickSize(0).orient("left").tickFormat(fmt) + x = d3.scale.linear(), + y = d3.scale.linear(); // Create chart var svg = d3.select(element[0]).append("svg"); var vis = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - // Define Axes - var xAxis = d3.svg.axis().outerTickSize(0).orient("bottom").tickFormat(fmt); - var yAxis = d3.svg.axis().outerTickSize(0).orient("left").tickFormat(fmt); // Define Area var area = d3.svg.area(); @@ -60,43 +60,47 @@ angular.module('app').directive('areaChart', ['$window', function ($window) { // Create and Add tooltip var tip = vis.append("g").style("display", "none"); + tip.append("rect").attr("width","4em").attr("height", "2em").attr("x", "0.5em").attr("y","-1em").attr("class","tip"); tip.append("circle") .attr("class", "marker") .attr("r", 4); - tip.append("text").attr("class", 'label x').attr("y", -2); + tip.append("text").attr("class", 'label x').attr("y", "-0.1em"); tip.append("text").attr("class", 'label y').attr("y", '0.7em'); /** * Watch for changes in the series data (mass changes, etc) */ scope.$watchCollection('series', render); - scope.$watchCollection('config.watch', render); - angular.element($window).bind('resize', render); + angular.element($window).bind('resize render', render); function render() { var width = element[0].parentElement.offsetWidth, height = width * 0.6, w = width - margin.left - margin.right, h = height - margin.top - margin.bottom, - x = d3.scale.linear().range([0, w]), - y = d3.scale.linear().range([h, 0]), data = [], func = series.func; - for (var d = series.xMin; d <= series.xMax; d++) { - data.push([ d, func(d) ]); + if (series.xMax == series.xMin) { + var yVal = func(series.xMin); + data.push([ series.xMin, yVal ]); + data.push([ series.xMin, yVal ]); + area.x(function(d,i) { return i * w; }).y0(h).y1(function(d) { return y(d[1]); }); + } else { + for (var d = series.xMin; d <= series.xMax; d += 1) { + data.push([ d, func(d) ]); + } + area.x(function(d) { return x(d[0]); }).y0(h).y1(function(d) { return y(d[1]); }); } // Update Chart Size svg.attr("width", width).attr("height", height); - area.x(function(d) { return x(d[0]); }).y0(h).y1(function(d) { return y(d[1]); }); - // Update domain and scale for axes; - x.domain([series.xMin, series.xMax]); + x.range([0, w]).domain([series.xMin, series.xMax]); xAxis.scale(x); xLbl.attr("transform", "translate(0," + h + ")"); xTxt.attr("x", w/2); - y.domain([data[data.length - 1][1], data[0][1]]); + y.range([h, 0]).domain([series.yMin, series.yMax]); yAxis.scale(y); yTxt.attr("x", -h/2); vis.selectAll(".y.axis").call(yAxis); @@ -114,8 +118,9 @@ angular.module('app').directive('areaChart', ['$window', function ($window) { .on("mouseout", function() { tip.style("display", "none"); }) .on('mousemove', function() { var xPos = d3.mouse(this)[0], x0 = x.invert(xPos), y0 = func(x0), flip = (xPos > w * 0.75); - tip.attr("transform", "translate(" + x(x0) + "," + y(y0) + ")"); - tip.selectAll('text.label').attr("x", flip? -10 : 10).style("text-anchor", flip? 'end' : 'start'); + tip.attr("transform", "translate(" + xPos + "," + y(y0) + ")"); + tip.selectAll('rect').attr("x", flip? '-4.5em' : "0.5em").style("text-anchor", flip? 'end' : 'start'); + tip.selectAll('text.label').attr("x", flip? "-1em" : "1em").style("text-anchor", flip? 'end' : 'start'); tip.select('text.label.x').text(fmtLong(x0) + ' ' + labels.xAxis.unit); tip.select('text.label.y').text(fmtLong(y0) + ' ' + labels.yAxis.unit); }); diff --git a/app/js/directives/directive-slider.js b/app/js/directives/directive-slider.js new file mode 100644 index 00000000..f1f33369 --- /dev/null +++ b/app/js/directives/directive-slider.js @@ -0,0 +1,74 @@ +angular.module('app').directive('slider', ['$window', function ($window) { + + return { + restrict: 'A', + scope:{ + max: '=', + unit: '=', + change: '&onChange' + }, + link: function(scope, element) { + var margin = {top: -10, right: 130, bottom: 0, left: 50}, + height = 40, // Height is fixed + h = height - margin.top - margin.bottom, + fmt = d3.format('.2f'), + pct = d3.format('.1%'), + unit = scope.unit, + val = scope.max, + svg = d3.select(element[0]).append("svg"), + vis = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"), + xAxis = vis.append("g").attr("class", "x slider-axis").attr("transform", "translate(0," + h / 2 + ")"), + x = d3.scale.linear(), + slider = vis.append("g").attr("class", "slider"), + filled = slider.append('path').attr('class', 'filled').attr("transform", "translate(0," + h/2 + ")"), + brush = d3.svg.brush().x(x).extent([scope.max, scope.max]).on("brush", brushed), + handle = slider.append("circle").attr("class", "handle").attr("r", '0.75em'), + lbl = slider.append("g").append("text").attr("y", h/2); + + slider.call(brush); + slider.select(".background").attr("height", h); + handle.attr("transform", "translate(0," + h / 2 + ")"); + + /** + * Watch for changes in the max, window size + */ + scope.$watch('max', function(newMax, oldMax) { + val = newMax * (val / oldMax); // Retain percentage filled + render(); + }); + + angular.element($window).bind('resize', render); + + function render() { + var width = element[0].offsetWidth, w = width - margin.left - margin.right; + + svg.attr("width", width).attr("height", height); + x.domain([0, scope.max]).range([0, w]).clamp(true); + handle.attr("cx", x(val)); + xAxis + .call(d3.svg.axis() + .scale(x) + .orient("bottom") + .tickFormat(function(d) { return d + unit; }) + .tickSize(0) + .tickPadding(12)) + .select(".domain"); + lbl.attr('x', w + 20).text(fmt(val) + ' ' + unit + ' (' + pct(val / scope.max) + ')'); + slider.call(brush.extent([val, val])).call(brush.event); + slider.selectAll(".extent,.resize").remove(); + } + + function brushed() { + val = brush.extent()[0]; + if (d3.event.sourceEvent) { // not a programmatic event + val = x.invert(d3.mouse(this)[0]); + brush.extent([val, val]); + } + lbl.text(fmt(val) + ' ' + unit + ' (' + pct(val / scope.max) + ')'); + scope.change({val: val}); + handle.attr("cx", x(val)); + filled.attr("d", "M0,0V0H" + x(val) + "V0"); + } + } + }; +}]); \ No newline at end of file diff --git a/app/less/app.less b/app/less/app.less index d1aded3f..b123513f 100755 --- a/app/less/app.less +++ b/app/less/app.less @@ -13,6 +13,7 @@ @import 'select'; @import 'modal'; @import 'charts'; +@import 'slider'; @import 'chart-tooltip'; @import 'buttons'; @import 'error'; diff --git a/app/less/charts.less b/app/less/charts.less index 908bf621..da0a150b 100755 --- a/app/less/charts.less +++ b/app/less/charts.less @@ -12,37 +12,44 @@ cursor: pointer; } } +} - svg { +svg { - .axis { - - line, path { - fill: none; - stroke: @primary-disabled; - } - - text { - font-size: 0.8em; - fill: @primary-disabled; - } - - &.y { - text tspan:first-child { - fill: @primary; - } - } + .axis { + line, path { + fill: none; + stroke: @primary-disabled; } - .marker { - fill: @secondary; + text { + font-size: 0.8em; + fill: @primary-disabled; } - .label { - font-size: 0.9em; - fill: @fg; + &.y { + text tspan:first-child { + fill: @primary; + } } + + } + + .marker { + fill: @secondary; + } + + .label { + font-size: 0.9em; + fill: @fg; + } + + .tip { + fill: @bgBlack; + stroke: @secondary; + stroke-width: 1px; } } + diff --git a/app/less/slider.less b/app/less/slider.less new file mode 100644 index 00000000..f7a9f19f --- /dev/null +++ b/app/less/slider.less @@ -0,0 +1,42 @@ + +.slider-axis { + line, path { + fill: none; + stroke: @primary-disabled; + } + + text { + font-size: 0.8em; + fill: @primary-disabled; + } + + + .domain { + fill: none; + stroke: @primary; + stroke-opacity: .3; + stroke-width: 0.7em; + stroke-linecap: round; + } + +} + +.slider { + + text { + dominant-baseline: central; + fill: @primary; + } + + .filled { + stroke-width: 0.3em; + stroke-linecap: round; + stroke: @primary-disabled; + } + + .handle { + fill: @primary; + stroke-opacity: .5; + cursor: crosshair; + } +} diff --git a/app/views/page-outfit.html b/app/views/page-outfit.html index 9e402fc4..95b95126 100755 --- a/app/views/page-outfit.html +++ b/app/views/page-outfit.html @@ -220,6 +220,9 @@
Jump Range
+
+ +