diff --git a/.gitignore b/.gitignore index 1bbc61e9..0dee7465 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ nginx.pid .idea /bin env +*.swp diff --git a/ChangeLog.md b/ChangeLog.md index 9da99a12..cd9a709b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -8,6 +8,11 @@ * Show integrity value for relevant modules * Reset old modification values when a new roll is applied * Fix issue with miner role where refinery would not be present in ships with class 5 slots but no class 4 + * Ensure that boost value is set correctly when modifications to power distributor enable/disable boost + * Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages + * Add tooltip for blueprints providing details of the features they alter + * Use opponent's saved pips if available + * Ignore rounds per shot for EPS and HPS calculations; it's already factored in to the numbers #2.2.19 * Power management panel now displays modules in descending order of power usage by default diff --git a/__tests__/fixtures/anaconda-test-detailed-export-v4.json b/__tests__/fixtures/anaconda-test-detailed-export-v4.json index 93aa7a93..65893b57 100644 --- a/__tests__/fixtures/anaconda-test-detailed-export-v4.json +++ b/__tests__/fixtures/anaconda-test-detailed-export-v4.json @@ -320,7 +320,6 @@ "shieldExplRes": 0.5, "shieldKinRes": 0.4, "shieldThermRes": -0.2, - "timeToDrain": 7.04, "crew": 3 } } diff --git a/__tests__/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json index 6cfc0f68..2e5498a6 100644 --- a/__tests__/fixtures/expected-builds.json +++ b/__tests__/fixtures/expected-builds.json @@ -36,7 +36,7 @@ "Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=." }, "diamondback_explorer": { - "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f.AwRj4zTI.AwiMIypI." + "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f-.AwRj4zTYg===.AwiMIyoo." }, "vulture": { "Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA." diff --git a/d3.min.js b/d3.min.js index 10e71019..5b9a0d80 100644 --- a/d3.min.js +++ b/d3.min.js @@ -1,4 +1,4 @@ -!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t){return function(n,e){return so(t(n),e)}}function e(t,n,e){var i=Math.abs(n-t)/Math.max(0,e),r=Math.pow(10,Math.floor(Math.log(i)/Math.LN10)),o=i/r;return o>=po?r*=10:o>=yo?r*=5:o>=vo&&(r*=2),n=0&&(e=t.slice(i+1),t=t.slice(0,i)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function p(t,n){for(var e,i=0,r=t.length;i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function w(t){return function(){var n=this.__on;if(n){for(var e,i=0,r=-1,o=n.length;in?1:t>=n?0:NaN}function A(t){return function(){this.removeAttribute(t)}}function E(t){return function(){this.removeAttributeNS(t.space,t.local)}}function U(t,n){return function(){this.setAttribute(t,n)}}function L(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function P(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function R(t){return function(){this.style.removeProperty(t)}}function F(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var i=n.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,e)}}function q(t){return function(){delete this[t]}}function H(t,n){return function(){this[t]=n}}function z(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function O(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new X(t)}function X(t){this._node=t,this._names=O(t.getAttribute("class")||"")}function I(t,n){for(var e=j(t),i=-1,r=n.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Au.exec(t))?pt(parseInt(n[1],16)):(n=Eu.exec(t))?new gt(n[1],n[2],n[3],1):(n=Uu.exec(t))?new gt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Lu.exec(t))?dt(n[1],n[2],n[3],n[4]):(n=Pu.exec(t))?dt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Du.exec(t))?xt(n[1],n[2]/100,n[3]/100,1):(n=Ru.exec(t))?xt(n[1],n[2]/100,n[3]/100,n[4]):Fu.hasOwnProperty(t)?pt(Fu[t]):"transparent"===t?new gt(NaN,NaN,NaN,0):null}function pt(t){return new gt(t>>16&255,t>>8&255,255&t,1)}function dt(t,n,e,i){return i<=0&&(t=n=e=NaN),new gt(t,n,e,i)}function yt(t){return t instanceof ft||(t=_t(t)),t?(t=t.rgb(),new gt(t.r,t.g,t.b,t.opacity)):new gt}function vt(t,n,e,i){return 1===arguments.length?yt(t):new gt(t,n,e,null==i?1:i)}function gt(t,n,e,i){this.r=+t,this.g=+n,this.b=+e,this.opacity=+i}function xt(t,n,e,i){return i<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new bt(t,n,e,i)}function mt(t){if(t instanceof bt)return new bt(t.h,t.s,t.l,t.opacity);if(t instanceof ft||(t=_t(t)),!t)return new bt;if(t instanceof bt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,i=t.b/255,r=Math.min(n,e,i),o=Math.max(n,e,i),u=NaN,a=o-r,s=(o+r)/2;return a?(u=n===o?(e-i)/a+6*(e0&&s<1?0:u,new bt(u,a,s,t.opacity)}function wt(t,n,e,i){return 1===arguments.length?mt(t):new bt(t,n,e,null==i?1:i)}function bt(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Mt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Nt(t){if(t instanceof Tt)return new Tt(t.l,t.a,t.b,t.opacity);if(t instanceof Pt){var n=t.h*Yu;return new Tt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof gt||(t=yt(t));var e=Et(t.r),i=Et(t.g),r=Et(t.b),o=Ct((.4124564*e+.3575761*i+.1804375*r)/zu),u=Ct((.2126729*e+.7151522*i+.072175*r)/Ou),a=Ct((.0193339*e+.119192*i+.9503041*r)/ju);return new Tt(116*u-16,500*(o-u),200*(u-a),t.opacity)}function kt(t,n,e,i){return 1===arguments.length?Nt(t):new Tt(t,n,e,null==i?1:i)}function Tt(t,n,e,i){this.l=+t,this.a=+n,this.b=+e,this.opacity=+i}function Ct(t){return t>Bu?Math.pow(t,1/3):t/$u+Xu}function St(t){return t>Iu?t*t*t:$u*(t-Xu)}function At(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Et(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Ut(t){if(t instanceof Pt)return new Pt(t.h,t.c,t.l,t.opacity);t instanceof Tt||(t=Nt(t));var n=Math.atan2(t.b,t.a)*qu;return new Pt(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Lt(t,n,e,i){return 1===arguments.length?Ut(t):new Pt(t,n,e,null==i?1:i)}function Pt(t,n,e,i){this.h=+t,this.c=+n,this.l=+e,this.opacity=+i}function Dt(t){if(t instanceof Ft)return new Ft(t.h,t.s,t.l,t.opacity);t instanceof gt||(t=yt(t));var n=t.r/255,e=t.g/255,i=t.b/255,r=(ta*i+Qu*n-Ku*e)/(ta+Qu-Ku),o=i-r,u=(Gu*(e-r)-Wu*o)/Ju,a=Math.sqrt(u*u+o*o)/(Gu*r*(1-r)),s=a?Math.atan2(u,o)*qu-120:NaN;return new Ft(s<0?s+360:s,a,r,t.opacity)}function Rt(t,n,e,i){return 1===arguments.length?Dt(t):new Ft(t,n,e,null==i?1:i)}function Ft(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Yt(t,n){return function(e){return t+e*n}}function qt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(i){return Math.pow(t+i*n,e)}}function Ht(t,n){var e=n-t;return e?Yt(t,e>180||e<-180?e-360*Math.round(e/360):e):oa(isNaN(t)?n:t)}function zt(t){return 1===(t=+t)?Ot:function(n,e){return e-n?qt(n,e,t):oa(isNaN(n)?e:n)}}function Ot(t,n){var e=n-t;return e?Yt(t,e):oa(isNaN(t)?n:t)}function jt(t){return function(){return t}}function Xt(t){return function(n){return t(n)+""}}function It(t){return"none"===t?va:(na||(na=document.createElement("DIV"),ea=document.documentElement,ia=document.defaultView),na.style.transform=t,t=ia.getComputedStyle(ea.appendChild(na),null).getPropertyValue("transform"),ea.removeChild(na),t=t.slice(7,-1).split(","),ga(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))}function $t(t){return null==t?va:(ra||(ra=document.createElementNS("http://www.w3.org/2000/svg","g")),ra.setAttribute("transform",t),(t=ra.transform.baseVal.consolidate())?(t=t.matrix,ga(t.a,t.b,t.c,t.d,t.e,t.f)):va)}function Bt(t,n,e,i){function r(t){return t.length?t.pop()+" ":""}function o(t,i,r,o,u,a){if(t!==r||i!==o){var s=u.push("translate(",null,n,null,e);a.push({i:s-4,x:ha(t,r)},{i:s-2,x:ha(i,o)})}else(r||o)&&u.push("translate("+r+n+o+e)}function u(t,n,e,o){t!==n?(t-n>180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(r(e)+"rotate(",null,i)-2,x:ha(t,n)})):n&&e.push(r(e)+"rotate("+n+i)}function a(t,n,e,o){t!==n?o.push({i:e.push(r(e)+"skewX(",null,i)-2,x:ha(t,n)}):n&&e.push(r(e)+"skewX("+n+i)}function s(t,n,e,i,o,u){if(t!==e||n!==i){var a=o.push(r(o)+"scale(",null,",",null,")");u.push({i:a-4,x:ha(t,e)},{i:a-2,x:ha(n,i)})}else 1===e&&1===i||o.push(r(o)+"scale("+e+","+i+")")}return function(n,e){var i=[],r=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,i,r),u(n.rotate,e.rotate,i,r),a(n.skewX,e.skewX,i,r),s(n.scaleX,n.scaleY,e.scaleX,e.scaleY,i,r),n=e=null,function(t){for(var n,e=-1,o=r.length;++e=0&&n._call.call(null,t),n=n._next;--Na}function Kt(){Aa=(Sa=Ua.now())+Ea,Na=ka=0;try{Qt()}finally{Na=0,nn(),Aa=0}}function tn(){var t=Ua.now(),n=t-Sa;n>Ca&&(Ea-=n,Sa=t)}function nn(){for(var t,n,e=wa,i=1/0;e;)e._call?(i>e._time&&(i=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:wa=n);ba=t,en(i)}function en(t){if(!Na){ka&&(ka=clearTimeout(ka));var n=t-Aa;n>24?(t<1/0&&(ka=setTimeout(Kt,n)),Ta&&(Ta=clearInterval(Ta))):(Ta||(Sa=Aa,Ta=setInterval(tn,Ca)),Na=1,La(Kt))}}function rn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Fa)throw new Error("too late");return e}function on(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>qa)throw new Error("too late");return e}function un(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function an(t,n,e){function i(t){e.state=Ya,e.timer.restart(r,e.delay,e.time),e.delay<=t&&r(t-e.delay)}function r(i){var h,c,l,f;if(e.state!==Ya)return u();for(h in s)if(f=s[h],f.name===e.name){if(f.state===Ha)return Pa(r);f.state===za?(f.state=ja,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete s[h]):+h=0&&(t=t.slice(0,n)),!t||"start"===t})}function kn(t,n,e){var i,r,o=Nn(n)?rn:on;return function(){var u=o(this,t),a=u.on;a!==i&&(r=(i=a).copy()).on(n,e),u.on=r}}function Tn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Cn(t,n){var e,i,r;return function(){var o=au(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===i?r:r=n(e=u,i=a)}}function Sn(t){return function(){this.style.removeProperty(t)}}function An(t,n,e){var i,r;return function(){var o=au(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===i?r:r=n(i=o,e)}}function En(t,n,e){var i,r,o;return function(){var u=au(this).getComputedStyle(this,null),a=u.getPropertyValue(t),s=e(this);return null==s&&(this.style.removeProperty(t),s=u.getPropertyValue(t)),a===s?null:a===i&&s===r?o:o=n(i=a,r=s)}}function Un(t,n,e){function i(){var i=this,r=n.apply(i,arguments);return r&&function(n){i.style.setProperty(t,r(n),e)}}return i._value=n,i}function Ln(t){return function(){this.textContent=t}}function Pn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Dn(t,n,e,i){this._groups=t,this._parents=n,this._name=e,this._id=i}function Rn(t){return ht().transition(t)}function Fn(){return++ls}function Yn(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function qn(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return gs.time=Zt(),gs;return e}function Hn(t){return{type:t}}function zn(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function On(){return new zn}function jn(){}function Xn(t,n){var e=new jn;if(t instanceof jn)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var i,r=-1,o=t.length;if(null==n)for(;++r=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u,r=_,!(_=_[l=c<<1|h]))return r[l]=p,t;if(a=+t._x.call(null,_.data),s=+t._y.call(null,_.data),n===a&&e===s)return p.next=_,r?r[l]=p:t._root=p,t;do r=r?r[l]=new Array(4):t._root=new Array(4),(h=n>=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u;while((l=c<<1|h)===(f=(s>=u)<<1|a>=o));return r[f]=_,r[l]=p,t}function Jn(t){var n,e,i,r,o=t.length,u=new Array(o),a=new Array(o),s=1/0,h=1/0,c=-(1/0),l=-(1/0);for(e=0;ec&&(c=i),rl&&(l=r));for(c",r=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],s=!!n[7],h=n[8]&&+n[8].slice(1),c=n[9]||"";"n"===c?(s=!0,c="g"):Qs[c]||(c=""),(u||"0"===e&&"="===i)&&(u=!0,e="0",i="="),this.fill=e,this.align=i,this.sign=r,this.symbol=o,this.zero=u,this.width=a,this.comma=s,this.precision=h,this.type=c}function re(t){return t}function oe(t){return nh=oh(t),eh=nh.format,ih=nh.formatPrefix,nh}function ue(){this.reset()}function ae(t,n,e){var i=t.s=n+e,r=i-n,o=i-r;t.t=n-o+(e-r)}function se(t){return t>1?0:t<-1?fh:Math.acos(t)}function he(t){return t>1?_h:t<-1?-_h:Math.asin(t)}function ce(){}function le(t){var n=t[0],e=t[1],i=mh(e);return[i*mh(n),i*Mh(n),Mh(e)]}function fe(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function _e(t){var n=Nh(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function pe(t,n){return[t>fh?t-dh:t<-fh?t+dh:t,n]}function de(t,n,e,i){this.x=t,this.z=n,this.o=e,this.e=i,this.v=!1,this.n=this.p=null}function ye(t){if(n=t.length){for(var n,e,i=0,r=t[0];++i1}function we(t,n){return((t=t.x)[0]<0?t[1]-_h-lh:_h-t[1])-((n=n.x)[0]<0?n[1]-_h-lh:_h-n[1])}function be(t){var n,e=NaN,i=NaN,r=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?fh:-fh,s=vh(o-e);vh(s-fh)0?_h:-_h),t.point(r,i),t.lineEnd(),t.lineStart(),t.point(a,i),t.point(o,i),n=0):r!==a&&s>=fh&&(vh(e-r)lh?gh((Mh(n)*(o=mh(i))*Mh(e)-Mh(i)*(r=mh(n))*Mh(t))/(r*o*u)):(n+i)/2}function Ne(t,n,e,i){var r;if(null==t)r=e*_h,i.point(-fh,r),i.point(0,r),i.point(fh,r),i.point(fh,0),i.point(fh,-r),i.point(0,-r),i.point(-fh,-r),i.point(-fh,0),i.point(-fh,r);else if(vh(t[0]-n[0])>lh){var o=t[0]=0;)n+=e[i].value;else n=1;t.value=n}function Ue(t,n){if(t===n)return t;var e=t.ancestors(),i=n.ancestors(),r=null;for(t=e.pop(),n=i.pop();t===n;)r=t,t=e.pop(),n=i.pop();return r}function Le(t,n){var e,i,r,o,u,a=new Ye(t),s=+t.value&&(a.value=t.value),h=[a];for(null==n&&(n=De);e=h.pop();)if(s&&(e.value=+e.data.value),(r=n(e.data))&&(u=r.length))for(e.children=new Array(u),o=u-1;o>=0;--o)h.push(i=e.children[o]=new Ye(r[o])),i.parent=e,i.depth=e.depth+1;return a.eachBefore(Fe)}function Pe(){return Le(this).eachBefore(Re)}function De(t){return t.children}function Re(t){t.data=t.data.data}function Fe(t){var n=0;do t.height=n;while((t=t.parent)&&t.height<++n)}function Ye(t){this.data=t,this.depth=this.height=0,this.parent=null}function qe(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function He(t,n,e,i,r,o){for(var u,a,s,h,c,l,f,_,p,d,y,v=[],g=n.children,x=0,m=0,w=g.length,b=n.value;xf&&(f=a),y=c*c*d,_=Math.max(f/y,y/l),_>p){c-=a;break}p=_}v.push(u={value:c,dice:s=0;)if((e=t._tasks[i])&&(t._tasks[i]=null,e.abort))try{e.abort()}catch(n){}t._active=NaN,Ie(t)}function Ie(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function $e(t){return function(n,e){t(null==n?e:null)}}function Be(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function Ve(t,n){return function(e){return t(e.responseText,n)}}function Ze(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(r!==ic)return r;e.set(o,u=i.push(n))}return t[(u-1)%t.length]}var e=Xn(),i=[],r=ic;return t=null==t?[]:ec.call(t),n.domain=function(t){if(!arguments.length)return i.slice();i=[],e=Xn();for(var r,o,u=-1,a=t.length;++u=e?1:i(t)}}}function Qe(t){return function(n,e){var i=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:i(t)}}}function Ke(t,n,e,i){var r=t[0],o=t[1],u=n[0],a=n[1];return o2?ti:Ke,o=u=null,i}function i(n){return(o||(o=r(a,s,c?Ge(t):t,h)))(+n)}var r,o,u,a=uc,s=uc,h=pa,c=!1;return i.invert=function(t){return(u||(u=r(s,a,Je,c?Qe(n):n)))(+t)},i.domain=function(t){return arguments.length?(a=nc.call(t,oc),e()):a.slice()},i.range=function(t){return arguments.length?(s=ec.call(t),e()):s.slice()},i.rangeRound=function(t){return s=ec.call(t),h=da,e()},i.clamp=function(t){return arguments.length?(c=!!t,e()):c},i.interpolate=function(t){return arguments.length?(h=t,e()):h},e()}function ii(t){var n=t.domain;return t.ticks=function(t){var e=n();return go(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return ac(n(),t,e)},t.nice=function(i){var r=n(),o=r.length-1,u=null==i?10:i,a=r[0],s=r[o],h=e(a,s,u);return h&&(h=e(Math.floor(a/h)*h,Math.ceil(s/h)*h,u),r[0]=Math.floor(a/h)*h,r[o]=Math.ceil(s/h)*h,n(r)),t},t}function ri(){var t=ei(Je,ha);return t.copy=function(){return ni(t,ri())},ii(t)}function oi(t,n,e,i){function r(n){return t(n=new Date((+n))),n}return r.floor=r,r.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},r.round=function(t){var n=r(t),e=r.ceil(t);return t-n0))return u;do u.push(new Date((+e)));while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,i){if(t>=t)for(;--i>=0;)for(;n(t,1),!e(t););})},e&&(r.count=function(n,i){return sc.setTime(+n),hc.setTime(+i),t(sc),t(hc),Math.floor(e(sc,hc))},r.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?r.filter(i?function(n){return i(n)%t===0}:function(n){return r.count(0,n)%t===0}):r:null}),r}function ui(t){return oi(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*fc)/dc})}function ai(t){return oi(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/dc})}function si(t){if(0<=t.y&&t.y<100){var n=new Date((-1),t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function hi(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function ci(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function li(t){function n(t,n){return function(e){var i,r,o,u=[],a=-1,s=0,h=t.length;for(e instanceof Date||(e=new Date((+e)));++a=s)return-1;if(r=n.charCodeAt(u++),37===r){if(r=n.charAt(u++),o=j[r in Ac?n.charAt(u++):r],!o||(i=o(t,e,i))<0)return-1}else if(r!=e.charCodeAt(i++))return-1}return i}function r(t,n,e){var i=E.exec(n.slice(e));return i?(t.p=U[i[0].toLowerCase()],e+i[0].length):-1}function o(t,n,e){var i=D.exec(n.slice(e));return i?(t.w=R[i[0].toLowerCase()],e+i[0].length):-1}function u(t,n,e){var i=L.exec(n.slice(e));return i?(t.w=P[i[0].toLowerCase()],e+i[0].length):-1}function a(t,n,e){var i=q.exec(n.slice(e));return i?(t.m=H[i[0].toLowerCase()],e+i[0].length):-1}function s(t,n,e){var i=F.exec(n.slice(e));return i?(t.m=Y[i[0].toLowerCase()],e+i[0].length):-1}function h(t,n,e){return i(t,b,n,e)}function c(t,n,e){return i(t,M,n,e)}function l(t,n,e){return i(t,N,n,e)}function f(t){return C[t.getDay()]}function _(t){return T[t.getDay()]}function p(t){return A[t.getMonth()]}function d(t){return S[t.getMonth()]}function y(t){return k[+(t.getHours()>=12)]}function v(t){return C[t.getUTCDay()]}function g(t){return T[t.getUTCDay()]}function x(t){return A[t.getUTCMonth()]}function m(t){return S[t.getUTCMonth()]}function w(t){return k[+(t.getUTCHours()>=12)]}var b=t.dateTime,M=t.date,N=t.time,k=t.periods,T=t.days,C=t.shortDays,S=t.months,A=t.shortMonths,E=pi(k),U=di(k),L=pi(T),P=di(T),D=pi(C),R=di(C),F=pi(S),Y=di(S),q=pi(A),H=di(A),z={a:f,A:_,b:p,B:d,c:null,d:Ei,e:Ei,H:Ui,I:Li,j:Pi,L:Di,m:Ri,M:Fi,p:y,S:Yi,U:qi,w:Hi,W:zi,x:null,X:null,y:Oi,Y:ji,Z:Xi,"%":rr},O={a:v,A:g,b:x,B:m,c:null,d:Ii,e:Ii,H:$i,I:Bi,j:Vi,L:Zi,m:Wi,M:Ji,p:w,S:Gi,U:Qi,w:Ki,W:tr,x:null,X:null,y:nr,Y:er,Z:ir,"%":rr},j={a:o,A:u,b:a,B:s,c:h,d:Mi,e:Mi,H:ki,I:ki,j:Ni,L:Si,m:bi,M:Ti,p:r,S:Ci,U:vi,w:yi,W:gi,x:c,X:l,y:mi,Y:xi,Z:wi,"%":Ai};return z.x=n(M,z),z.X=n(N,z),z.c=n(b,z),O.x=n(M,O),O.X=n(N,O),O.c=n(b,O),{format:function(t){var e=n(t+="",z);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",si);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",O);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,hi);return n.toString=function(){return t},n}}}function fi(t,n,e){var i=t<0?"-":"",r=(i?-t:t)+"",o=r.length;return i+(o68?1900:2e3),e+i[0].length):-1}function wi(t,n,e){var i=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return i?(t.Z=i[1]?0:-(i[2]+(i[3]||"00")),e+i[0].length):-1}function bi(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.m=i[0]-1,e+i[0].length):-1}function Mi(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.d=+i[0],e+i[0].length):-1}function Ni(t,n,e){var i=Ec.exec(n.slice(e,e+3));return i?(t.m=0,t.d=+i[0],e+i[0].length):-1}function ki(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.H=+i[0],e+i[0].length):-1}function Ti(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.M=+i[0],e+i[0].length):-1}function Ci(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.S=+i[0],e+i[0].length):-1}function Si(t,n,e){var i=Ec.exec(n.slice(e,e+3));return i?(t.L=+i[0],e+i[0].length):-1}function Ai(t,n,e){var i=Uc.exec(n.slice(e,e+1));return i?e+i[0].length:-1}function Ei(t,n){return fi(t.getDate(),n,2)}function Ui(t,n){return fi(t.getHours(),n,2)}function Li(t,n){return fi(t.getHours()%12||12,n,2)}function Pi(t,n){return fi(1+yc.count(xc(t),t),n,3)}function Di(t,n){return fi(t.getMilliseconds(),n,3)}function Ri(t,n){return fi(t.getMonth()+1,n,2)}function Fi(t,n){return fi(t.getMinutes(),n,2)}function Yi(t,n){return fi(t.getSeconds(),n,2)}function qi(t,n){return fi(vc.count(xc(t),t),n,2)}function Hi(t){return t.getDay()}function zi(t,n){return fi(gc.count(xc(t),t),n,2)}function Oi(t,n){return fi(t.getFullYear()%100,n,2)}function ji(t,n){return fi(t.getFullYear()%1e4,n,4)}function Xi(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+fi(n/60|0,"0",2)+fi(n%60,"0",2)}function Ii(t,n){return fi(t.getUTCDate(),n,2)}function $i(t,n){return fi(t.getUTCHours(),n,2)}function Bi(t,n){return fi(t.getUTCHours()%12||12,n,2)}function Vi(t,n){return fi(1+mc.count(Mc(t),t),n,3)}function Zi(t,n){return fi(t.getUTCMilliseconds(),n,3)}function Wi(t,n){return fi(t.getUTCMonth()+1,n,2)}function Ji(t,n){return fi(t.getUTCMinutes(),n,2)}function Gi(t,n){return fi(t.getUTCSeconds(),n,2)}function Qi(t,n){return fi(wc.count(Mc(t),t),n,2)}function Ki(t){return t.getUTCDay()}function tr(t,n){return fi(bc.count(Mc(t),t),n,2)}function nr(t,n){return fi(t.getUTCFullYear()%100,n,2)}function er(t,n){return fi(t.getUTCFullYear()%1e4,n,4)}function ir(){return"+0000"}function rr(){return"%"}function or(t){return Nc=li(t),kc=Nc.format,Tc=Nc.parse,Cc=Nc.utcFormat,Sc=Nc.utcParse,Nc}function ur(t){return t.toISOString()}function ar(t){var n=new Date(t);return isNaN(n)?null:n}function sr(t){this._context=t}function hr(t){return t[0]}function cr(t){return t[1]}function lr(t){this._curve=t}function fr(t){function n(n){return new lr(t(n))}return n._curve=t,n}function _r(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function pr(t){this._context=t}function dr(t,n){this._basis=new pr(t),this._beta=n}function yr(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function vr(t,n){this._context=t,this._k=(1-n)/6}function gr(t,n){this._context=t,this._k=(1-n)/6}function xr(t,n){this._context=t,this._k=(1-n)/6}function mr(t,n,e){var i=t._x1,r=t._y1,o=t._x2,u=t._y2;if(t._l01_a>Fc){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,s=3*t._l01_a*(t._l01_a+t._l12_a);i=(i*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/s,r=(r*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/s}if(t._l23_a>Fc){var h=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*h+t._x1*t._l23_2a-n*t._l12_2a)/c,u=(u*h+t._y1*t._l23_2a-e*t._l12_2a)/c}t._context.bezierCurveTo(i,r,o,u,t._x2,t._y2)}function wr(t,n){this._context=t,this._alpha=n}function br(t,n){this._context=t,this._alpha=n}function Mr(t,n){this._context=t,this._alpha=n}function Nr(t){return t<0?-1:1}function kr(t,n,e){var i=t._x1-t._x0,r=n-t._x1,o=(t._y1-t._y0)/(i||r<0&&-0),u=(e-t._y1)/(r||i<0&&-0),a=(o*r+u*i)/(i+r);return(Nr(o)+Nr(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Tr(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function Cr(t,n,e){var i=t._x0,r=t._y0,o=t._x1,u=t._y1,a=(o-i)/3;t._context.bezierCurveTo(i+a,r+a*n,o-a,u-a*e,o,u)}function Sr(t){this._context=t}function Ar(t){this._context=new Er(t)}function Er(t){this._context=t}function Ur(){this._=null}function Lr(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function Pr(t,n){var e=n,i=n.R,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.R=i.L,e.R&&(e.R.U=e),i.L=e}function Dr(t,n){var e=n,i=n.L,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.L=i.R,e.L&&(e.L.U=e),i.R=e}function Rr(t){for(;t.L;)t=t.L;return t}function Fr(t,n,e,i){var r=[null,null],o=Ic.push(r)-1;return r.left=t,r.right=n,e&&qr(r,t,n,e),i&&qr(r,n,t,i),jc[t.index].halfedges.push(o),jc[n.index].halfedges.push(o),r}function Yr(t,n,e){var i=[n,e];return i.left=t,i}function qr(t,n,e,i){t[0]||t[1]?t.left===e?t[1]=i:t[0]=i:(t[0]=i,t.left=n,t.right=e)}function Hr(t,n,e,i,r){var o,u=t[0],a=t[1],s=u[0],h=u[1],c=a[0],l=a[1],f=0,_=1,p=c-s,d=l-h;if(o=n-s,p||!(o>0)){if(o/=p,p<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=i-s,p||!(o<0)){if(o/=p,p<0){if(o>_)return;o>f&&(f=o)}else if(p>0){if(o0)){if(o/=d,d<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=r-h,d||!(o<0)){if(o/=d,d<0){if(o>_)return;o>f&&(f=o)}else if(d>0){if(o0||_<1)||(f>0&&(t[0]=[s+f*p,h+f*d]),_<1&&(t[1]=[s+_*p,h+_*d]),!0)}}}}}function zr(t,n,e,i,r){var o=t[1];if(o)return!0;var u,a,s=t[0],h=t.left,c=t.right,l=h[0],f=h[1],_=c[0],p=c[1],d=(l+_)/2,y=(f+p)/2;if(p===f){if(d=i)return;if(l>_){if(s){if(s[1]>=r)return}else s=[d,e];o=[d,r]}else{if(s){if(s[1]1)if(l>_){if(s){if(s[1]>=r)return}else s=[(e-a)/u,e];o=[(r-a)/u,r]}else{if(s){if(s[1]=i)return}else s=[n,u*n+a];o=[i,u*i+a]}else{if(s){if(s[0]Vc||Math.abs(r[0][1]-r[1][1])>Vc)||delete Ic[o]}function jr(t){return jc[t.index]={site:t,halfedges:[]}}function Xr(t,n){var e=t.site,i=n.left,r=n.right;return e===r&&(r=i,i=e),r?Math.atan2(r[1]-i[1],r[0]-i[0]):(e===i?(i=n[1],r=n[0]):(i=n[0],r=n[1]),Math.atan2(i[0]-r[0],r[1]-i[1]))}function Ir(t,n){return n[+(n.left!==t.site)]}function $r(t,n){return n[+(n.left===t.site)]}function Br(){for(var t,n,e,i,r=0,o=jc.length;rVc||Math.abs(d-f)>Vc)&&(s.splice(a,0,Ic.push(Yr(u,_,Math.abs(p-t)Vc?[t,Math.abs(l-t)Vc?[Math.abs(f-i)Vc?[e,Math.abs(l-e)Vc?[Math.abs(f-n)=-Zc)){var _=s*s+h*h,p=c*c+l*l,d=(l*_-h*p)/f,y=(s*p-c*_)/f,v=$c.pop()||new Zr;v.arc=t,v.site=r,v.x=d+u,v.y=(v.cy=y+a)+Math.sqrt(d*d+y*y),t.circle=v;for(var g=null,x=Xc._;x;)if(v.yVc)a=a.L;else{if(r=o-io(a,u),!(r>Vc)){i>-Vc?(n=a.P,e=a):r>-Vc?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}jr(t);var s=Qr(t);if(Oc.insert(n,s),n||e){if(n===e)return Jr(n),e=Qr(n.site),Oc.insert(s,e),s.edge=e.edge=Fr(n.site,s.site),Wr(n),void Wr(e);if(!e)return void(s.edge=Fr(n.site,s.site));Jr(n),Jr(e);var h=n.site,c=h[0],l=h[1],f=t[0]-c,_=t[1]-l,p=e.site,d=p[0]-c,y=p[1]-l,v=2*(f*y-_*d),g=f*f+_*_,x=d*d+y*y,m=[(y*g-_*x)/v+c,(f*x-d*g)/v+l];qr(e.edge,h,p,m),s.edge=Fr(h,t,null,m),e.edge=Fr(t,p,null,m),Wr(n),Wr(e)}}function eo(t,n){var e=t.site,i=e[0],r=e[1],o=r-n;if(!o)return i;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],s=e[1],h=s-n;if(!h)return a;var c=a-i,l=1/o-1/h,f=c/h;return l?(-f+Math.sqrt(f*f-2*l*(c*c/(-2*h)-s+h/2+r-o/2)))/l+i:(i+a)/2}function io(t,n){var e=t.N;if(e)return eo(e,n);var i=t.site;return i[1]===n?i[0]:1/0}function ro(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function oo(t,n){return n[1]-t[1]||n[0]-t[0]}function uo(t,n){var e,i,r,o=t.sort(oo).pop();for(Ic=[],jc=new Array(t.length),Oc=new Ur,Xc=new Ur;;)if(r=zc,o&&(!r||o[1]n?1:t>=n?0:NaN},ho=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)<0?i=o+1:r=o}return i},right:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)>0?r=o:i=o+1}return i}}},co=ho(so),lo=co.right,fo=Array.prototype,_o=(fo.slice,fo.map,function(t,n,e){t=+t,n=+n,e=(r=arguments.length)<2?(n=t,t=0,1):r<3?1:+e;for(var i=-1,r=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(r);++i=0;)for(i=t[r],n=i.length;--n>=0;)e[--u]=i[n];return e},mo=Array.prototype.slice,wo=function(t){return t},bo=1,Mo=2,No=3,ko=4,To=1e-6,Co={value:function(){}};f.prototype=l.prototype={constructor:f,on:function(t,n){var e,i=this._,r=_(t+"",i),o=-1,u=r.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,i,r=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ao.hasOwnProperty(n)?{space:Ao[n],local:t}:t},Uo=function(t){var n=Eo(t);return(n.local?v:y)(n)},Lo=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Po=document.documentElement;if(!Po.matches){var Do=Po.webkitMatchesSelector||Po.msMatchesSelector||Po.mozMatchesSelector||Po.oMatchesSelector;Lo=function(t){return function(){return Do.call(this,t)}}}}var Ro=Lo,Fo={},Yo=null;if("undefined"!=typeof document){var qo=document.documentElement;"onmouseenter"in qo||(Fo={mouseenter:"mouseover",mouseleave:"mouseout"})}var Ho=function(t,n,e){var i,r,o=m(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?b:w,null==e&&(e=!1),i=0;i=w&&(w=m+1);!(x=y[w])&&++w=0;)(i=r[o])&&(u&&u!==i.nextSibling&&u.parentNode.insertBefore(i,u),u=i);return this},Ko=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=S);for(var e=this._groups,i=e.length,r=new Array(i),o=0;o1?this.each((null==n?R:"function"==typeof n?Y:F)(t,n,null==e?"":e)):au(i=this.node()).getComputedStyle(i,null).getPropertyValue(t)},hu=function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?z:H)(t,n)):this.node()[t]};X.prototype={add:function(t){var n=this._names.indexOf(t);n<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var cu=function(t,n){var e=O(t+"");if(arguments.length<2){for(var i=j(this.node()),r=-1,o=e.length;++r=240?t-240:t+120,r,i),Mt(t,r,i),Mt(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Yu=Math.PI/180,qu=180/Math.PI,Hu=18,zu=.95047,Ou=1,ju=1.08883,Xu=4/29,Iu=6/29,$u=3*Iu*Iu,Bu=Iu*Iu*Iu;bu(Tt,kt,lt(ft,{brighter:function(t){return new Tt(this.l+Hu*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Tt(this.l-Hu*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Ou*St(t),n=zu*St(n),e=ju*St(e),new gt(At(3.2404542*n-1.5371385*t-.4985314*e),At(-.969266*n+1.8760108*t+.041556*e),At(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),bu(Pt,Lt,lt(ft,{brighter:function(t){return new Pt(this.h,this.c,this.l+Hu*(null==t?1:t),this.opacity)},darker:function(t){return new Pt(this.h,this.c,this.l-Hu*(null==t?1:t),this.opacity)},rgb:function(){return Nt(this).rgb()}}));var Vu=-.14861,Zu=1.78277,Wu=-.29227,Ju=-.90649,Gu=1.97294,Qu=Gu*Ju,Ku=Gu*Zu,ta=Zu*Wu-Ju*Vu;bu(Ft,Rt,lt(ft,{brighter:function(t){return t=null==t?Nu:Math.pow(Nu,t),new Ft(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Mu:Math.pow(Mu,t),new Ft(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Yu,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),i=Math.cos(t),r=Math.sin(t);return new gt(255*(n+e*(Vu*i+Zu*r)),255*(n+e*(Wu*i+Ju*r)),255*(n+e*(Gu*i)),this.opacity)}}));var na,ea,ia,ra,oa=function(t){return function(){return t}},ua=function Wc(t){function n(t,n){var i=e((t=vt(t)).r,(n=vt(n)).r),r=e(t.g,n.g),o=e(t.b,n.b),u=Ot(t.opacity,n.opacity);return function(n){return t.r=i(n),t.g=r(n),t.b=o(n),t.opacity=u(n),t+""}}var e=zt(t);return n.gamma=Wc,n}(1),aa=function(t,n){var e,i=n?n.length:0,r=t?Math.min(i,t.length):0,o=new Array(i),u=new Array(i);for(e=0;eo&&(r=n.slice(o,r),a[u]?a[u]+=r:a[++u]=r),(e=e[0])===(i=i[0])?a[u]?a[u]+=i:a[++u]=i:(a[++u]=null,s.push({i:u,x:ha(e,i)})),o=fa.lastIndex;return oqa&&e.statebs)if(Math.abs(c*a-s*h)>bs&&r){var f=e-o,_=i-u,p=a*a+s*s,d=f*f+_*_,y=Math.sqrt(p),v=Math.sqrt(l),g=r*Math.tan((ms-Math.acos((p+l-d)/(2*y*v)))/2),x=g/v,m=g/y;Math.abs(x-1)>bs&&(this._+="L"+(t+x*h)+","+(n+x*c)),this._+="A"+r+","+r+",0,0,"+ +(c*f>h*_)+","+(this._x1=t+m*a)+","+(this._y1=n+m*s)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,i,r,o){t=+t,n=+n,e=+e;var u=e*Math.cos(i),a=e*Math.sin(i),s=t+u,h=n+a,c=1^o,l=o?i-r:r-i;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+h:(Math.abs(this._x1-s)>bs||Math.abs(this._y1-h)>bs)&&(this._+="L"+s+","+h),e&&(l>Ms?this._+="A"+e+","+e+",0,1,"+c+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=h):(l<0&&(l=l%ws+ws),this._+="A"+e+","+e+",0,"+ +(l>=ms)+","+c+","+(this._x1=t+e*Math.cos(r))+","+(this._y1=n+e*Math.sin(r))))},rect:function(t,n,e,i){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +i+"h"+-e+"Z"},toString:function(){return this._}};var Ns="$";jn.prototype=Xn.prototype={constructor:jn,has:function(t){return Ns+t in this},get:function(t){return this[Ns+t]},set:function(t,n){return this[Ns+t]=n,this},remove:function(t){var n=Ns+t;return n in this&&delete this[n]},clear:function(){for(var t in this)t[0]===Ns&&delete this[t]},keys:function(){var t=[];for(var n in this)n[0]===Ns&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)n[0]===Ns&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)n[0]===Ns&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)n[0]===Ns&&++t;return t},empty:function(){for(var t in this)if(t[0]===Ns)return!1;return!0},each:function(t){for(var n in this)n[0]===Ns&&t(this[n],n.slice(1),this)}};var ks=Xn.prototype;In.prototype=$n.prototype={constructor:In,has:ks.has,add:function(t){return t+="",this[Ns+t]=t,this},remove:ks.remove,clear:ks.clear,values:ks.keys,size:ks.size,empty:ks.empty,each:ks.each};var Ts=function(t){function n(t,n){var i,r,o=e(t,function(t,e){return i?i(t,e-1):(r=t,void(i=n?Vn(t,n):Bn(t)))});return o.columns=r,o}function e(t,n){function e(){if(c>=h)return u;if(r)return r=!1,o;var n,e=c;if(34===t.charCodeAt(e)){for(var i=e;i++t||t>r||i>n||n>o))return this;var u,a,s=r-e,h=this._root;switch(a=(n<(i+o)/2)<<1|t<(e+r)/2){case 0:do u=new Array(4),u[a]=h,h=u;while(s*=2,r=e+s,o=i+s,t>r||n>o);break;case 1:do u=new Array(4),u[a]=h,h=u;while(s*=2,e=r-s,o=i+s,e>t||n>o);break;case 2:do u=new Array(4),u[a]=h,h=u;while(s*=2,r=e+s,i=o-s,t>r||i>n);break;case 3:do u=new Array(4),u[a]=h,h=u;while(s*=2,e=r-s,i=o-s,e>t||i>n)}this._root&&this._root.length&&(this._root=h)}return this._x0=e,this._y0=i,this._x1=r,this._y1=o,this},Ps=function(){var t=[];return this.visit(function(n){if(!n.length)do t.push(n.data);while(n=n.next)}),t},Ds=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},Rs=function(t,n,e,i,r){this.node=t,this.x0=n,this.y0=e,this.x1=i,this.y1=r},Fs=function(t,n,e){var i,r,o,u,a,s,h,c=this._x0,l=this._y0,f=this._x1,_=this._y1,p=[],d=this._root;for(d&&p.push(new Rs(d,c,l,f,_)),null==e?e=1/0:(c=t-e,l=n-e,f=t+e,_=n+e,e*=e);s=p.pop();)if(!(!(d=s.node)||(r=s.x0)>f||(o=s.y0)>_||(u=s.x1)=v)<<1|t>=y)&&(s=p[p.length-1],p[p.length-1]=p[p.length-1-h],p[p.length-1-h]=s)}else{var g=t-+this._x.call(null,d.data),x=n-+this._y.call(null,d.data),m=g*g+x*x;if(m=(a=(p+y)/2))?p=a:y=a,(c=u>=(s=(d+v)/2))?d=s:v=s,n=_,!(_=_[l=c<<1|h]))return this;if(!_.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,f=l)}for(;_.data!==t;)if(i=_,!(_=_.next))return this;return(r=_.next)&&delete _.next,i?(r?i.next=r:delete i.next,this):n?(r?n[l]=r:delete n[l],(_=n[0]||n[1]||n[2]||n[3])&&_===(n[3]||n[2]||n[1]||n[0])&&!_.length&&(e?e[f]=_:this._root=_),this):(this._root=r,this)},qs=function(){return this._root},Hs=function(){var t=0;return this.visit(function(n){if(!n.length)do++t;while(n=n.next)}),t},zs=function(t){var n,e,i,r,o,u,a=[],s=this._root;for(s&&a.push(new Rs(s,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(s=n.node,i=n.x0,r=n.y0,o=n.x1,u=n.y1)&&s.length){var h=(i+o)/2,c=(r+u)/2;(e=s[3])&&a.push(new Rs(e,h,c,o,u)),(e=s[2])&&a.push(new Rs(e,i,c,h,u)),(e=s[1])&&a.push(new Rs(e,h,r,o,c)),(e=s[0])&&a.push(new Rs(e,i,r,h,c))}return this},Os=function(t){var n,e=[],i=[];for(this._root&&e.push(new Rs(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var r=n.node;if(r.length){var o,u=n.x0,a=n.y0,s=n.x1,h=n.y1,c=(u+s)/2,l=(a+h)/2;(o=r[0])&&e.push(new Rs(o,u,a,c,l)),(o=r[1])&&e.push(new Rs(o,c,a,s,l)),(o=r[2])&&e.push(new Rs(o,u,l,c,h)),(o=r[3])&&e.push(new Rs(o,c,l,s,h))}i.push(n)}for(;n=i.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},js=function(t){return arguments.length?(this._x=t,this):this._x},Xs=function(t){return arguments.length?(this._y=t,this):this._y},Is=te.prototype=ne.prototype;Is.copy=function(){var t,n,e=new ne(this._x,this._y,this._x0,this._y0,this._x1,this._y1),i=this._root;if(!i)return e;if(!i.length)return e._root=ee(i),e;for(t=[{source:i,target:e._root=new Array(4)}];i=t.pop();)for(var r=0;r<4;++r)(n=i.source[r])&&(n.length?t.push({source:n,target:i.target[r]=new Array(4)}):i.target[r]=ee(n));return e},Is.add=Us,Is.addAll=Jn,Is.cover=Ls,Is.data=Ps,Is.extent=Ds,Is.find=Fs,Is.remove=Ys,Is.removeAll=Gn,Is.root=qs,Is.size=Hs,Is.visit=zs,Is.visitAfter=Os,Is.x=js,Is.y=Xs;var $s,Bs=(Math.PI*(3-Math.sqrt(5)),function(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,i=t.slice(0,e);return[i.length>1?i[0]+i.slice(2):i,+t.slice(e+1)]}),Vs=function(t){return t=Bs(Math.abs(t)),t?t[1]:NaN},Zs=function(t,n){return function(e,i){for(var r=e.length,o=[],u=0,a=t[0],s=0;r>0&&a>0&&(s+a+1>i&&(a=Math.max(1,i-s)),o.push(e.substring(r-=a,r+a)),!((s+=a+1)>i));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},Ws=function(t,n){t=t.toPrecision(n);t:for(var e,i=t.length,r=1,o=-1;r0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Js=function(t,n){var e=Bs(t,n);if(!e)return t+"";var i=e[0],r=e[1],o=r-($s=3*Math.max(-8,Math.min(8,Math.floor(r/3))))+1,u=i.length;return o===u?i:o>u?i+new Array(o-u+1).join("0"):o>0?i.slice(0,o)+"."+i.slice(o):"0."+new Array(1-o).join("0")+Bs(t,Math.max(0,n+o-1))[0]},Gs=function(t,n){var e=Bs(t,n);if(!e)return t+"";var i=e[0],r=e[1];return r<0?"0."+new Array((-r)).join("0")+i:i.length>r+1?i.slice(0,r+1)+"."+i.slice(r+1):i+new Array(r-i.length+2).join("0")},Qs={"":Ws,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Gs(100*t,n)},r:Gs,s:Js,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Ks=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i,th=function(t){return new ie(t)};ie.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var nh,eh,ih,rh=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],oh=function(t){function n(t){function n(t){var n,r,s,g=p,x=d;if("c"===_)x=y(t)+x,t="";else{t=+t;var m=(t<0||1/t<0)&&(t*=-1,!0);if(t=y(t,f),m)for(n=-1,r=t.length,m=!1;++ns||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}l&&!h&&(t=i(t,1/0));var w=g.length+t.length+x.length,b=w>1)+g+t+x+b.slice(w)}return b+g+t+x}t=th(t);var e=t.fill,u=t.align,a=t.sign,s=t.symbol,h=t.zero,c=t.width,l=t.comma,f=t.precision,_=t.type,p="$"===s?r[0]:"#"===s&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",d="$"===s?r[1]:/[%p]/.test(_)?"%":"",y=Qs[_],v=!_||/[defgprs%]/.test(_);return f=null==f?_?6:12:/[gprs]/.test(_)?Math.max(1,Math.min(21,f)):Math.max(0,Math.min(20,f)),n.toString=function(){return t+""},n}function e(t,e){var i=n((t=th(t),t.type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Vs(e)/3))),o=Math.pow(10,-r),u=rh[8+r/3];return function(t){return i(o*t)+u}}var i=t.grouping&&t.thousands?Zs(t.grouping,t.thousands):re,r=t.currency,o=t.decimal;return{format:n,formatPrefix:e}};oe({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var uh=function(t){return Math.max(0,-Vs(Math.abs(t)))},ah=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Vs(n)/3)))-Vs(Math.abs(t)))},sh=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Vs(n)-Vs(t))+1},hh=function(){return new ue};ue.prototype={constructor:ue,reset:function(){this.s=this.t=0},add:function(t){ae(ch,t,this.t),ae(this,ch.s,this.s),this.s?this.t+=ch.t:this.s=ch.t},valueOf:function(){return this.s}};var ch=new ue,lh=1e-6,fh=Math.PI,_h=fh/2,ph=fh/4,dh=2*fh,yh=fh/180,vh=Math.abs,gh=Math.atan,xh=Math.atan2,mh=Math.cos,wh=(Math.ceil,Math.exp),bh=Math.log,Mh=(Math.pow,Math.sin),Nh=(Math.sign||function(t){return t>0?1:t<0?-1:0},Math.sqrt),kh=Math.tan;hh(),hh(),hh();pe.invert=pe;var Th=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:ce,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},Ch=function(t,n){return vh(t[0]-n[0])=0;--o)r.point((c=h[o])[0],c[1]);else i(f.x,f.p.x,-1,r);f=f.p}f=f.o,h=f.z,_=!_}while(!f.v);r.lineEnd()}}},Ah=(hh(),hh(),hh(),1/0),Eh=-Ah;ve.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,dh)}},result:ce};hh();ge.prototype={_circle:xe(4.5),pointRadius:function(t){return this._circle=xe(t),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}}};var Uh=hh(),Lh=function(t,n){var e=n[0],i=n[1],r=[Mh(e),-mh(e),0],o=0,u=0;Uh.reset();for(var a=0,s=t.length;a=0?1:-1,N=M*b,k=N>fh,T=p*m;if(Uh.add(xh(T*M*Mh(N),d*w+T*mh(N))),o+=k?b+M*dh:b,k^f>=e^g>=e){var C=fe(le(l),le(v));_e(C);var S=fe(r,C);_e(S);var A=(k^b>=0?-1:1)*he(S[2]);(i>A||i===A&&(C[0]||C[1]))&&(u+=k^b>=0?1:-1)}}return(o<-lh||o0){for(m||(o.polygonStart(),m=!0),o.lineStart(),t=0;t1&&2&r&&u.push(u.pop().concat(u.shift())),p.push(u.filter(me))}var _,p,d,y=n(o),v=r.invert(i[0],i[1]),g=Th(),x=n(g),m=!1,w={point:u,lineStart:s,lineEnd:h,polygonStart:function(){w.point=c,w.lineStart=l,w.lineEnd=f,p=[],_=[]},polygonEnd:function(){w.point=u,w.lineStart=s,w.lineEnd=h,p=xo(p);var t=Lh(_,v);p.length?(m||(o.polygonStart(),m=!0),Sh(p,we,t,e,o)):t&&(m||(o.polygonStart(),m=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),m&&(o.polygonEnd(),m=!1),p=_=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return w}};Ph(function(){return!0},be,Ne,[-fh,-_h]);Te.prototype={constructor:Te,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Dh=(mh(30*yh),ke({point:function(t,n){this.stream.point(t*yh,n*yh)}}),Ce(function(t){return Nh(2/(1+t))}));Dh.invert=Se(function(t){return 2*he(t/2)});var Rh=Ce(function(t){return(t=se(t))&&t/Mh(t)});Rh.invert=Se(function(t){return t}),Ae.invert=function(t,n){return[-n,2*gh(wh(t))-_h]};var Fh=function(){return this.eachAfter(Ee)},Yh=function(t){var n,e,i,r,o=this,u=[o];do for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(i=0,r=e.length;i=0;--e)r.push(n[e]);return this},Hh=function(t){for(var n,e,i,r=this,o=[r],u=[];r=o.pop();)if(u.push(r),n=r.children)for(e=0,i=n.length;e=0;)e+=i[r].value;n.value=e})},Oh=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},jh=function(t){for(var n=this,e=Ue(n,t),i=[n];n!==e;)n=n.parent,i.push(n);for(var r=i.length;t!==e;)i.splice(r,0,t),t=t.parent;return i},Xh=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},Ih=function(){var t=[];return this.each(function(n){t.push(n)}),t},$h=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Bh=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Ye.prototype=Le.prototype={constructor:Ye,count:Fh,each:Yh,eachAfter:Hh,eachBefore:qh,sum:zh,sort:Oh,path:jh,ancestors:Xh,descendants:Ih,leaves:$h,links:Bh,copy:Pe};var Vh=function(t,n,e,i,r){for(var o,u=t.children,a=-1,s=u.length,h=t.value&&(i-n)/t.value;++a1?t:1)},n})(Wh);!function ul(t){function n(n,e,i,r,o){if((u=n._squarify)&&u.ratio===t)for(var u,a,s,h,c,l=-1,f=u.length,_=n.value;++l1?t:1)},n}(Wh);var Jh=([].slice,{}),Gh=function(t,n){function e(t){var n,e=h.status;if(!e&&Be(h)||e>=200&&e<300||304===e){if(o)try{n=o.call(i,h)}catch(r){return void a.call("error",i,r)}else n=h;a.call("load",i,n)}else a.call("error",i,t)}var i,r,o,u,a=l("beforesend","progress","load","error"),s=Xn(),h=new XMLHttpRequest,c=null,f=null,_=0;if("undefined"==typeof XDomainRequest||"withCredentials"in h||!/^(http(s)?:)?\/\//.test(t)||(h=new XDomainRequest),"onload"in h?h.onload=h.onerror=h.ontimeout=e:h.onreadystatechange=function(t){h.readyState>3&&e(t)},h.onprogress=function(t){a.call("progress",i,t)},i={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?s.get(t):(null==n?s.remove(t):s.set(t,n+""),i)},mimeType:function(t){return arguments.length?(r=null==t?null:t+"",i):r},responseType:function(t){return arguments.length?(u=t,i):u},timeout:function(t){return arguments.length?(_=+t,i):_},user:function(t){return arguments.length<1?c:(c=null==t?null:t+"",i)},password:function(t){return arguments.length<1?f:(f=null==t?null:t+"",i)},response:function(t){return o=t,i},get:function(t,n){return i.send("GET",t,n)},post:function(t,n){return i.send("POST",t,n)},send:function(n,e,o){return h.open(n,t,!0,c,f),null==r||s.has("accept")||s.set("accept",r+",*/*"),h.setRequestHeader&&s.each(function(t,n){h.setRequestHeader(n,t)}),null!=r&&h.overrideMimeType&&h.overrideMimeType(r),null!=u&&(h.responseType=u),_>0&&(h.timeout=_),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=$e(o)),null!=o&&i.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",i,h),h.send(null==e?null:e),i},abort:function(){return h.abort(),i},on:function(){var t=a.on.apply(a,arguments);return t===a?i:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return i.get(n)}return i},Qh=function(t,n){return function(e,i){var r=Gh(e).mimeType(t).response(n);if(null!=i){if("function"!=typeof i)throw new Error("invalid callback: "+i);return r.get(i)}return r}};Qh("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),Qh("application/json",function(t){return JSON.parse(t.responseText)}),Qh("text/plain",function(t){return t.responseText}),Qh("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n});var Kh=function(t,n){return function(e,i,r){arguments.length<3&&(r=i,i=null);var o=Gh(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(Ve(n,i=t)):i},o.row(i),r?o.get(r):o}};Kh("text/csv",Ss),Kh("text/tab-separated-values",Es);var tc=Array.prototype,nc=tc.map,ec=tc.slice,ic={name:"implicit"},rc=function(t){return function(){return t}},oc=function(t){return+t},uc=[0,1],ac=function(t,n,i){var r,o=t[0],u=t[t.length-1],a=e(o,u,null==n?10:n);switch(i=th(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(o),Math.abs(u));return null!=i.precision||isNaN(r=ah(a,s))||(i.precision=r),ih(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(r=sh(a,Math.max(Math.abs(o),Math.abs(u))))||(i.precision=r-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(r=uh(a))||(i.precision=r-2*("%"===i.type))}return eh(i)},sc=new Date,hc=new Date,cc=oi(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});cc.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?oi(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):cc:null};var lc=1e3,fc=6e4,_c=36e5,pc=864e5,dc=6048e5,yc=(oi(function(t){t.setTime(Math.floor(t/lc)*lc)},function(t,n){t.setTime(+t+n*lc)},function(t,n){return(n-t)/lc},function(t){return t.getUTCSeconds()}),oi(function(t){t.setTime(Math.floor(t/fc)*fc)},function(t,n){t.setTime(+t+n*fc)},function(t,n){return(n-t)/fc},function(t){return t.getMinutes()}),oi(function(t){var n=t.getTimezoneOffset()*fc%_c;n<0&&(n+=_c),t.setTime(Math.floor((+t-n)/_c)*_c+n)},function(t,n){t.setTime(+t+n*_c)},function(t,n){return(n-t)/_c},function(t){return t.getHours()}),oi(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*fc)/pc},function(t){return t.getDate()-1})),vc=ui(0),gc=ui(1),xc=(ui(2),ui(3),ui(4),ui(5),ui(6),oi(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),oi(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()}));xc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?oi(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var mc=(oi(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*fc)},function(t,n){return(n-t)/fc},function(t){return t.getUTCMinutes()}),oi(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+n*_c)},function(t,n){return(n-t)/_c},function(t){return t.getUTCHours()}),oi(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/pc},function(t){return t.getUTCDate()-1})),wc=ai(0),bc=ai(1),Mc=(ai(2),ai(3),ai(4),ai(5),ai(6),oi(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),oi(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()}));Mc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?oi(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Nc,kc,Tc,Cc,Sc,Ac={"-":"",_:" ",0:"0"},Ec=/^\s*\d+/,Uc=/^%/,Lc=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;or({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Pc="%Y-%m-%dT%H:%M:%S.%LZ",Dc=(Date.prototype.toISOString?ur:Cc(Pc),+new Date("2000-01-01T00:00:00.000Z")?ar:Sc(Pc),function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})});Dc("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Dc("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Dc("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),Dc("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),Ma(Rt(300,.5,0),Rt(-240,.5,1));var Rc=(Ma(Rt(-100,.75,.35),Rt(80,1.5,.8)),Ma(Rt(260,.75,.35),Rt(80,1.5,.8)),Rt(),function(t){return function(){return t}}),Fc=1e-12;Math.PI;sr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Yc=function(t){return new sr(t)},qc=function(){function t(t){ -var a,s,h,c=t.length,l=!1;for(null==r&&(u=o(h=On())),a=0;a<=c;++a)!(a0)for(var i,r=t[0],o=n[0],u=t[e]-r,a=n[e]-o,s=-1;++s<=e;)i=s/e,this._basis.point(this._beta*t[s]+(1-this._beta)*(r+i*u),this._beta*n[s]+(1-this._beta)*(o+i*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}},function al(t){function n(n){return 1===t?new pr(n):new dr(n,t)}return n.beta=function(t){return al(+t)},n}(.85),vr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:yr(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function sl(t){function n(n){return new vr(n,t)}return n.tension=function(t){return sl(+t)},n}(0),gr.prototype={areaStart:Hc,areaEnd:Hc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function hl(t){function n(n){return new gr(n,t)}return n.tension=function(t){return hl(+t)},n}(0),xr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function cl(t){function n(n){return new xr(n,t)}return n.tension=function(t){return cl(+t)},n}(0),wr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function ll(t){function n(n){return t?new wr(n,t):new vr(n,0)}return n.alpha=function(t){return ll(+t)},n}(.5),br.prototype={areaStart:Hc,areaEnd:Hc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function fl(t){function n(n){return t?new br(n,t):new gr(n,0)}return n.alpha=function(t){return fl(+t)},n}(.5),Mr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function _l(t){function n(n){return t?new Mr(n,t):new xr(n,0)}return n.alpha=function(t){return _l(+t)},n}(.5),Sr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:Cr(this,this._t0,Tr(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,Cr(this,Tr(this,e=kr(this,t,n)),e);break;default:Cr(this,this._t0,e=kr(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Ar.prototype=Object.create(Sr.prototype)).point=function(t,n){Sr.prototype.point.call(this,n,t)},Er.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,i,r,o){this._context.bezierCurveTo(n,t,i,e,o,r)}};Array.prototype.slice;Ur.prototype={constructor:Ur,insert:function(t,n){var e,i,r;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Rr(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)i=e.U,e===i.L?(r=i.R,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.R&&(Pr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Dr(this,i))):(r=i.L,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.L&&(Dr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Pr(this,i))),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,i,r=t.U,o=t.L,u=t.R;if(e=o?u?Rr(u):o:u,r?r.L===t?r.L=e:r.R=e:this._=e,o&&u?(i=e.C,e.C=t.C,e.L=o,o.U=e,e!==u?(r=e.U,e.U=t.U,t=e.R,r.L=t,e.R=u,u.U=e):(e.U=r,r=e,t=e.R)):(i=t.C,t=e),t&&(t.U=r),!i){if(t&&t.C)return void(t.C=!1);do{if(t===this._)break;if(t===r.L){if(n=r.R,n.C&&(n.C=!1,r.C=!0,Pr(this,r),n=r.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,Dr(this,n),n=r.R),n.C=r.C,r.C=n.R.C=!1,Pr(this,r),t=this._;break}}else if(n=r.L,n.C&&(n.C=!1,r.C=!0,Dr(this,r),n=r.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,Pr(this,n),n=r.L),n.C=r.C,r.C=n.L.C=!1,Dr(this,r),t=this._;break}n.C=!0,t=r,r=r.U}while(!t.C);t&&(t.C=!1)}}};var zc,Oc,jc,Xc,Ic,$c=[],Bc=[],Vc=1e-6,Zc=1e-12;uo.prototype={constructor:uo,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return Ir(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,i){if(o=(r=e.halfedges).length)for(var r,o,u,a=e.site,s=-1,h=n[r[o-1]],c=h.left===a?h.right:h.left;++s=a)return null;var s=t-r.site[0],h=n-r.site[1],c=s*s+h*h;do r=o.cells[i=u],u=null,r.halfedges.forEach(function(e){var i=o.edges[e],a=i.left;if(a!==r.site&&a||(a=i.right)){var s=t-a[0],h=n-a[1],l=s*s+h*h;l=fo?r*=10:o>=_o?r*=5:o>=po&&(r*=2),n=0&&(e=t.slice(i+1),t=t.slice(0,i)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function p(t,n){for(var e,i=0,r=t.length;i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function w(t){return function(){var n=this.__on;if(n){for(var e,i=0,r=-1,o=n.length;in?1:t>=n?0:NaN}function A(t){return function(){this.removeAttribute(t)}}function E(t){return function(){this.removeAttributeNS(t.space,t.local)}}function U(t,n){return function(){this.setAttribute(t,n)}}function L(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function P(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function R(t){return function(){this.style.removeProperty(t)}}function F(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var i=n.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,e)}}function q(t){return function(){delete this[t]}}function H(t,n){return function(){this[t]=n}}function z(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function O(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new X(t)}function X(t){this._node=t,this._names=O(t.getAttribute("class")||"")}function I(t,n){for(var e=j(t),i=-1,r=n.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Nu.exec(t))?pt(parseInt(n[1],16)):(n=ku.exec(t))?new gt(n[1],n[2],n[3],1):(n=Tu.exec(t))?new gt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Cu.exec(t))?dt(n[1],n[2],n[3],n[4]):(n=Su.exec(t))?dt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Au.exec(t))?xt(n[1],n[2]/100,n[3]/100,1):(n=Eu.exec(t))?xt(n[1],n[2]/100,n[3]/100,n[4]):Uu.hasOwnProperty(t)?pt(Uu[t]):"transparent"===t?new gt(NaN,NaN,NaN,0):null}function pt(t){return new gt(t>>16&255,t>>8&255,255&t,1)}function dt(t,n,e,i){return i<=0&&(t=n=e=NaN),new gt(t,n,e,i)}function yt(t){return t instanceof ft||(t=_t(t)),t?(t=t.rgb(),new gt(t.r,t.g,t.b,t.opacity)):new gt}function vt(t,n,e,i){return 1===arguments.length?yt(t):new gt(t,n,e,null==i?1:i)}function gt(t,n,e,i){this.r=+t,this.g=+n,this.b=+e,this.opacity=+i}function xt(t,n,e,i){return i<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new bt(t,n,e,i)}function mt(t){if(t instanceof bt)return new bt(t.h,t.s,t.l,t.opacity);if(t instanceof ft||(t=_t(t)),!t)return new bt;if(t instanceof bt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,i=t.b/255,r=Math.min(n,e,i),o=Math.max(n,e,i),u=NaN,a=o-r,s=(o+r)/2;return a?(u=n===o?(e-i)/a+6*(e0&&s<1?0:u,new bt(u,a,s,t.opacity)}function wt(t,n,e,i){return 1===arguments.length?mt(t):new bt(t,n,e,null==i?1:i)}function bt(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Mt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Nt(t){if(t instanceof Tt)return new Tt(t.l,t.a,t.b,t.opacity);if(t instanceof Pt){var n=t.h*Lu;return new Tt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof gt||(t=yt(t));var e=Et(t.r),i=Et(t.g),r=Et(t.b),o=Ct((.4124564*e+.3575761*i+.1804375*r)/Du),u=Ct((.2126729*e+.7151522*i+.072175*r)/Ru);return new Tt(116*u-16,500*(o-u),200*(u-Ct((.0193339*e+.119192*i+.9503041*r)/Fu)),t.opacity)}function kt(t,n,e,i){return 1===arguments.length?Nt(t):new Tt(t,n,e,null==i?1:i)}function Tt(t,n,e,i){this.l=+t,this.a=+n,this.b=+e,this.opacity=+i}function Ct(t){return t>zu?Math.pow(t,1/3):t/Hu+Yu}function St(t){return t>qu?t*t*t:Hu*(t-Yu)}function At(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Et(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Ut(t){if(t instanceof Pt)return new Pt(t.h,t.c,t.l,t.opacity);t instanceof Tt||(t=Nt(t));var n=Math.atan2(t.b,t.a)*Pu;return new Pt(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Lt(t,n,e,i){return 1===arguments.length?Ut(t):new Pt(t,n,e,null==i?1:i)}function Pt(t,n,e,i){this.h=+t,this.c=+n,this.l=+e,this.opacity=+i}function Dt(t){if(t instanceof Ft)return new Ft(t.h,t.s,t.l,t.opacity);t instanceof gt||(t=yt(t));var n=t.r/255,e=t.g/255,i=t.b/255,r=(Zu*i+Bu*n-Vu*e)/(Zu+Bu-Vu),o=i-r,u=($u*(e-r)-Xu*o)/Iu,a=Math.sqrt(u*u+o*o)/($u*r*(1-r)),s=a?Math.atan2(u,o)*Pu-120:NaN;return new Ft(s<0?s+360:s,a,r,t.opacity)}function Rt(t,n,e,i){return 1===arguments.length?Dt(t):new Ft(t,n,e,null==i?1:i)}function Ft(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Yt(t,n){return function(e){return t+e*n}}function qt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(i){return Math.pow(t+i*n,e)}}function Ht(t,n){var e=n-t;return e?Yt(t,e>180||e<-180?e-360*Math.round(e/360):e):Ku(isNaN(t)?n:t)}function zt(t){return 1==(t=+t)?Ot:function(n,e){return e-n?qt(n,e,t):Ku(isNaN(n)?e:n)}}function Ot(t,n){var e=n-t;return e?Yt(t,e):Ku(isNaN(t)?n:t)}function jt(t){return function(){return t}}function Xt(t){return function(n){return t(n)+""}}function It(t){return"none"===t?la:(Wu||(Wu=document.createElement("DIV"),Ju=document.documentElement,Gu=document.defaultView),Wu.style.transform=t,t=Gu.getComputedStyle(Ju.appendChild(Wu),null).getPropertyValue("transform"),Ju.removeChild(Wu),t=t.slice(7,-1).split(","),fa(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))}function $t(t){return null==t?la:(Qu||(Qu=document.createElementNS("http://www.w3.org/2000/svg","g")),Qu.setAttribute("transform",t),(t=Qu.transform.baseVal.consolidate())?(t=t.matrix,fa(t.a,t.b,t.c,t.d,t.e,t.f)):la)}function Bt(t,n,e,i){function r(t){return t.length?t.pop()+" ":""}function o(t,i,r,o,u,a){if(t!==r||i!==o){var s=u.push("translate(",null,n,null,e);a.push({i:s-4,x:ia(t,r)},{i:s-2,x:ia(i,o)})}else(r||o)&&u.push("translate("+r+n+o+e)}function u(t,n,e,o){t!==n?(t-n>180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(r(e)+"rotate(",null,i)-2,x:ia(t,n)})):n&&e.push(r(e)+"rotate("+n+i)}function a(t,n,e,o){t!==n?o.push({i:e.push(r(e)+"skewX(",null,i)-2,x:ia(t,n)}):n&&e.push(r(e)+"skewX("+n+i)}function s(t,n,e,i,o,u){if(t!==e||n!==i){var a=o.push(r(o)+"scale(",null,",",null,")");u.push({i:a-4,x:ia(t,e)},{i:a-2,x:ia(n,i)})}else 1===e&&1===i||o.push(r(o)+"scale("+e+","+i+")")}return function(n,e){var i=[],r=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,i,r),u(n.rotate,e.rotate,i,r),a(n.skewX,e.skewX,i,r),s(n.scaleX,n.scaleY,e.scaleX,e.scaleY,i,r),n=e=null,function(t){for(var n,e=-1,o=r.length;++e=0&&n._call.call(null,t),n=n._next;--ga}function Kt(){Ma=(ba=ka.now())+Na,ga=xa=0;try{Qt()}finally{ga=0,nn(),Ma=0}}function tn(){var t=ka.now(),n=t-ba;n>wa&&(Na-=n,ba=t)}function nn(){for(var t,n,e=da,i=1/0;e;)e._call?(i>e._time&&(i=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:da=n);ya=t,en(i)}function en(t){if(!ga){xa&&(xa=clearTimeout(xa));var n=t-Ma;n>24?(t<1/0&&(xa=setTimeout(Kt,n)),ma&&(ma=clearInterval(ma))):(ma||(ba=Ma,ma=setInterval(tn,wa)),ga=1,Ta(Kt))}}function rn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Ea)throw new Error("too late");return e}function on(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>La)throw new Error("too late");return e}function un(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function an(t,n,e){function i(t){e.state=Ua,e.timer.restart(r,e.delay,e.time),e.delay<=t&&r(t-e.delay)}function r(i){var h,c,l,f;if(e.state!==Ua)return u();for(h in s)if(f=s[h],f.name===e.name){if(f.state===Pa)return Ca(r);f.state===Da?(f.state=Fa,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete s[h]):+h=0&&(t=t.slice(0,n)),!t||"start"===t})}function kn(t,n,e){var i,r,o=Nn(n)?rn:on;return function(){var u=o(this,t),a=u.on;a!==i&&(r=(i=a).copy()).on(n,e),u.on=r}}function Tn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Cn(t,n){var e,i,r;return function(){var o=ru(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===i?r:r=n(e=u,i=a)}}function Sn(t){return function(){this.style.removeProperty(t)}}function An(t,n,e){var i,r;return function(){var o=ru(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===i?r:r=n(i=o,e)}}function En(t,n,e){var i,r,o;return function(){var u=ru(this).getComputedStyle(this,null),a=u.getPropertyValue(t),s=e(this);return null==s&&(this.style.removeProperty(t),s=u.getPropertyValue(t)),a===s?null:a===i&&s===r?o:o=n(i=a,r=s)}}function Un(t,n,e){function i(){var i=this,r=n.apply(i,arguments);return r&&function(n){i.style.setProperty(t,r(n),e)}}return i._value=n,i}function Ln(t){return function(){this.textContent=t}}function Pn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Dn(t,n,e,i){this._groups=t,this._parents=n,this._name=e,this._id=i}function Rn(t){return ht().transition(t)}function Fn(){return++os}function Yn(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function qn(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return ss.time=Zt(),ss;return e}function Hn(t){return{type:t}}function zn(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function On(){return new zn}function jn(){}function Xn(t,n){var e=new jn;if(t instanceof jn)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var i,r=-1,o=t.length;if(null==n)for(;++r=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u,r=_,!(_=_[l=c<<1|h]))return r[l]=p,t;if(a=+t._x.call(null,_.data),s=+t._y.call(null,_.data),n===a&&e===s)return p.next=_,r?r[l]=p:t._root=p,t;do{r=r?r[l]=new Array(4):t._root=new Array(4),(h=n>=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u}while((l=c<<1|h)==(f=(s>=u)<<1|a>=o));return r[f]=_,r[l]=p,t}function Jn(t){var n,e,i,r,o=t.length,u=new Array(o),a=new Array(o),s=1/0,h=1/0,c=-(1/0),l=-(1/0);for(e=0;ec&&(c=i),rl&&(l=r));for(c",r=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],s=!!n[7],h=n[8]&&+n[8].slice(1),c=n[9]||"";"n"===c?(s=!0,c="g"):zs[c]||(c=""),(u||"0"===e&&"="===i)&&(u=!0,e="0",i="="),this.fill=e,this.align=i,this.sign=r,this.symbol=o,this.zero=u,this.width=a,this.comma=s,this.precision=h,this.type=c}function re(t){return t}function oe(){this.reset()}function ue(t,n,e){var i=t.s=n+e,r=i-n,o=i-r;t.t=n-o+(e-r)}function ae(t){return t>1?0:t<-1?th:Math.acos(t)}function se(t){return t>1?nh:t<-1?-nh:Math.asin(t)}function he(){}function ce(t){var n=t[0],e=t[1],i=sh(e);return[i*sh(n),i*lh(n),lh(e)]}function le(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function fe(t){var n=fh(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function _e(t,n){return[t>th?t-ih:t<-th?t+ih:t,n]}function pe(t,n,e,i){this.x=t,this.z=n,this.o=e,this.e=i,this.v=!1,this.n=this.p=null}function de(t){if(n=t.length){for(var n,e,i=0,r=t[0];++i1}function me(t,n){return((t=t.x)[0]<0?t[1]-nh-Ks:nh-t[1])-((n=n.x)[0]<0?n[1]-nh-Ks:nh-n[1])}function we(t){var n,e=NaN,i=NaN,r=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?th:-th,s=oh(o-e);oh(s-th)0?nh:-nh),t.point(r,i),t.lineEnd(),t.lineStart(),t.point(a,i),t.point(o,i),n=0):r!==a&&s>=th&&(oh(e-r)Ks?uh((lh(n)*(o=sh(i))*lh(e)-lh(i)*(r=sh(n))*lh(t))/(r*o*u)):(n+i)/2}function Me(t,n,e,i){var r;if(null==t)r=e*nh,i.point(-th,r),i.point(0,r),i.point(th,r),i.point(th,0),i.point(th,-r),i.point(0,-r),i.point(-th,-r),i.point(-th,0),i.point(-th,r);else if(oh(t[0]-n[0])>Ks){var o=t[0]=0;)n+=e[i].value;else n=1;t.value=n}function Ee(t,n){if(t===n)return t;var e=t.ancestors(),i=n.ancestors(),r=null;for(t=e.pop(),n=i.pop();t===n;)r=t,t=e.pop(),n=i.pop();return r}function Ue(t,n){var e,i,r,o,u,a=new Fe(t),s=+t.value&&(a.value=t.value),h=[a];for(null==n&&(n=Pe);e=h.pop();)if(s&&(e.value=+e.data.value),(r=n(e.data))&&(u=r.length))for(e.children=new Array(u),o=u-1;o>=0;--o)h.push(i=e.children[o]=new Fe(r[o])),i.parent=e,i.depth=e.depth+1;return a.eachBefore(Re)}function Le(){return Ue(this).eachBefore(De)}function Pe(t){return t.children}function De(t){t.data=t.data.data}function Re(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Fe(t){this.data=t,this.depth=this.height=0,this.parent=null}function Ye(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function qe(t,n,e,i,r,o){for(var u,a,s,h,c,l,f,_,p,d,y,v=[],g=n.children,x=0,m=0,w=g.length,b=n.value;xf&&(f=a),y=c*c*d,(_=Math.max(f/y,y/l))>p){c-=a;break}p=_}v.push(u={value:c,dice:s=0;)if((e=t._tasks[i])&&(t._tasks[i]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,Xe(t)}function Xe(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function Ie(t){return function(n,e){t(null==n?e:null)}}function $e(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function Be(t,n){return function(e){return t(e.responseText,n)}}function Ve(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(r!==Xh)return r;e.set(o,u=i.push(n))}return t[(u-1)%t.length]}var e=Xn(),i=[],r=Xh;return t=null==t?[]:jh.call(t),n.domain=function(t){if(!arguments.length)return i.slice();i=[],e=Xn();for(var r,o,u=-1,a=t.length;++u=e?1:i(t)}}}function Ge(t){return function(n,e){var i=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:i(t)}}}function Qe(t,n,e,i){var r=t[0],o=t[1],u=n[0],a=n[1];return o2?Ke:Qe,o=u=null,i}function i(n){return(o||(o=r(a,s,c?Je(t):t,h)))(+n)}var r,o,u,a=Bh,s=Bh,h=sa,c=!1;return i.invert=function(t){return(u||(u=r(s,a,We,c?Ge(n):n)))(+t)},i.domain=function(t){return arguments.length?(a=Oh.call(t,$h),e()):a.slice()},i.range=function(t){return arguments.length?(s=jh.call(t),e()):s.slice()},i.rangeRound=function(t){return s=jh.call(t),h=ha,e()},i.clamp=function(t){return arguments.length?(c=!!t,e()):c},i.interpolate=function(t){return arguments.length?(h=t,e()):h},e()}function ei(t){var n=t.domain;return t.ticks=function(t){var e=n();return yo(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Vh(n(),t,e)},t.nice=function(i){var r=n(),o=r.length-1,u=null==i?10:i,a=r[0],s=r[o],h=e(a,s,u);return h&&(h=e(Math.floor(a/h)*h,Math.ceil(s/h)*h,u),r[0]=Math.floor(a/h)*h,r[o]=Math.ceil(s/h)*h,n(r)),t},t}function ii(){var t=ni(We,ia);return t.copy=function(){return ti(t,ii())},ei(t)}function ri(t,n,e,i){function r(n){return t(n=new Date(+n)),n}return r.floor=r,r.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},r.round=function(t){var n=r(t),e=r.ceil(t);return t-n0))return u;do{u.push(new Date(+e))}while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,i){if(t>=t)for(;--i>=0;)for(;n(t,1),!e(t););})},e&&(r.count=function(n,i){return Zh.setTime(+n),Wh.setTime(+i),t(Zh),t(Wh),Math.floor(e(Zh,Wh))},r.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?r.filter(i?function(n){return i(n)%t==0}:function(n){return r.count(0,n)%t==0}):r:null}),r}function oi(t){return ri(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/Qh})}function ui(t){return ri(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Qh})}function ai(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function si(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function hi(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function ci(t){function n(t,n){return function(e){var i,r,o,u=[],a=-1,s=0,h=t.length;for(e instanceof Date||(e=new Date(+e));++a=s)return-1;if(37===(r=n.charCodeAt(u++))){if(r=n.charAt(u++),!(o=j[r in fc?n.charAt(u++):r])||(i=o(t,e,i))<0)return-1}else if(r!=e.charCodeAt(i++))return-1}return i}function r(t,n,e){var i=E.exec(n.slice(e));return i?(t.p=U[i[0].toLowerCase()],e+i[0].length):-1}function o(t,n,e){var i=D.exec(n.slice(e));return i?(t.w=R[i[0].toLowerCase()],e+i[0].length):-1}function u(t,n,e){var i=L.exec(n.slice(e));return i?(t.w=P[i[0].toLowerCase()],e+i[0].length):-1}function a(t,n,e){var i=q.exec(n.slice(e));return i?(t.m=H[i[0].toLowerCase()],e+i[0].length):-1}function s(t,n,e){var i=F.exec(n.slice(e));return i?(t.m=Y[i[0].toLowerCase()],e+i[0].length):-1}function h(t,n,e){return i(t,b,n,e)}function c(t,n,e){return i(t,M,n,e)}function l(t,n,e){return i(t,N,n,e)}function f(t){return C[t.getDay()]}function _(t){return T[t.getDay()]}function p(t){return A[t.getMonth()]}function d(t){return S[t.getMonth()]}function y(t){return k[+(t.getHours()>=12)]}function v(t){return C[t.getUTCDay()]}function g(t){return T[t.getUTCDay()]}function x(t){return A[t.getUTCMonth()]}function m(t){return S[t.getUTCMonth()]}function w(t){return k[+(t.getUTCHours()>=12)]}var b=t.dateTime,M=t.date,N=t.time,k=t.periods,T=t.days,C=t.shortDays,S=t.months,A=t.shortMonths,E=_i(k),U=pi(k),L=_i(T),P=pi(T),D=_i(C),R=pi(C),F=_i(S),Y=pi(S),q=_i(A),H=pi(A),z={a:f,A:_,b:p,B:d,c:null,d:Ai,e:Ai,H:Ei,I:Ui,j:Li,L:Pi,m:Di,M:Ri,p:y,S:Fi,U:Yi,w:qi,W:Hi,x:null,X:null,y:zi,Y:Oi,Z:ji,"%":ir},O={a:v,A:g,b:x,B:m,c:null,d:Xi,e:Xi,H:Ii,I:$i,j:Bi,L:Vi,m:Zi,M:Wi,p:w,S:Ji,U:Gi,w:Qi,W:Ki,x:null,X:null,y:tr,Y:nr,Z:er,"%":ir},j={a:o,A:u,b:a,B:s,c:h,d:bi,e:bi,H:Ni,I:Ni,j:Mi,L:Ci,m:wi,M:ki,p:r,S:Ti,U:yi,w:di,W:vi,x:c,X:l,y:xi,Y:gi,Z:mi,"%":Si};return z.x=n(M,z),z.X=n(N,z),z.c=n(b,z),O.x=n(M,O),O.X=n(N,O),O.c=n(b,O),{format:function(t){var e=n(t+="",z);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",ai);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",O);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,si);return n.toString=function(){return t},n}}}function li(t,n,e){var i=t<0?"-":"",r=(i?-t:t)+"",o=r.length;return i+(o68?1900:2e3),e+i[0].length):-1}function mi(t,n,e){var i=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return i?(t.Z=i[1]?0:-(i[2]+(i[3]||"00")),e+i[0].length):-1}function wi(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.m=i[0]-1,e+i[0].length):-1}function bi(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.d=+i[0],e+i[0].length):-1}function Mi(t,n,e){var i=_c.exec(n.slice(e,e+3));return i?(t.m=0,t.d=+i[0],e+i[0].length):-1}function Ni(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.H=+i[0],e+i[0].length):-1}function ki(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.M=+i[0],e+i[0].length):-1}function Ti(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.S=+i[0],e+i[0].length):-1}function Ci(t,n,e){var i=_c.exec(n.slice(e,e+3));return i?(t.L=+i[0],e+i[0].length):-1}function Si(t,n,e){var i=pc.exec(n.slice(e,e+1));return i?e+i[0].length:-1}function Ai(t,n){return li(t.getDate(),n,2)}function Ei(t,n){return li(t.getHours(),n,2)}function Ui(t,n){return li(t.getHours()%12||12,n,2)}function Li(t,n){return li(1+Kh.count(ec(t),t),n,3)}function Pi(t,n){return li(t.getMilliseconds(),n,3)}function Di(t,n){return li(t.getMonth()+1,n,2)}function Ri(t,n){return li(t.getMinutes(),n,2)}function Fi(t,n){return li(t.getSeconds(),n,2)}function Yi(t,n){return li(tc.count(ec(t),t),n,2)}function qi(t){return t.getDay()}function Hi(t,n){return li(nc.count(ec(t),t),n,2)}function zi(t,n){return li(t.getFullYear()%100,n,2)}function Oi(t,n){return li(t.getFullYear()%1e4,n,4)}function ji(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+li(n/60|0,"0",2)+li(n%60,"0",2)}function Xi(t,n){return li(t.getUTCDate(),n,2)}function Ii(t,n){return li(t.getUTCHours(),n,2)}function $i(t,n){return li(t.getUTCHours()%12||12,n,2)}function Bi(t,n){return li(1+ic.count(uc(t),t),n,3)}function Vi(t,n){return li(t.getUTCMilliseconds(),n,3)}function Zi(t,n){return li(t.getUTCMonth()+1,n,2)}function Wi(t,n){return li(t.getUTCMinutes(),n,2)}function Ji(t,n){return li(t.getUTCSeconds(),n,2)}function Gi(t,n){return li(rc.count(uc(t),t),n,2)}function Qi(t){return t.getUTCDay()}function Ki(t,n){return li(oc.count(uc(t),t),n,2)}function tr(t,n){return li(t.getUTCFullYear()%100,n,2)}function nr(t,n){return li(t.getUTCFullYear()%1e4,n,4)}function er(){return"+0000"}function ir(){return"%"}function rr(t){return t.toISOString()}function or(t){var n=new Date(t);return isNaN(n)?null:n}function ur(t){this._context=t}function ar(t){return t[0]}function sr(t){return t[1]}function hr(t){this._curve=t}function cr(t){function n(n){return new hr(t(n))}return n._curve=t,n}function lr(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function fr(t){this._context=t}function _r(t,n){this._basis=new fr(t),this._beta=n}function pr(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function dr(t,n){this._context=t,this._k=(1-n)/6}function yr(t,n){this._context=t,this._k=(1-n)/6}function vr(t,n){this._context=t,this._k=(1-n)/6}function gr(t,n,e){var i=t._x1,r=t._y1,o=t._x2,u=t._y2;if(t._l01_a>gc){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,s=3*t._l01_a*(t._l01_a+t._l12_a);i=(i*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/s,r=(r*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/s}if(t._l23_a>gc){var h=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*h+t._x1*t._l23_2a-n*t._l12_2a)/c,u=(u*h+t._y1*t._l23_2a-e*t._l12_2a)/c}t._context.bezierCurveTo(i,r,o,u,t._x2,t._y2)}function xr(t,n){this._context=t,this._alpha=n}function mr(t,n){this._context=t,this._alpha=n}function wr(t,n){this._context=t,this._alpha=n}function br(t){return t<0?-1:1}function Mr(t,n,e){var i=t._x1-t._x0,r=n-t._x1,o=(t._y1-t._y0)/(i||r<0&&-0),u=(e-t._y1)/(r||i<0&&-0),a=(o*r+u*i)/(i+r);return(br(o)+br(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Nr(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function kr(t,n,e){var i=t._x0,r=t._y0,o=t._x1,u=t._y1,a=(o-i)/3;t._context.bezierCurveTo(i+a,r+a*n,o-a,u-a*e,o,u)}function Tr(t){this._context=t}function Cr(t){this._context=new Sr(t)}function Sr(t){this._context=t}function Ar(){this._=null}function Er(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function Ur(t,n){var e=n,i=n.R,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.R=i.L,e.R&&(e.R.U=e),i.L=e}function Lr(t,n){var e=n,i=n.L,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.L=i.R,e.L&&(e.L.U=e),i.R=e}function Pr(t){for(;t.L;)t=t.L;return t}function Dr(t,n,e,i){var r=[null,null],o=Tc.push(r)-1;return r.left=t,r.right=n,e&&Fr(r,t,n,e),i&&Fr(r,n,t,i),Nc[t.index].halfedges.push(o),Nc[n.index].halfedges.push(o),r}function Rr(t,n,e){var i=[n,e];return i.left=t,i}function Fr(t,n,e,i){t[0]||t[1]?t.left===e?t[1]=i:t[0]=i:(t[0]=i,t.left=n,t.right=e)}function Yr(t,n,e,i,r){var o,u=t[0],a=t[1],s=u[0],h=u[1],c=a[0],l=a[1],f=0,_=1,p=c-s,d=l-h;if(o=n-s,p||!(o>0)){if(o/=p,p<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=i-s,p||!(o<0)){if(o/=p,p<0){if(o>_)return;o>f&&(f=o)}else if(p>0){if(o0)){if(o/=d,d<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=r-h,d||!(o<0)){if(o/=d,d<0){if(o>_)return;o>f&&(f=o)}else if(d>0){if(o0||_<1)||(f>0&&(t[0]=[s+f*p,h+f*d]),_<1&&(t[1]=[s+_*p,h+_*d]),!0)}}}}}function qr(t,n,e,i,r){var o=t[1];if(o)return!0;var u,a,s=t[0],h=t.left,c=t.right,l=h[0],f=h[1],_=c[0],p=c[1],d=(l+_)/2,y=(f+p)/2;if(p===f){if(d=i)return;if(l>_){if(s){if(s[1]>=r)return}else s=[d,e];o=[d,r]}else{if(s){if(s[1]1)if(l>_){if(s){if(s[1]>=r)return}else s=[(e-a)/u,e];o=[(r-a)/u,r]}else{if(s){if(s[1]=i)return}else s=[n,u*n+a];o=[i,u*i+a]}else{if(s){if(s[0]Ac||Math.abs(r[0][1]-r[1][1])>Ac)||delete Tc[o]}function zr(t){return Nc[t.index]={site:t,halfedges:[]}}function Or(t,n){var e=t.site,i=n.left,r=n.right;return e===r&&(r=i,i=e),r?Math.atan2(r[1]-i[1],r[0]-i[0]):(e===i?(i=n[1],r=n[0]):(i=n[0],r=n[1]),Math.atan2(i[0]-r[0],r[1]-i[1]))}function jr(t,n){return n[+(n.left!==t.site)]}function Xr(t,n){return n[+(n.left===t.site)]}function Ir(){for(var t,n,e,i,r=0,o=Nc.length;rAc||Math.abs(d-f)>Ac)&&(s.splice(a,0,Tc.push(Rr(u,_,Math.abs(p-t)Ac?[t,Math.abs(l-t)Ac?[Math.abs(f-i)Ac?[e,Math.abs(l-e)Ac?[Math.abs(f-n)=-Ec)){var _=s*s+h*h,p=c*c+l*l,d=(l*_-h*p)/f,y=(s*p-c*_)/f,v=Cc.pop()||new Br;v.arc=t,v.site=r,v.x=d+u,v.y=(v.cy=y+a)+Math.sqrt(d*d+y*y),t.circle=v;for(var g=null,x=kc._;x;)if(v.yAc)a=a.L;else{if(!((r=o-no(a,u))>Ac)){i>-Ac?(n=a.P,e=a):r>-Ac?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}zr(t);var s=Jr(t);if(Mc.insert(n,s),n||e){if(n===e)return Zr(n),e=Jr(n.site),Mc.insert(s,e),s.edge=e.edge=Dr(n.site,s.site),Vr(n),void Vr(e);if(!e)return void(s.edge=Dr(n.site,s.site));Zr(n),Zr(e);var h=n.site,c=h[0],l=h[1],f=t[0]-c,_=t[1]-l,p=e.site,d=p[0]-c,y=p[1]-l,v=2*(f*y-_*d),g=f*f+_*_,x=d*d+y*y,m=[(y*g-_*x)/v+c,(f*x-d*g)/v+l];Fr(e.edge,h,p,m),s.edge=Dr(h,t,null,m),e.edge=Dr(t,p,null,m),Vr(n),Vr(e)}}function to(t,n){var e=t.site,i=e[0],r=e[1],o=r-n;if(!o)return i;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],s=e[1],h=s-n;if(!h)return a;var c=a-i,l=1/o-1/h,f=c/h;return l?(-f+Math.sqrt(f*f-2*l*(c*c/(-2*h)-s+h/2+r-o/2)))/l+i:(i+a)/2}function no(t,n){var e=t.N;if(e)return to(e,n);var i=t.site;return i[1]===n?i[0]:1/0}function eo(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function io(t,n){return n[1]-t[1]||n[0]-t[0]}function ro(t,n){var e,i,r,o=t.sort(io).pop();for(Tc=[],Nc=new Array(t.length),Mc=new Ar,kc=new Ar;;)if(r=bc,o&&(!r||o[1]n?1:t>=n?0:NaN},ao=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)<0?i=o+1:r=o}return i},right:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)>0?r=o:i=o+1}return i}}},so=ao(uo),ho=so.right,co=Array.prototype,lo=(co.slice,co.map,function(t,n,e){t=+t,n=+n,e=(r=arguments.length)<2?(n=t,t=0,1):r<3?1:+e;for(var i=-1,r=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(r);++i=0;)for(i=t[r],n=i.length;--n>=0;)e[--u]=i[n];return e},go=Array.prototype.slice,xo=function(t){return t},mo=1,wo=2,bo=3,Mo=4,No=1e-6,ko={value:function(){}};f.prototype=l.prototype={constructor:f,on:function(t,n){var e,i=this._,r=_(t+"",i),o=-1,u=r.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,i,r=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Co.hasOwnProperty(n)?{space:Co[n],local:t}:t},Ao=function(t){var n=So(t);return(n.local?v:y)(n)},Eo=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Uo=document.documentElement;if(!Uo.matches){var Lo=Uo.webkitMatchesSelector||Uo.msMatchesSelector||Uo.mozMatchesSelector||Uo.oMatchesSelector;Eo=function(t){return function(){return Lo.call(this,t)}}}}var Po=Eo,Do={},Ro=null;if("undefined"!=typeof document){"onmouseenter"in document.documentElement||(Do={mouseenter:"mouseover",mouseleave:"mouseout"})}var Fo=function(t,n,e){var i,r,o=m(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?b:w,null==e&&(e=!1),i=0;i=m&&(m=x+1);!(g=y[m])&&++m=0;)(i=r[o])&&(u&&u!==i.nextSibling&&u.parentNode.insertBefore(i,u),u=i);return this},Jo=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=S);for(var e=this._groups,i=e.length,r=new Array(i),o=0;o1?this.each((null==n?R:"function"==typeof n?Y:F)(t,n,null==e?"":e)):ru(i=this.node()).getComputedStyle(i,null).getPropertyValue(t)},uu=function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?z:H)(t,n)):this.node()[t]};X.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var au=function(t,n){var e=O(t+"");if(arguments.length<2){for(var i=j(this.node()),r=-1,o=e.length;++r=240?t-240:t+120,r,i),Mt(t,r,i),Mt(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Lu=Math.PI/180,Pu=180/Math.PI,Du=.95047,Ru=1,Fu=1.08883,Yu=4/29,qu=6/29,Hu=3*qu*qu,zu=qu*qu*qu;xu(Tt,kt,lt(ft,{brighter:function(t){return new Tt(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Tt(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Ru*St(t),n=Du*St(n),e=Fu*St(e),new gt(At(3.2404542*n-1.5371385*t-.4985314*e),At(-.969266*n+1.8760108*t+.041556*e),At(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),xu(Pt,Lt,lt(ft,{brighter:function(t){return new Pt(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Pt(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return Nt(this).rgb()}}));var Ou=-.14861,ju=1.78277,Xu=-.29227,Iu=-.90649,$u=1.97294,Bu=$u*Iu,Vu=$u*ju,Zu=ju*Xu-Iu*Ou;xu(Ft,Rt,lt(ft,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Ft(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Ft(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Lu,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),i=Math.cos(t),r=Math.sin(t);return new gt(255*(n+e*(Ou*i+ju*r)),255*(n+e*(Xu*i+Iu*r)),255*(n+e*($u*i)),this.opacity)}}));var Wu,Ju,Gu,Qu,Ku=function(t){return function(){return t}},ta=function t(n){function e(t,n){var e=i((t=vt(t)).r,(n=vt(n)).r),r=i(t.g,n.g),o=i(t.b,n.b),u=Ot(t.opacity,n.opacity);return function(n){return t.r=e(n),t.g=r(n),t.b=o(n),t.opacity=u(n),t+""}}var i=zt(n);return e.gamma=t,e}(1),na=function(t,n){var e,i=n?n.length:0,r=t?Math.min(i,t.length):0,o=new Array(i),u=new Array(i);for(e=0;eo&&(r=n.slice(o,r),a[u]?a[u]+=r:a[++u]=r),(e=e[0])===(i=i[0])?a[u]?a[u]+=i:a[++u]=i:(a[++u]=null,s.push({i:u,x:ia(e,i)})),o=ua.lastIndex;return oLa&&e.state1e-6)if(Math.abs(c*a-s*h)>1e-6&&r){var f=e-o,_=i-u,p=a*a+s*s,d=f*f+_*_,y=Math.sqrt(p),v=Math.sqrt(l),g=r*Math.tan((cs-Math.acos((p+l-d)/(2*y*v)))/2),x=g/v,m=g/y;Math.abs(x-1)>1e-6&&(this._+="L"+(t+x*h)+","+(n+x*c)),this._+="A"+r+","+r+",0,0,"+ +(c*f>h*_)+","+(this._x1=t+m*a)+","+(this._y1=n+m*s)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,i,r,o){t=+t,n=+n,e=+e;var u=e*Math.cos(i),a=e*Math.sin(i),s=t+u,h=n+a,c=1^o,l=o?i-r:r-i;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+h:(Math.abs(this._x1-s)>1e-6||Math.abs(this._y1-h)>1e-6)&&(this._+="L"+s+","+h),e&&(l>fs?this._+="A"+e+","+e+",0,1,"+c+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=h):(l<0&&(l=l%ls+ls),this._+="A"+e+","+e+",0,"+ +(l>=cs)+","+c+","+(this._x1=t+e*Math.cos(r))+","+(this._y1=n+e*Math.sin(r))))},rect:function(t,n,e,i){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +i+"h"+-e+"Z"},toString:function(){return this._}};jn.prototype=Xn.prototype={constructor:jn,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var _s=Xn.prototype;In.prototype=$n.prototype={constructor:In,has:_s.has,add:function(t){return t+="",this["$"+t]=t,this},remove:_s.remove,clear:_s.clear,values:_s.keys,size:_s.size,empty:_s.empty,each:_s.each};var ps=function(t){function n(t,n){var i,r,o=e(t,function(t,e){if(i)return i(t,e-1);r=t,i=n?Vn(t,n):Bn(t)});return o.columns=r,o}function e(t,n){function e(){if(c>=h)return u;if(r)return r=!1,o;var n,e=c;if(34===t.charCodeAt(e)){for(var i=e;i++t||t>r||i>n||n>o))return this;var u,a,s=r-e,h=this._root;switch(a=(n<(i+o)/2)<<1|t<(e+r)/2){case 0:do{u=new Array(4),u[a]=h,h=u}while(s*=2,r=e+s,o=i+s,t>r||n>o);break;case 1:do{u=new Array(4),u[a]=h,h=u}while(s*=2,e=r-s,o=i+s,e>t||n>o);break;case 2:do{u=new Array(4),u[a]=h,h=u}while(s*=2,r=e+s,i=o-s,t>r||i>n);break;case 3:do{u=new Array(4),u[a]=h,h=u}while(s*=2,e=r-s,i=o-s,e>t||i>n)}this._root&&this._root.length&&(this._root=h)}return this._x0=e,this._y0=i,this._x1=r,this._y1=o,this},ws=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},bs=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},Ms=function(t,n,e,i,r){this.node=t,this.x0=n,this.y0=e,this.x1=i,this.y1=r},Ns=function(t,n,e){var i,r,o,u,a,s,h,c=this._x0,l=this._y0,f=this._x1,_=this._y1,p=[],d=this._root;for(d&&p.push(new Ms(d,c,l,f,_)),null==e?e=1/0:(c=t-e,l=n-e,f=t+e,_=n+e,e*=e);s=p.pop();)if(!(!(d=s.node)||(r=s.x0)>f||(o=s.y0)>_||(u=s.x1)=v)<<1|t>=y)&&(s=p[p.length-1],p[p.length-1]=p[p.length-1-h],p[p.length-1-h]=s)}else{var g=t-+this._x.call(null,d.data),x=n-+this._y.call(null,d.data),m=g*g+x*x;if(m=(a=(p+y)/2))?p=a:y=a,(c=u>=(s=(d+v)/2))?d=s:v=s,n=_,!(_=_[l=c<<1|h]))return this;if(!_.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,f=l)}for(;_.data!==t;)if(i=_,!(_=_.next))return this;return(r=_.next)&&delete _.next,i?(r?i.next=r:delete i.next,this):n?(r?n[l]=r:delete n[l],(_=n[0]||n[1]||n[2]||n[3])&&_===(n[3]||n[2]||n[1]||n[0])&&!_.length&&(e?e[f]=_:this._root=_),this):(this._root=r,this)},Ts=function(){return this._root},Cs=function(){var t=0;return this.visit(function(n){if(!n.length)do{++t}while(n=n.next)}),t},Ss=function(t){var n,e,i,r,o,u,a=[],s=this._root;for(s&&a.push(new Ms(s,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(s=n.node,i=n.x0,r=n.y0,o=n.x1,u=n.y1)&&s.length){var h=(i+o)/2,c=(r+u)/2;(e=s[3])&&a.push(new Ms(e,h,c,o,u)),(e=s[2])&&a.push(new Ms(e,i,c,h,u)),(e=s[1])&&a.push(new Ms(e,h,r,o,c)),(e=s[0])&&a.push(new Ms(e,i,r,h,c))}return this},As=function(t){var n,e=[],i=[];for(this._root&&e.push(new Ms(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var r=n.node;if(r.length){var o,u=n.x0,a=n.y0,s=n.x1,h=n.y1,c=(u+s)/2,l=(a+h)/2;(o=r[0])&&e.push(new Ms(o,u,a,c,l)),(o=r[1])&&e.push(new Ms(o,c,a,s,l)),(o=r[2])&&e.push(new Ms(o,u,l,c,h)),(o=r[3])&&e.push(new Ms(o,c,l,s,h))}i.push(n)}for(;n=i.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},Es=function(t){return arguments.length?(this._x=t,this):this._x},Us=function(t){return arguments.length?(this._y=t,this):this._y},Ls=te.prototype=ne.prototype;Ls.copy=function(){var t,n,e=new ne(this._x,this._y,this._x0,this._y0,this._x1,this._y1),i=this._root;if(!i)return e;if(!i.length)return e._root=ee(i),e;for(t=[{source:i,target:e._root=new Array(4)}];i=t.pop();)for(var r=0;r<4;++r)(n=i.source[r])&&(n.length?t.push({source:n,target:i.target[r]=new Array(4)}):i.target[r]=ee(n));return e},Ls.add=xs,Ls.addAll=Jn,Ls.cover=ms,Ls.data=ws,Ls.extent=bs,Ls.find=Ns,Ls.remove=ks,Ls.removeAll=Gn,Ls.root=Ts,Ls.size=Cs,Ls.visit=Ss,Ls.visitAfter=As,Ls.x=Es,Ls.y=Us;var Ps,Ds=(Math.PI,Math.sqrt(5),function(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,i=t.slice(0,e);return[i.length>1?i[0]+i.slice(2):i,+t.slice(e+1)]}),Rs=function(t){return t=Ds(Math.abs(t)),t?t[1]:NaN},Fs=function(t,n){return function(e,i){for(var r=e.length,o=[],u=0,a=t[0],s=0;r>0&&a>0&&(s+a+1>i&&(a=Math.max(1,i-s)),o.push(e.substring(r-=a,r+a)),!((s+=a+1)>i));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},Ys=function(t,n){t=t.toPrecision(n);t:for(var e,i=t.length,r=1,o=-1;r0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},qs=function(t,n){var e=Ds(t,n);if(!e)return t+"";var i=e[0],r=e[1],o=r-(Ps=3*Math.max(-8,Math.min(8,Math.floor(r/3))))+1,u=i.length;return o===u?i:o>u?i+new Array(o-u+1).join("0"):o>0?i.slice(0,o)+"."+i.slice(o):"0."+new Array(1-o).join("0")+Ds(t,Math.max(0,n+o-1))[0]},Hs=function(t,n){var e=Ds(t,n);if(!e)return t+"";var i=e[0],r=e[1];return r<0?"0."+new Array(-r).join("0")+i:i.length>r+1?i.slice(0,r+1)+"."+i.slice(r+1):i+new Array(r-i.length+2).join("0")},zs={"":Ys,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Hs(100*t,n)},r:Hs,s:qs,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Os=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i,js=function(t){return new ie(t)};ie.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Xs,Is,$s,Bs=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],Vs=function(t){function n(t){function n(t){var n,r,s,g=p,x=d;if("c"===_)x=y(t)+x,t="";else{t=+t;var m=(t<0||1/t<0)&&(t*=-1,!0);if(t=y(t,f),m)for(n=-1,r=t.length,m=!1;++n(s=t.charCodeAt(n))||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}l&&!h&&(t=i(t,1/0));var w=g.length+t.length+x.length,b=w>1)+g+t+x+b.slice(w)}return b+g+t+x}t=js(t);var e=t.fill,u=t.align,a=t.sign,s=t.symbol,h=t.zero,c=t.width,l=t.comma,f=t.precision,_=t.type,p="$"===s?r[0]:"#"===s&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",d="$"===s?r[1]:/[%p]/.test(_)?"%":"",y=zs[_],v=!_||/[defgprs%]/.test(_);return f=null==f?_?6:12:/[gprs]/.test(_)?Math.max(1,Math.min(21,f)):Math.max(0,Math.min(20,f)),n.toString=function(){return t+""},n}function e(t,e){var i=n((t=js(t),t.type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Rs(e)/3))),o=Math.pow(10,-r),u=Bs[8+r/3];return function(t){return i(o*t)+u}}var i=t.grouping&&t.thousands?Fs(t.grouping,t.thousands):re,r=t.currency,o=t.decimal;return{format:n,formatPrefix:e}};!function(t){Xs=Vs(t),Is=Xs.format,$s=Xs.formatPrefix,Xs}({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Zs=function(t){return Math.max(0,-Rs(Math.abs(t)))},Ws=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Rs(n)/3)))-Rs(Math.abs(t)))},Js=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Rs(n)-Rs(t))+1},Gs=function(){return new oe};oe.prototype={constructor:oe,reset:function(){this.s=this.t=0},add:function(t){ue(Qs,t,this.t),ue(this,Qs.s,this.s),this.s?this.t+=Qs.t:this.s=Qs.t},valueOf:function(){return this.s}};var Qs=new oe,Ks=1e-6,th=Math.PI,nh=th/2,eh=th/4,ih=2*th,rh=th/180,oh=Math.abs,uh=Math.atan,ah=Math.atan2,sh=Math.cos,hh=(Math.ceil,Math.exp),ch=Math.log,lh=(Math.pow,Math.sin),fh=(Math.sign,Math.sqrt),_h=Math.tan;Gs(),Gs(),Gs();_e.invert=_e;var ph=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:he,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},dh=function(t,n){return oh(t[0]-n[0])=0;--o)r.point((c=h[o])[0],c[1]);else i(f.x,f.p.x,-1,r);f=f.p}f=f.o,h=f.z,_=!_}while(!f.v);r.lineEnd()}}},vh=(Gs(),Gs(),Gs(),1/0),gh=-vh;ye.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,ih)}},result:he};Gs();ve.prototype={_circle:ge(4.5),pointRadius:function(t){return this._circle=ge(t),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}}};var xh=Gs(),mh=function(t,n){var e=n[0],i=n[1],r=[lh(e),-sh(e),0],o=0,u=0;xh.reset();for(var a=0,s=t.length;a=0?1:-1,N=M*b,k=N>th,T=p*m;if(xh.add(ah(T*M*lh(N),d*w+T*sh(N))),o+=k?b+M*ih:b,k^f>=e^g>=e){var C=le(ce(l),ce(v));fe(C);var S=le(r,C);fe(S);var A=(k^b>=0?-1:1)*se(S[2]);(i>A||i===A&&(C[0]||C[1]))&&(u+=k^b>=0?1:-1)}}return(o<-Ks||o0){for(m||(o.polygonStart(),m=!0),o.lineStart(),t=0;t1&&2&r&&u.push(u.pop().concat(u.shift())),p.push(u.filter(xe))}var _,p,d,y=n(o),v=r.invert(i[0],i[1]),g=ph(),x=n(g),m=!1,w={point:u,lineStart:s,lineEnd:h,polygonStart:function(){w.point=c,w.lineStart=l,w.lineEnd=f,p=[],_=[]},polygonEnd:function(){w.point=u,w.lineStart=s,w.lineEnd=h,p=vo(p);var t=mh(_,v);p.length?(m||(o.polygonStart(),m=!0),yh(p,me,t,e,o)):t&&(m||(o.polygonStart(),m=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),m&&(o.polygonEnd(),m=!1),p=_=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return w}};wh(function(){return!0},we,Me,[-th,-nh]);ke.prototype={constructor:ke,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};sh(30*rh),Ne({point:function(t,n){this.stream.point(t*rh,n*rh)}});Te(function(t){return fh(2/(1+t))}).invert=Ce(function(t){return 2*se(t/2)}),Te(function(t){return(t=ae(t))&&t/lh(t)}).invert=Ce(function(t){return t}),Se.invert=function(t,n){return[-n,2*uh(hh(t))-nh]};var bh=function(){return this.eachAfter(Ae)},Mh=function(t){var n,e,i,r,o=this,u=[o];do{for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(i=0,r=e.length;i=0;--e)r.push(n[e]);return this},kh=function(t){for(var n,e,i,r=this,o=[r],u=[];r=o.pop();)if(u.push(r),n=r.children)for(e=0,i=n.length;e=0;)e+=i[r].value;n.value=e})},Ch=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Sh=function(t){for(var n=this,e=Ee(n,t),i=[n];n!==e;)n=n.parent,i.push(n);for(var r=i.length;t!==e;)i.splice(r,0,t),t=t.parent;return i},Ah=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},Eh=function(){var t=[];return this.each(function(n){t.push(n)}),t},Uh=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Lh=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Fe.prototype=Ue.prototype={constructor:Fe,count:bh,each:Mh,eachAfter:kh,eachBefore:Nh,sum:Th,sort:Ch,path:Sh,ancestors:Ah,descendants:Eh,leaves:Uh,links:Lh,copy:Le};var Ph=function(t,n,e,i,r){for(var o,u=t.children,a=-1,s=u.length,h=t.value&&(i-n)/t.value;++a1?n:1)},e}(Rh);!function t(n){function e(t,e,i,r,o){if((u=t._squarify)&&u.ratio===n)for(var u,a,s,h,c,l=-1,f=u.length,_=t.value;++l1?n:1)},e}(Rh);var Fh=([].slice,{}),Yh=function(t,n){function e(t){var n,e=h.status;if(!e&&$e(h)||e>=200&&e<300||304===e){if(o)try{n=o.call(i,h)}catch(t){return void a.call("error",i,t)}else n=h;a.call("load",i,n)}else a.call("error",i,t)}var i,r,o,u,a=l("beforesend","progress","load","error"),s=Xn(),h=new XMLHttpRequest,c=null,f=null,_=0;if("undefined"==typeof XDomainRequest||"withCredentials"in h||!/^(http(s)?:)?\/\//.test(t)||(h=new XDomainRequest),"onload"in h?h.onload=h.onerror=h.ontimeout=e:h.onreadystatechange=function(t){h.readyState>3&&e(t)},h.onprogress=function(t){a.call("progress",i,t)},i={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?s.get(t):(null==n?s.remove(t):s.set(t,n+""),i)},mimeType:function(t){return arguments.length?(r=null==t?null:t+"",i):r},responseType:function(t){return arguments.length?(u=t,i):u},timeout:function(t){return arguments.length?(_=+t,i):_},user:function(t){return arguments.length<1?c:(c=null==t?null:t+"",i)},password:function(t){return arguments.length<1?f:(f=null==t?null:t+"",i)},response:function(t){return o=t,i},get:function(t,n){return i.send("GET",t,n)},post:function(t,n){return i.send("POST",t,n)},send:function(n,e,o){return h.open(n,t,!0,c,f),null==r||s.has("accept")||s.set("accept",r+",*/*"),h.setRequestHeader&&s.each(function(t,n){h.setRequestHeader(n,t)}),null!=r&&h.overrideMimeType&&h.overrideMimeType(r),null!=u&&(h.responseType=u),_>0&&(h.timeout=_),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=Ie(o)),null!=o&&i.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",i,h),h.send(null==e?null:e),i},abort:function(){return h.abort(),i},on:function(){var t=a.on.apply(a,arguments);return t===a?i:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return i.get(n)}return i},qh=function(t,n){return function(e,i){var r=Yh(e).mimeType(t).response(n);if(null!=i){if("function"!=typeof i)throw new Error("invalid callback: "+i);return r.get(i)}return r}};qh("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),qh("application/json",function(t){return JSON.parse(t.responseText)}),qh("text/plain",function(t){return t.responseText}),qh("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n});var Hh=function(t,n){return function(e,i,r){arguments.length<3&&(r=i,i=null);var o=Yh(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(Be(n,i=t)):i},o.row(i),r?o.get(r):o}};Hh("text/csv",ys),Hh("text/tab-separated-values",gs);var zh=Array.prototype,Oh=zh.map,jh=zh.slice,Xh={name:"implicit"},Ih=function(t){return function(){return t}},$h=function(t){return+t},Bh=[0,1],Vh=function(t,n,i){var r,o=t[0],u=t[t.length-1],a=e(o,u,null==n?10:n);switch(i=js(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(o),Math.abs(u));return null!=i.precision||isNaN(r=Ws(a,s))||(i.precision=r),$s(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(r=Js(a,Math.max(Math.abs(o),Math.abs(u))))||(i.precision=r-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(r=Zs(a))||(i.precision=r-2*("%"===i.type))}return Is(i)},Zh=new Date,Wh=new Date,Jh=ri(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});Jh.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?ri(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Jh:null};var Gh=6e4,Qh=6048e5,Kh=(ri(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),ri(function(t){t.setTime(Math.floor(t/Gh)*Gh)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getMinutes()}),ri(function(t){var n=t.getTimezoneOffset()*Gh%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),ri(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/864e5},function(t){return t.getDate()-1})),tc=oi(0),nc=oi(1),ec=(oi(2),oi(3),oi(4),oi(5),oi(6),ri(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ri(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()}));ec.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ri(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var ic=(ri(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getUTCMinutes()}),ri(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),ri(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1})),rc=ui(0),oc=ui(1),uc=(ui(2),ui(3),ui(4),ui(5),ui(6),ri(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),ri(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()}));uc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ri(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var ac,sc,hc,cc,lc,fc={"-":"",_:" ",0:"0"},_c=/^\s*\d+/,pc=/^%/,dc=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;!function(t){ac=ci(t),sc=ac.format,hc=ac.parse,cc=ac.utcFormat,lc=ac.utcParse,ac}({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var yc=(Date.prototype.toISOString||cc("%Y-%m-%dT%H:%M:%S.%LZ"),+new Date("2000-01-01T00:00:00.000Z")||lc("%Y-%m-%dT%H:%M:%S.%LZ"),function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})});yc("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),yc("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),yc("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),yc("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),va(Rt(300,.5,0),Rt(-240,.5,1));var vc=(va(Rt(-100,.75,.35),Rt(80,1.5,.8)),va(Rt(260,.75,.35),Rt(80,1.5,.8)),Rt(),function(t){return function(){return t}}),gc=1e-12;Math.PI;ur.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var xc=function(t){return new ur(t)},mc=function(){function t(t){var a,s,h,c=t.length,l=!1;for(null==r&&(u=o(h=On())),a=0;a<=c;++a)!(a0)for(var i,r=t[0],o=n[0],u=t[e]-r,a=n[e]-o,s=-1;++s<=e;)i=s/e,this._basis.point(this._beta*t[s]+(1-this._beta)*(r+i*u),this._beta*n[s]+(1-this._beta)*(o+i*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}},function t(n){function e(t){return 1===n?new fr(t):new _r(t,n)}return e.beta=function(n){return t(+n)},e}(.85),dr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:pr(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new dr(t,n)}return e.tension=function(n){return t(+n)},e}(0),yr.prototype={areaStart:wc,areaEnd:wc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new yr(t,n)}return e.tension=function(n){return t(+n)},e}(0),vr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new vr(t,n)}return e.tension=function(n){return t(+n)},e}(0),xr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new xr(t,n):new dr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),mr.prototype={areaStart:wc,areaEnd:wc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new mr(t,n):new yr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),wr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new wr(t,n):new vr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),Tr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:kr(this,this._t0,Nr(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,kr(this,Nr(this,e=Mr(this,t,n)),e);break;default:kr(this,this._t0,e=Mr(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Cr.prototype=Object.create(Tr.prototype)).point=function(t,n){Tr.prototype.point.call(this,n,t)},Sr.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,i,r,o){this._context.bezierCurveTo(n,t,i,e,o,r)}};Array.prototype.slice;Ar.prototype={constructor:Ar,insert:function(t,n){var e,i,r;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Pr(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)i=e.U,e===i.L?(r=i.R,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.R&&(Ur(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Lr(this,i))):(r=i.L,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.L&&(Lr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Ur(this,i))),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,i,r=t.U,o=t.L,u=t.R;if(e=o?u?Pr(u):o:u,r?r.L===t?r.L=e:r.R=e:this._=e,o&&u?(i=e.C,e.C=t.C,e.L=o,o.U=e,e!==u?(r=e.U,e.U=t.U,t=e.R,r.L=t,e.R=u,u.U=e):(e.U=r,r=e,t=e.R)):(i=t.C,t=e),t&&(t.U=r),!i){if(t&&t.C)return void(t.C=!1);do{if(t===this._)break;if(t===r.L){if(n=r.R,n.C&&(n.C=!1,r.C=!0,Ur(this,r),n=r.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,Lr(this,n),n=r.R),n.C=r.C,r.C=n.R.C=!1,Ur(this,r),t=this._;break}}else if(n=r.L,n.C&&(n.C=!1,r.C=!0,Lr(this,r),n=r.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,Ur(this,n),n=r.L),n.C=r.C,r.C=n.L.C=!1,Lr(this,r),t=this._;break}n.C=!0,t=r,r=r.U}while(!t.C);t&&(t.C=!1)}}};var bc,Mc,Nc,kc,Tc,Cc=[],Sc=[],Ac=1e-6,Ec=1e-12;ro.prototype={constructor:ro,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return jr(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,i){if(o=(r=e.halfedges).length)for(var r,o,u,a=e.site,s=-1,h=n[r[o-1]],c=h.left===a?h.right:h.left;++s=a)return null;var s=t-r.site[0],h=n-r.site[1],c=s*s+h*h;do{r=o.cells[i=u],u=null,r.halfedges.forEach(function(e){var i=o.edges[e],a=i.left;if(a!==r.site&&a||(a=i.right)){var s=t-a[0],h=n-a[1],l=s*s+h*h;l{this.state.language.translate(term)}, event.currentTarget.getBoundingClientRect(), diff --git a/src/app/components/BarChart.jsx b/src/app/components/BarChart.jsx index 8ab59851..801e9f88 100644 --- a/src/app/components/BarChart.jsx +++ b/src/app/components/BarChart.jsx @@ -44,7 +44,7 @@ export default class BarChart extends TranslatedComponent { unit: '' }; - static PropTypes = { + static propTypes = { colors: React.PropTypes.array, data: React.PropTypes.array.isRequired, desc: React.PropTypes.bool, diff --git a/src/app/components/Boost.jsx b/src/app/components/Boost.jsx new file mode 100644 index 00000000..10e60927 --- /dev/null +++ b/src/app/components/Boost.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import ShipSelector from './ShipSelector'; +import { nameComparator } from '../utils/SlotFunctions'; +import { Pip } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; +import Module from '../shipyard/Module'; + +/** + * Boost displays a boost button that toggles bosot + * Requires an onChange() function of the form onChange(boost) which is triggered whenever the boost changes. + */ +export default class Boost extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + boost: React.PropTypes.bool.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + const { ship, boost } = props; + + this._keyDown = this._keyDown.bind(this); + this._toggleBoost = this._toggleBoost.bind(this); + } + + /** + * Add listeners after mounting + */ + componentDidMount() { + document.addEventListener('keydown', this._keyDown); + } + + /** + * Remove listeners before unmounting + */ + componentWillUnmount() { + document.removeEventListener('keydown', this._keyDown); + } + + /** + * Handle Key Down + * @param {Event} e Keyboard Event + */ + _keyDown(e) { + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + switch (e.keyCode) { + case 66: // b == boost + if (this.props.ship.canBoost()) { + e.preventDefault(); + this._toggleBoost(); + } + break; + } + } + } + + /** + * Toggle the boost feature + */ + _toggleBoost() { + this.props.onChange(!this.props.boost); + } + + /** + * Render boost + * @return {React.Component} contents + */ + render() { + const { formats, translate, units } = this.context.language; + const { ship, boost } = this.props; + + // TODO disable if ship cannot boost + return ( + + + + ); + } +} diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx new file mode 100644 index 00000000..96bbac83 --- /dev/null +++ b/src/app/components/Cargo.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Cargo slider + * Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change + */ +export default class Cargo extends TranslatedComponent { + static propTypes = { + cargo: React.PropTypes.number.isRequired, + cargoCapacity: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._cargoChange = this._cargoChange.bind(this); + } + + /** + * Update cargo level + * @param {number} cargoLevel percentage level from 0 to 1 + */ + _cargoChange(cargoLevel) { + const { cargo, cargoCapacity } = this.props; + if (cargoCapacity > 0) { + // We round the cargo to whole number of tonnes + const newCargo = Math.round(cargoLevel * cargoCapacity); + if (newCargo != cargo) { + this.props.onChange(newCargo); + } + } + } + + /** + * Render cargo slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { cargo, cargoCapacity } = this.props; + + return ( + +

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index 2df44855..f0dc5bfd 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -12,7 +12,7 @@ import TranslatedComponent from './TranslatedComponent'; */ export default class CostSection extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, buildName: React.PropTypes.string diff --git a/src/app/components/DamageDealt.jsx b/src/app/components/DamageDealt.jsx index bb4bd484..d1d0fbe6 100644 --- a/src/app/components/DamageDealt.jsx +++ b/src/app/components/DamageDealt.jsx @@ -50,9 +50,8 @@ export function weaponComparator(translate, propComparator, desc) { * Damage against a selected ship */ export default class DamageDealt extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired }; @@ -556,7 +555,6 @@ export default class DamageDealt extends TranslatedComponent {

{translate('sustained dps against standard shields')}

{translate('sustained dps against standard armour')}

0) shieldTooltipDetails.push(
{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
); + if (shield.boosters > 0) shieldTooltipDetails.push(
{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
); + if (shield.cells > 0) shieldTooltipDetails.push(
{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
); + + shieldAbsoluteTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.absolute.generator)}
); + shieldAbsoluteTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.absolute.boosters)}
); + shieldAbsoluteTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}
); + + shieldExplosiveTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}
); + shieldExplosiveTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}
); + shieldExplosiveTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}
); + + shieldKineticTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}
); + shieldKineticTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}
); + shieldKineticTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}
); + + shieldThermalTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}
); + shieldThermalTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}
); + shieldThermalTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}
); + + const effectiveAbsoluteShield = shield.total / shield.absolute.total; + effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute') }); + const effectiveExplosiveShield = shield.total / shield.explosive.total; + effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive') }); + const effectiveKineticShield = shield.total / shield.kinetic.total; + effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic') }); + const effectiveThermalShield = shield.total / shield.thermal.total; + effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal') }); + + shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute'), tooltip: shieldAbsoluteTooltipDetails }); + shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive'), tooltip: shieldExplosiveTooltipDetails }); + shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic'), tooltip: shieldKineticTooltipDetails }); + shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal'), tooltip: shieldThermalTooltipDetails }); + + maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max); + } + + const armourSourcesData = []; + armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); + armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') }); + + const armourTooltipDetails = []; + if (armour.bulkheads > 0) armourTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
); + if (armour.reinforcement > 0) armourTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
); + + const armourAbsoluteTooltipDetails = []; + armourAbsoluteTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}
); + armourAbsoluteTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}
); + + const armourExplosiveTooltipDetails = []; + armourExplosiveTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}
); + armourExplosiveTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}
); + + const armourKineticTooltipDetails = []; + armourKineticTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}
); + armourKineticTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}
); + + const armourThermalTooltipDetails = []; + armourThermalTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}
); + armourThermalTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}
); + + const effectiveArmourData = []; + const effectiveAbsoluteArmour = armour.total / armour.absolute.total; + effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute') }); + const effectiveExplosiveArmour = armour.total / armour.explosive.total; + effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive') }); + const effectiveKineticArmour = armour.total / armour.kinetic.total; + effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic') }); + const effectiveThermalArmour = armour.total / armour.thermal.total; + effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal') }); + + const armourDamageTakenData = []; + armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourAbsoluteTooltipDetails }); + armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourExplosiveTooltipDetails }); + armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourKineticTooltipDetails }); + armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourThermalTooltipDetails }); + + return ( + + {shield.total ? +
+

{translate('shield metrics')}

+
+

{shieldTooltipDetails}

)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}
{formats.int(shield.total)}{units.MJ} +

{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}
{shielddamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}

+

{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}
{shield.recover === Math.Inf ? translate('never') : formats.time(shield.recover)}

+

{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}
{shield.recharge === Math.Inf ? translate('never') : formats.time(shield.recharge)}

+
+
+

{translate('shield sources')}

+ +
+
+

{translate('damage taken')}(%)

+ +
+
+

{translate('effective shield')}(MJ)

+ +
+ : null } + +
+

{translate('armour metrics')}

+

{armourTooltipDetails}

)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}
{formats.int(armour.total)} +

{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}
{armourdamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}

+

{translate('raw module armour')}
{formats.int(armour.modulearmour)}

+

{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}
{formats.pct1(armour.moduleprotection / 2)}

+

{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}
{formats.pct1(armour.moduleprotection)}

+
+ +
+

{translate('armour sources')}

+ +
+
+

{translate('damage taken')}(%)

+ +
+
+

{translate('effective armour')}

+ +
+ ); + } +} diff --git a/src/app/components/DefenceSummary.jsx b/src/app/components/DefenceSummary.jsx index 66cfd5de..93b230b0 100644 --- a/src/app/components/DefenceSummary.jsx +++ b/src/app/components/DefenceSummary.jsx @@ -7,7 +7,7 @@ import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; * Defence summary */ export default class DefenceSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx new file mode 100644 index 00000000..2a3bdfd1 --- /dev/null +++ b/src/app/components/EngagementRange.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Engagement range slider + * Requires an onChange() function of the form onChange(range), providing the range in metres, which is triggered on range change + */ +export default class EngagementRange extends TranslatedComponent { + static propTypes = { + ship: React.PropTypes.object.isRequired, + engagementRange: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + const { ship } = props; + + const maxRange = this._calcMaxRange(ship); + + this.state = { + maxRange + }; + } + + /** + * Calculate the maximum range of a ship's weapons + * @param {Object} ship The ship + * @returns {int} The maximum range, in metres + */ + _calcMaxRange(ship) { + let maxRange = 1000; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const thisRange = ship.hardpoints[i].m.getRange(); + if (thisRange > maxRange) { + maxRange = thisRange; + } + } + } + + return maxRange; + } + + /** + * Update range + * @param {number} rangeLevel percentage level from 0 to 1 + */ + _rangeChange(rangeLevel) { + const { maxRange } = this.state; + + // We round the range to an integer value + const range = Math.round(rangeLevel * maxRange); + + if (range !== this.props.engagementRange) { + this.props.onChange(range); + } + } + + /** + * Render range slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { engagementRange } = this.props; + const { maxRange } = this.state; + + return ( + +

{translate('engagement range')}: {formats.int(engagementRange)}{translate('m')}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/EngineProfile.jsx b/src/app/components/EngineProfile.jsx index b8f874c3..8c9ddf0f 100644 --- a/src/app/components/EngineProfile.jsx +++ b/src/app/components/EngineProfile.jsx @@ -13,10 +13,13 @@ import * as Calc from '../shipyard/Calculations'; * Engine profile for a given ship */ export default class EngineProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, - code: React.PropTypes.string.isRequired + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + boost: React.PropTypes.bool.isRequired, + marker: React.PropTypes.string.isRequired }; /** @@ -30,8 +33,7 @@ export default class EngineProfile extends TranslatedComponent { const ship = this.props.ship; this.state = { - cargo: ship.cargoCapacity, - calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, ship) + calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, ship, this.props.eng, this.props.boost) }; } @@ -42,36 +44,23 @@ export default class EngineProfile extends TranslatedComponent { * @return {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, nextProps.ship) }); + if (nextProps.marker != this.props.marker) { + this.setState({ calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, nextProps.ship, nextProps.eng, nextProps.boost) }); } return true; } /** - * Calculate the maximum speed for this ship across its applicable mass + * Calculate the top speed for this ship given thrusters, mass and pips to ENG * @param {Object} ship The ship + * @param {Object} eng The number of pips to ENG + * @param {Object} boost If boost is enabled * @param {Object} mass The mass at which to calculate the top speed * @return {number} The maximum speed */ - _calcMaxSpeed(ship, mass) { - // Obtain the thrusters for this ship - const thrusters = ship.standard[1].m; - + calcMaxSpeed(ship, eng, boost, mass) { // Obtain the top speed - return Calc.speed(mass, ship.speed, thrusters, ship.engpip)[4]; - } - - /** - * Update cargo level - * @param {number} cargoLevel Cargo level 0 - 1 - */ - _cargoChange(cargoLevel) { - let ship = this.props.ship; - let cargo = Math.round(ship.cargoCapacity * cargoLevel); - this.setState({ - cargo - }); + return Calc.calcSpeed(mass, ship.speed, ship.standard[1].m, ship.pipSpeed, eng, ship.boost / ship.speed, boost); } /** @@ -81,64 +70,37 @@ export default class EngineProfile extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { ship } = this.props; - const { cargo } = this.state; + const { ship, cargo, eng, fuel, boost } = this.props; // Calculate bounds for our line chart const thrusters = ship.standard[1].m; const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const maxMass = thrusters.getMaxMass(); - let mass = ship.unladenMass + ship.fuelCapacity + cargo; - const minSpeed = Calc.speed(maxMass, ship.speed, thrusters, ship.engpip)[4]; - const maxSpeed = Calc.speed(minMass, ship.speed, thrusters, ship.engpip)[4]; + const mass = ship.unladenMass + fuel + cargo; + const minSpeed = Calc.calcSpeed(maxMass, ship.speed, thrusters, ship.pipSpeed, 0, ship.boost / ship.speed, false); + const maxSpeed = Calc.calcSpeed(minMass, ship.speed, thrusters, ship.pipSpeed, 4, ship.boost / ship.speed, true); // Add a mark at our current mass const mark = Math.min(mass, maxMass); - const cargoPercent = cargo / ship.cargoCapacity; + const code = `${ship.toString()}:${cargo}:${fuel}:${eng}:${boost}`; - const code = ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString(); - - // This graph has a precipitous fall-off so we use lots of points to make it look a little smoother + // This graph can have a precipitous fall-off so we use lots of points to make it look a little smoother return ( - -

{translate('engine profile')}

- - {ship.cargoCapacity ? - -

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

- - - - - - -
- -
-
: '' } -
+ ); } } diff --git a/src/app/components/FSDProfile.jsx b/src/app/components/FSDProfile.jsx index 1fa92289..81548632 100644 --- a/src/app/components/FSDProfile.jsx +++ b/src/app/components/FSDProfile.jsx @@ -13,10 +13,11 @@ import * as Calc from '../shipyard/Calculations'; * FSD profile for a given ship */ export default class FSDProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, - code: React.PropTypes.string.isRequired + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + marker: React.PropTypes.string.isRequired }; /** @@ -30,8 +31,7 @@ export default class FSDProfile extends TranslatedComponent { const ship = this.props.ship; this.state = { - cargo: ship.cargoCapacity, - calcMaxRangeFunc: this._calcMaxRange.bind(this, ship) + calcMaxRangeFunc: this._calcMaxRange.bind(this, ship, this.props.fuel) }; } @@ -42,8 +42,8 @@ export default class FSDProfile extends TranslatedComponent { * @return {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship) }); + if (nextProps.marker != this.props.marker) { + this.setState({ calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship, nextProps.fuel) }); } return true; } @@ -51,38 +51,23 @@ export default class FSDProfile extends TranslatedComponent { /** * Calculate the maximum range for this ship across its applicable mass * @param {Object} ship The ship + * @param {Object} fuel The fuel on the ship * @param {Object} mass The mass at which to calculate the maximum range * @return {number} The maximum range */ - _calcMaxRange(ship, mass) { - // Obtain the FSD for this ship - const fsd = ship.standard[2].m; - + _calcMaxRange(ship, fuel, mass) { // Obtain the maximum range - return Calc.jumpRange(mass, fsd, fsd.getMaxFuelPerJump()); + return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump())); } /** - * Update cargo level - * @param {number} cargoLevel Cargo level 0 - 1 - */ - _cargoChange(cargoLevel) { - let ship = this.props.ship; - let cargo = Math.round(ship.cargoCapacity * cargoLevel); - this.setState({ - cargo - }); - } - - /** - * Render engine profile + * Render FSD profile * @return {React.Component} contents */ render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { ship } = this.props; - const { cargo } = this.state; + const { ship, cargo, fuel } = this.props; // Calculate bounds for our line chart - use thruster info for X @@ -90,56 +75,30 @@ export default class FSDProfile extends TranslatedComponent { const fsd = ship.standard[2].m; const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const maxMass = thrusters.getMaxMass(); - let mass = ship.unladenMass + ship.fuelCapacity + cargo; - const minRange = Calc.jumpRange(maxMass, fsd, ship.fuelCapacity); + const mass = ship.unladenMass + fuel + cargo; + const minRange = 0; const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump()); // Add a mark at our current mass const mark = Math.min(mass, maxMass); - const cargoPercent = cargo / ship.cargoCapacity; - - const code = ship.name + ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString(); + const code = ship.name + ship.toString() + '.' + fuel; return ( - -

{translate('fsd profile')}

- - {ship.cargoCapacity ? - -

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

- - - - - - -
- -
-
: '' } -
+ ); } } diff --git a/src/app/components/Fuel.jsx b/src/app/components/Fuel.jsx new file mode 100644 index 00000000..8c8ea8d7 --- /dev/null +++ b/src/app/components/Fuel.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Fuel slider + * Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change + */ +export default class Fuel extends TranslatedComponent { + static propTypes = { + fuel: React.PropTypes.number.isRequired, + fuelCapacity: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._fuelChange = this._fuelChange.bind(this); + } + + /** + * Update fuel level + * @param {number} fuelLevel percentage level from 0 to 1 + */ + _fuelChange(fuelLevel) { + const { fuel, fuelCapacity } = this.props; + + const newFuel = fuelLevel * fuelCapacity; + // Only send an update if the fuel has changed significantly + if (Math.round(fuel * 10) != Math.round(newFuel * 10)) { + this.props.onChange(Math.round(newFuel * 10) / 10); + } + } + + /** + * Render fuel slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { fuel, fuelCapacity } = this.props; + + return ( + +

{translate('fuel carried')}: {formats.f1(fuel)}{units.T}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 4726f526..bca5ecbc 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -4,6 +4,7 @@ import Persist from '../stores/Persist'; import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** @@ -51,6 +52,12 @@ export default class HardpointSlot extends Slot { if (m.blueprint.special && m.blueprint.special.id >= 0) { modTT += ', ' + translate(m.blueprint.special.name); } + modTT = ( +
+
{modTT}
+ {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)} +
+ ); } return
@@ -74,7 +81,7 @@ export default class HardpointSlot extends Slot { { m.getHps() ?
{translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? ({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }) : null }
: null } { m.getDps() && m.getEps() ?
{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}
: null } { m.getRoF() ?
{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}
: null } - { m.getRange() ?
{translate('range')} {formats.f1(m.getRange() / 1000)}{u.km}
: null } + { m.getRange() ?
{translate('range', m.grp)} {formats.f1(m.getRange() / 1000)}{u.km}
: null } { m.getScanTime() ?
{translate('scantime')} {formats.f1(m.getScanTime())}{u.s}
: null } { m.getFalloff() ?
{translate('falloff')} {formats.round(m.getFalloff() / 1000)}{u.km}
: null } { m.getShieldBoost() ?
+{formats.pct1(m.getShieldBoost())}
: null } diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index ff880f1b..c36660da 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -356,7 +356,7 @@ export default class Header extends TranslatedComponent { let comps = Object.keys(Persist.getComparisons()).sort(); for (let name of comps) { - comparisons.push({name}); + comparisons.push({name}); } } else { comparisons = {translate('none created')}; diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index ec580311..562f7555 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -4,6 +4,7 @@ import Persist from '../stores/Persist'; import { ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Internal Slot @@ -30,6 +31,12 @@ export default class InternalSlot extends Slot { let modTT = translate('modified'); if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + modTT = ( +
+
{modTT}
+ {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)} +
+ ); } let mass = m.getMass() || m.cargo || m.fuel || 0; diff --git a/src/app/components/JumpRange.jsx b/src/app/components/JumpRange.jsx index d197ff04..7fc28c36 100644 --- a/src/app/components/JumpRange.jsx +++ b/src/app/components/JumpRange.jsx @@ -13,9 +13,8 @@ import * as Calc from '../shipyard/Calculations'; * Jump range for a given ship */ export default class JumpRange extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired }; @@ -91,7 +90,6 @@ export default class JumpRange extends TranslatedComponent {

{translate('jump range')}

0.60), + flip = (xPos / width > 0.50), tipWidth = 0, tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; @@ -110,19 +117,21 @@ export default class LineChart extends TranslatedComponent { /** * Update dimensions based on properties and scale - * @param {Object} props React Component properties + * @param {Object} props React Component properties * @param {number} scale size ratio / scale + * @returns {Object} calculated dimensions */ _updateDimensions(props, scale) { - let { width, xMax, xMin, yMin, yMax } = props; - let innerWidth = width - MARGIN.left - MARGIN.right; - let outerHeight = Math.round(width * 0.5 * scale); - let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; + const { xMax, xMin, yMin, yMax } = props; + const { width, height } = this.state.dimensions; + const innerWidth = width - MARGIN.left - MARGIN.right; + const outerHeight = Math.round(width * props.aspect); + const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true); this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility - this.setState({ innerWidth, outerHeight, innerHeight }); + return { innerWidth, outerHeight, innerHeight }; } /** @@ -183,7 +192,7 @@ export default class LineChart extends TranslatedComponent { for (let i = 0, l = series ? series.length : 1; i < l; i++) { const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]); seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor)); - detailElems.push(); + detailElems.push(); markerElems.push(); } @@ -196,7 +205,6 @@ export default class LineChart extends TranslatedComponent { * Update dimensions and series data based on props and context. */ componentWillMount() { - this._updateDimensions(this.props, this.context.sizeRatio); this._updateSeries(this.props, this.state); } @@ -206,14 +214,7 @@ export default class LineChart extends TranslatedComponent { * @param {Object} nextContext Incoming/Next conext */ componentWillReceiveProps(nextProps, nextContext) { - let { func, xMin, xMax, yMin, yMax, width } = nextProps; - let props = this.props; - - let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func; - - if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) { - this._updateDimensions(nextProps, nextContext.sizeRatio); - } + const props = this.props; if (props.code != nextProps.code) { this._updateSeries(nextProps, this.state); @@ -225,53 +226,57 @@ export default class LineChart extends TranslatedComponent { * @return {React.Component} Chart SVG */ render() { - if (!this.props.width) { - return null; - } - - let { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; - let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; - let line = this.line; - let lines = seriesLines.map((line, i) => ).reverse(); + const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio); + const { width, height } = this.state.dimensions; + const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; + const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; + const line = this.line; + const lines = seriesLines.map((line, i) => ).reverse(); const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; - const xmark = xMark ? : ''; + const xmark = xMark ? : ''; - return - - {xmark} - {lines} - d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> - - {xLabel} - ({xUnit}) - - - d3.select(elem).call(this.yAxis)}> - - {yLabel} - { yUnit && ({yUnit}) } - - - this.tipContainer = d3.select(g)} style={{ display: 'none' }}> - - {detailElems} - - this.markersContainer = d3.select(g)} style={{ display: 'none' }}> - {markerElems} - - - - ; + return ( + { this.setState({ dimensions }); }}> +
+ + + {xmark} + {lines} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {xLabel} + ({xUnit}) + + + d3.select(elem).call(this.yAxis)}> + + {yLabel} + { yUnit && ({yUnit}) } + + + this.tipContainer = d3.select(g)} style={{ display: 'none' }}> + + {detailElems} + + this.markersContainer = d3.select(g)} style={{ display: 'none' }}> + {markerElems} + + + + +
+
+ ); } } diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx index ee961d29..ee950579 100644 --- a/src/app/components/ModificationsMenu.jsx +++ b/src/app/components/ModificationsMenu.jsx @@ -5,6 +5,7 @@ import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions'; import cn from 'classnames'; import { Modifications } from 'coriolis-data/dist'; import Modification from './Modification'; +import { getBlueprint, blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Modifications menu @@ -43,16 +44,18 @@ export default class ModificationsMenu extends TranslatedComponent { */ _initState(props, context) { let { m } = props; - const { language } = context; + const { language, tooltip, termtip } = context; const translate = language.translate; // Set up the blueprints let blueprints = []; for (const blueprintName in Modifications.modules[m.grp].blueprints) { for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) { - const close = this._blueprintSelected.bind(this, Modifications.blueprints[blueprintName].id, grade); + const close = this._blueprintSelected.bind(this, blueprintName, grade); const key = blueprintName + ':' + grade; - blueprints.push(
{translate(Modifications.blueprints[blueprintName].name + ' grade ' + grade)}
); + const blueprint = getBlueprint(blueprintName, m); + const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade].features); + blueprints.push(
{translate(blueprint.name + ' grade ' + grade)}
); } } @@ -103,12 +106,12 @@ export default class ModificationsMenu extends TranslatedComponent { /** * Activated when a blueprint is selected - * @param {int} blueprintId The ID of the selected blueprint - * @param {int} grade The grade of the selected blueprint + * @param {int} fdname The Frontier name of the blueprint + * @param {int} grade The grade of the selected blueprint */ - _blueprintSelected(blueprintId, grade) { + _blueprintSelected(fdname, grade) { const { m } = this.props; - const blueprint = Object.assign({}, _.find(Modifications.blueprints, function(o) { return o.id === blueprintId; })); + const blueprint = getBlueprint(fdname, m); blueprint.grade = grade; m.blueprint = blueprint; @@ -153,13 +156,6 @@ export default class ModificationsMenu extends TranslatedComponent { * @param {number} value The value of the roll */ _setRollResult(ship, m, featureName, value) { - if (Modifications.modifications[featureName].method !== 'overwrite') { - if (m.grp == 'sb' && featureName == 'shieldboost') { - // Shield boosters are a special case. Their boost is dependent on their base so we need to calculate the value here - value = ((1 + m.shieldboost) * (1 + value) - 1) / m.shieldboost - 1; - } - } - if (Modifications.modifications[featureName].type == 'percentage') { ship.setModification(m, featureName, value * 10000); } else if (Modifications.modifications[featureName].type == 'numeric') { @@ -276,11 +272,11 @@ export default class ModificationsMenu extends TranslatedComponent { let blueprintLabel; let haveBlueprint = false; + let blueprintTt; if (m.blueprint && !isEmpty(m.blueprint)) { blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; haveBlueprint = true; - } else { - blueprintLabel = translate('PHRASE_SELECT_BLUEPRINT'); + blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features); } let specialLabel; @@ -291,7 +287,7 @@ export default class ModificationsMenu extends TranslatedComponent { } const showBlueprintsMenu = blueprintMenuOpened; - const showSpecial = haveBlueprint && this.state.specials.length > 0; + const showSpecial = haveBlueprint && this.state.specials.length > 0 && !blueprintMenuOpened; const showSpecialsMenu = specialMenuOpened; const showRolls = haveBlueprint && !blueprintMenuOpened && !specialMenuOpened; const showReset = !blueprintMenuOpened && !specialMenuOpened; @@ -303,7 +299,10 @@ export default class ModificationsMenu extends TranslatedComponent { onClick={(e) => e.stopPropagation() } onContextMenu={stopCtxPropagation} > -
{blueprintLabel}
+ { haveBlueprint ? +
{blueprintLabel}
+ : +
{translate('PHRASE_SELECT_BLUEPRINT')}
} { showBlueprintsMenu ? this.state.blueprints : null } { showSpecial ?
{specialLabel}
: null } { showSpecialsMenu ? this.state.specials : null } diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx new file mode 100644 index 00000000..e7432969 --- /dev/null +++ b/src/app/components/Movement.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; + +/** + * Movement + */ +export default class Movement extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + boost: React.PropTypes.bool.isRequired, + eng: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + cargo: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + } + + /** + * Render movement + * @return {React.Component} contents + */ + render() { + const { ship, boost, eng, cargo, fuel } = this.props; + const { language } = this.context; + const { formats, translate, units } = language; + + return ( + + + // Axes + + + + // End Arrow + + // Axes arcs and arrows + + + + + + + + + + + + + + + + // Speed + {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s + // Pitch + {formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s + // Roll + {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s + // Yaw + {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s + + ); + } +} diff --git a/src/app/components/MovementSummary.jsx b/src/app/components/MovementSummary.jsx index adc3974e..f4c268f1 100644 --- a/src/app/components/MovementSummary.jsx +++ b/src/app/components/MovementSummary.jsx @@ -6,7 +6,7 @@ import TranslatedComponent from './TranslatedComponent'; * Movement summary */ export default class MovementSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx new file mode 100644 index 00000000..4f502c0f --- /dev/null +++ b/src/app/components/Offence.jsx @@ -0,0 +1,264 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import * as Calc from '../shipyard/Calculations'; +import PieChart from './PieChart'; +import { nameComparator } from '../utils/SlotFunctions'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import VerticalBarChart from './VerticalBarChart'; + +/** + * Generates an internationalization friendly weapon comparator that will + * sort by specified property (if provided) then by name/group, class, rating + * @param {function} translate Translation function + * @param {function} propComparator Optional property comparator + * @param {boolean} desc Use descending order + * @return {function} Comparator function for names + */ +export function weaponComparator(translate, propComparator, desc) { + return (a, b) => { + if (!desc) { // Flip A and B if ascending order + let t = a; + a = b; + b = t; + } + + // If a property comparator is provided use it first + let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b); + + if (diff) { + return diff; + } + + // Property matches so sort by name / group, then class, rating + if (a.name === b.name && a.grp === b.grp) { + if(a.class == b.class) { + return a.rating > b.rating ? 1 : -1; + } + return a.class - b.class; + } + + return nameComparator(translate, a, b); + }; +} + +/** + * Offence information + * Offence information consists of four panels: + * - textual information (time to drain cap, time to take down shields etc.) + * - breakdown of damage sources (pie chart) + * - comparison of shield resistances (table chart) + * - effective sustained DPS of weapons (bar chart) + */ +export default class Offence extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + engagementrange: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired, + opponentSys: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this._sort = this._sort.bind(this); + + const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange); + this.state = { + predicate: 'n', + desc: true, + damage + }; + } + + /** + * Update the state if our properties change + * @param {Object} nextProps Incoming/Next properties + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps) { + if (this.props.marker != nextProps.marker || this.props.eng != nextProps.eng) { + const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.opponentSys, nextProps.engagementrange); + this.setState({ damage }); + } + return true; + } + + /** + * Set the sort order and sort + * @param {string} predicate Sort predicate + */ + _sortOrder(predicate) { + let desc = this.state.desc; + + if (predicate == this.state.predicate) { + desc = !desc; + } else { + desc = true; + } + + this._sort(predicate, desc); + this.setState({ predicate, desc }); + } + + /** + * Sorts the weapon list + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort order descending + */ + _sort(predicate, desc) { + let comp = weaponComparator.bind(null, this.context.language.translate); + + switch (predicate) { + case 'n': comp = comp(null, desc); break; + case 'esdpss': comp = comp((a, b) => a.sdps.shields.total - b.sdps.shields.total, desc); break; + case 'es': comp = comp((a, b) => a.effectiveness.shields.total - b.effectiveness.shields.total, desc); break; + case 'esdpsh': comp = comp((a, b) => a.sdps.armour.total - b.sdps.armour.total, desc); break; + case 'eh': comp = comp((a, b) => a.effectiveness.armour.total - b.effectiveness.armour.total, desc); break; + } + + this.state.damage.sort(comp); + } + + /** + * Render offence + * @return {React.Component} contents + */ + render() { + const { ship, opponent, wep, engagementrange } = this.props; + const { language, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { damage } = this.state; + const sortOrder = this._sortOrder; + + const pd = ship.standard[4].m; + + const opponentShields = Calc.shieldMetrics(opponent, 4); + const opponentArmour = Calc.armourMetrics(opponent); + + const timeToDrain = Calc.timeToDrainWep(ship, wep); + + let absoluteShieldsSDps = 0; + let explosiveShieldsSDps = 0; + let kineticShieldsSDps = 0; + let thermalShieldsSDps = 0; + let absoluteArmourSDps = 0; + let explosiveArmourSDps = 0; + let kineticArmourSDps = 0; + let thermalArmourSDps = 0; + + let totalSEps = 0; + + const rows = []; + for (let i = 0; i < damage.length; i++) { + const weapon = damage[i]; + + totalSEps += weapon.seps; + absoluteShieldsSDps += weapon.sdps.shields.absolute; + explosiveShieldsSDps += weapon.sdps.shields.explosive; + kineticShieldsSDps += weapon.sdps.shields.kinetic; + thermalShieldsSDps += weapon.sdps.shields.thermal; + absoluteArmourSDps += weapon.sdps.armour.absolute; + explosiveArmourSDps += weapon.sdps.armour.explosive; + kineticArmourSDps += weapon.sdps.armour.kinetic; + thermalArmourSDps += weapon.sdps.armour.thermal; + + const effectivenessShieldsTooltipDetails = []; + effectivenessShieldsTooltipDetails.push(
{translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}
); + effectivenessShieldsTooltipDetails.push(
{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}
); + effectivenessShieldsTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}
); + + const effectiveShieldsSDpsTooltipDetails = []; + if (weapon.sdps.shields.absolute) effectiveShieldsSDpsTooltipDetails.push(
{translate('absolute') + ' ' + formats.f1(weapon.sdps.shields.absolute)}
); + if (weapon.sdps.shields.explosive) effectiveShieldsSDpsTooltipDetails.push(
{translate('explosive') + ' ' + formats.f1(weapon.sdps.shields.explosive)}
); + if (weapon.sdps.shields.kinetic) effectiveShieldsSDpsTooltipDetails.push(
{translate('kinetic') + ' ' + formats.f1(weapon.sdps.shields.kinetic)}
); + if (weapon.sdps.shields.thermal) effectiveShieldsSDpsTooltipDetails.push(
{translate('thermal') + ' ' + formats.f1(weapon.sdps.shields.thermal)}
); + + const effectivenessArmourTooltipDetails = []; + effectivenessArmourTooltipDetails.push(
{translate('range') + ' ' + formats.pct1(weapon.effectiveness.armour.range)}
); + effectivenessArmourTooltipDetails.push(
{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.armour.resistance)}
); + effectivenessArmourTooltipDetails.push(
{translate('hardness') + ' ' + formats.pct1(weapon.effectiveness.armour.hardness)}
); + const effectiveArmourSDpsTooltipDetails = []; + if (weapon.sdps.armour.absolute) effectiveArmourSDpsTooltipDetails.push(
{translate('absolute') + ' ' + formats.f1(weapon.sdps.armour.absolute)}
); + if (weapon.sdps.armour.explosive) effectiveArmourSDpsTooltipDetails.push(
{translate('explosive') + ' ' + formats.f1(weapon.sdps.armour.explosive)}
); + if (weapon.sdps.armour.kinetic) effectiveArmourSDpsTooltipDetails.push(
{translate('kinetic') + ' ' + formats.f1(weapon.sdps.armour.kinetic)}
); + if (weapon.sdps.armour.thermal) effectiveArmourSDpsTooltipDetails.push(
{translate('thermal') + ' ' + formats.f1(weapon.sdps.armour.thermal)}
); + + rows.push( + + + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + {weapon.engineering ? ' (' + weapon.engineering + ')' : null } + + {formats.f1(weapon.sdps.shields.total)} + {formats.pct1(weapon.effectiveness.shields.total)} + {formats.f1(weapon.sdps.armour.total)} + {formats.pct1(weapon.effectiveness.armour.total)} + ); + } + + const totalShieldsSDps = absoluteShieldsSDps + explosiveShieldsSDps + kineticShieldsSDps + thermalShieldsSDps; + const totalArmourSDps = absoluteArmourSDps + explosiveArmourSDps + kineticArmourSDps + thermalArmourSDps; + + const shieldsSDpsData = []; + shieldsSDpsData.push({ value: Math.round(absoluteShieldsSDps), label: translate('absolute') }); + shieldsSDpsData.push({ value: Math.round(explosiveShieldsSDps), label: translate('explosive') }); + shieldsSDpsData.push({ value: Math.round(kineticShieldsSDps), label: translate('kinetic') }); + shieldsSDpsData.push({ value: Math.round(thermalShieldsSDps), label: translate('thermal') }); + + const armourSDpsData = []; + armourSDpsData.push({ value: Math.round(absoluteArmourSDps), label: translate('absolute') }); + armourSDpsData.push({ value: Math.round(explosiveArmourSDps), label: translate('explosive') }); + armourSDpsData.push({ value: Math.round(kineticArmourSDps), label: translate('kinetic') }); + armourSDpsData.push({ value: Math.round(thermalArmourSDps), label: translate('thermal') }); + + const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); + const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); + + return ( + +
+ + + + + + + + + + + + + + + + {rows} + +
{translate('weapon')}{translate('opponent\`s shields')}{translate('opponent\`s armour')}
{'sdps'}{'eft'}{'sdps'}{'eft'}
+
+
+

{translate('offence metrics')}

+

{translate('PHRASE_TIME_TO_DRAIN_WEP')}
{timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}

+

{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}
{timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}

+

{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}
{timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}

+
+
+

{translate('shield damage sources')}

+ +
+
+

{translate('armour damage sources')}

+ +
+
); + } +} diff --git a/src/app/components/OffenceSummary.jsx b/src/app/components/OffenceSummary.jsx index 1aac8b8c..d7778a70 100644 --- a/src/app/components/OffenceSummary.jsx +++ b/src/app/components/OffenceSummary.jsx @@ -7,7 +7,7 @@ import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive } from '. * Offence summary */ export default class OffenceSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx new file mode 100644 index 00000000..2d25037c --- /dev/null +++ b/src/app/components/OutfittingSubpages.jsx @@ -0,0 +1,186 @@ +import React from 'react'; +import cn from 'classnames'; +import { Ships } from 'coriolis-data/dist'; +import Ship from '../shipyard/Ship'; +import Persist from '../stores/Persist'; +import TranslatedComponent from './TranslatedComponent'; +import PowerManagement from './PowerManagement'; +import CostSection from './CostSection'; +import EngineProfile from './EngineProfile'; +import FSDProfile from './FSDProfile'; +import Movement from './Movement'; +import Offence from './Offence'; +import Defence from './Defence'; +import WeaponDamageChart from './WeaponDamageChart'; + +/** + * Outfitting subpages + */ +export default class OutfittingSubpages extends TranslatedComponent { + + static propTypes = { + ship: React.PropTypes.object.isRequired, + code: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired, + buildName: React.PropTypes.string, + sys: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired, + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + boost: React.PropTypes.bool.isRequired, + engagementRange: React.PropTypes.number.isRequired, + opponent: React.PropTypes.object.isRequired, + opponentBuild: React.PropTypes.string, + opponentSys: React.PropTypes.number.isRequired, + opponentEng: React.PropTypes.number.isRequired, + opponentWep: React.PropTypes.number.isRequired, + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this._powerTab = this._powerTab.bind(this); + this._profilesTab = this._profilesTab.bind(this); + this._offenceTab = this._offenceTab.bind(this); + this._defenceTab = this._defenceTab.bind(this); + + this.state = { + tab: Persist.getOutfittingTab() || 'power', + }; + } + + /** + * Show selected tab + * @param {string} tab Tab name + */ + _showTab(tab) { + this.setState({ tab }); + } + + /** + * Render the power tab + * @return {React.Component} Tab contents + */ + _powerTab() { + let { ship, buildName, code, onChange } = this.props; + Persist.setOutfittingTab('power'); + + const powerMarker = `${ship.toString()}`; + const costMarker = `${ship.toString().split('.')[0]}`; + + return
+ + +
; + } + + /** + * Render the profiles tab + * @return {React.Component} Tab contents + */ + _profilesTab() { + const { ship, opponent, cargo, fuel, eng, boost, engagementRange, opponentSys } = this.props; + const { translate } = this.context.language; + let realBoost = boost && ship.canBoost(); + Persist.setOutfittingTab('profiles'); + + const engineProfileMarker = `${ship.toString()}:${cargo}:${fuel}:${eng}:${realBoost}`; + const fsdProfileMarker = `${ship.toString()}:${cargo}:${fuel}`; + const movementMarker = `${ship.topSpeed}:${ship.pitch}:${ship.roll}:${ship.yaw}:${ship.canBoost()}`; + const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}:${opponentSys}`; + + return
+
+

{translate('engine profile')}

+ +
+ +
+

{translate('fsd profile')}

+ +
+ +
+

{translate('movement profile')}

+ +
+ +
+

{translate('damage to opponent\'s shields')}

+ +
+ +
+

{translate('damage to opponent\'s hull')}

+ +
+
; + } + + /** + * Render the offence tab + * @return {React.Component} Tab contents + */ + _offenceTab() { + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentSys } = this.props; + Persist.setOutfittingTab('offence'); + + const marker = `${ship.toString()}${opponent.toString()}${opponentBuild}${engagementRange}${opponentSys}`; + + return
+ +
; + } + + /** + * Render the defence tab + * @return {React.Component} Tab contents + */ + _defenceTab() { + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentWep } = this.props; + Persist.setOutfittingTab('defence'); + + const marker = `${ship.toString()}${opponent.toString()}{opponentBuild}${engagementRange}${opponentWep}`; + + return
+ +
; + } + + /** + * Render the section + * @return {React.Component} Contents + */ + render() { + const tab = this.state.tab; + const translate = this.context.language.translate; + let tabSection; + + switch (tab) { + case 'power': tabSection = this._powerTab(); break; + case 'profiles': tabSection = this._profilesTab(); break; + case 'offence': tabSection = this._offenceTab(); break; + case 'defence': tabSection = this._defenceTab(); break; + } + + return ( +
+ + + + + + + + + +
{translate('power and costs')}{translate('profiles')}{translate('offence')}{translate('defence')}
+ {tabSection} +
+ ); + } +} diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx new file mode 100644 index 00000000..c5e5435e --- /dev/null +++ b/src/app/components/PieChart.jsx @@ -0,0 +1,96 @@ +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import * as d3 from 'd3'; + +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; + +/** + * A pie chart + */ +export default class PieChart extends Component { + + static propTypes = { + data : React.PropTypes.array.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.pie = d3.pie().value((d) => d.value); + this.colors = CORIOLIS_COLOURS; + this.arc = d3.arc(); + this.arc.innerRadius(0); + + this.state = { + dimensions: { + width: 100, + height: 100 + } + }; + } + + + /** + * Generate a slice of the pie chart + * @param {Object} d the data for this slice + * @param {number} i the index of this slice + * @returns {Object} the SVG for the slice + */ + sliceGenerator(d, i) { + if (!d || d.value == 0) { + // Ignore 0 values + return null; + } + + const { width, height } = this.state.dimensions; + const { data } = this.props; + + // Push the labels further out from the centre of the slice + let [labelX, labelY] = this.arc.centroid(d); + const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`; + + // Put the keys in a line with equal spacing + const nonZeroItems = data.filter(d => d.value != 0).length; + const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1; + const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5); + const keyTranslate = `translate(${keyX}, ${width * 0.45})`; + + return ( + + + {d.value} + {d.data.label} + + ); + } + + /** + * Render the component + * @returns {object} Markup + */ + render() { + const { width, height } = this.state.dimensions; + const pie = this.pie(this.props.data), + translate = `translate(${width / 2}, ${width * 0.4})`; + + this.arc.outerRadius(width * 0.4); + + return ( + { this.setState({ dimensions }); }}> +
+ + + {pie.map((d, i) => this.sliceGenerator(d, i))} + + +
+
+ ); + } +} diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx new file mode 100644 index 00000000..29051318 --- /dev/null +++ b/src/app/components/Pips.jsx @@ -0,0 +1,299 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import ShipSelector from './ShipSelector'; +import { nameComparator } from '../utils/SlotFunctions'; +import { Pip } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; +import Module from '../shipyard/Module'; + +/** + * Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area. + * Requires an onChange() function of the form onChange(sys, eng, wep) which is triggered whenever the pips change. + */ +export default class Pips extends TranslatedComponent { + static propTypes = { + sys: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + const { sys, eng, wep } = props; + + this._keyDown = this._keyDown.bind(this); + } + + /** + * Add listeners after mounting + */ + componentDidMount() { + document.addEventListener('keydown', this._keyDown); + } + + /** + * Remove listeners before unmounting + */ + componentWillUnmount() { + document.removeEventListener('keydown', this._keyDown); + } + + /** + * Handle Key Down + * @param {Event} e Keyboard Event + */ + _keyDown(e) { + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + switch (e.keyCode) { + case 37: // Left arrow == increase SYS + e.preventDefault(); + this._incSys(); + break; + case 38: // Up arrow == increase ENG + e.preventDefault(); + this._incEng(); + break; + case 39: // Right arrow == increase WEP + e.preventDefault(); + this._incWep(); + break; + case 40: // Down arrow == reset + e.preventDefault(); + this._reset(); + break; + } + } + } + + /** + * Handle a click + * @param {string} which Which item was clicked + */ + onClick(which) { + if (which == 'SYS') { + this._incSys(); + } else if (which == 'ENG') { + this._incEng(); + } else if (which == 'WEP') { + this._incWep(); + } else if (which == 'RST') { + this._reset(); + } + } + + /** + * Reset the capacitor + */ + _reset() { + let { sys, eng, wep } = this.props; + if (sys != 2 || eng != 2 || wep != 2) { + sys = eng = wep = 2; + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the SYS capacitor + */ + _incSys() { + let { sys, eng, wep } = this.props; + + const required = Math.min(1, 4 - sys); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (eng > wep) { + eng -= 0.5; + sys += 0.5; + } else { + wep -= 0.5; + sys += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (eng == 0) { + wep -= 1; + sys += 1; + } else if (wep == 0) { + eng -= 1; + sys += 1; + } else { + eng -= 0.5; + wep -= 0.5; + sys += 1; + } + } + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the ENG capacitor + */ + _incEng() { + let { sys, eng, wep } = this.props; + + const required = Math.min(1, 4 - eng); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > wep) { + sys -= 0.5; + eng += 0.5; + } else { + wep -= 0.5; + eng += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + wep -= 1; + eng += 1; + } else if (wep == 0) { + sys -= 1; + eng += 1; + } else { + sys -= 0.5; + wep -= 0.5; + eng += 1; + } + } + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the WEP capacitor + */ + _incWep() { + let { sys, eng, wep } = this.props; + + const required = Math.min(1, 4 - wep); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > eng) { + sys -= 0.5; + wep += 0.5; + } else { + eng -= 0.5; + wep += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + eng -= 1; + wep += 1; + } else if (eng == 0) { + sys -= 1; + wep += 1; + } else { + sys -= 0.5; + eng -= 0.5; + wep += 1; + } + } + this.props.onChange(sys, eng, wep); + } + } + + /** + * Set up the rendering for pips + * @param {int} sys the SYS pips + * @param {int} eng the ENG pips + * @param {int} wep the WEP pips + * @returns {Object} Object containing the rendering for the pips + */ + _renderPips(sys, eng, wep) { + const pipsSvg = {}; + + // SYS + pipsSvg['SYS'] = []; + for (let i = 0; i < Math.floor(sys); i++) { + pipsSvg['SYS'].push(); + } + if (sys > Math.floor(sys)) { + pipsSvg['SYS'].push(); + } + for (let i = Math.floor(sys + 0.5); i < 4; i++) { + pipsSvg['SYS'].push(); + } + + // ENG + pipsSvg['ENG'] = []; + for (let i = 0; i < Math.floor(eng); i++) { + pipsSvg['ENG'].push(); + } + if (eng > Math.floor(eng)) { + pipsSvg['ENG'].push(); + } + for (let i = Math.floor(eng + 0.5); i < 4; i++) { + pipsSvg['ENG'].push(); + } + + // WEP + pipsSvg['WEP'] = []; + for (let i = 0; i < Math.floor(wep); i++) { + pipsSvg['WEP'].push(); + } + if (wep > Math.floor(wep)) { + pipsSvg['WEP'].push(); + } + for (let i = Math.floor(wep + 0.5); i < 4; i++) { + pipsSvg['WEP'].push(); + } + + return pipsSvg; + } + + /** + * Render pips + * @return {React.Component} contents + */ + render() { + const { formats, translate, units } = this.context.language; + const { sys, eng, wep } = this.props; + + const onSysClicked = this.onClick.bind(this, 'SYS'); + const onEngClicked = this.onClick.bind(this, 'ENG'); + const onWepClicked = this.onClick.bind(this, 'WEP'); + const onRstClicked = this.onClick.bind(this, 'RST'); + + const pipsSvg = this._renderPips(sys, eng, wep); + return ( + + + + + + + + + + + + + + + + + + + + + + +
  {pipsSvg['ENG']} 
 {pipsSvg['SYS']}{translate('ENG')}{pipsSvg['WEP']}
 {translate('SYS')}{translate('RST')}{translate('WEP')}
+
+ ); + } +} diff --git a/src/app/components/PowerManagement.jsx b/src/app/components/PowerManagement.jsx index e8a81fd0..39e78a19 100644 --- a/src/app/components/PowerManagement.jsx +++ b/src/app/components/PowerManagement.jsx @@ -17,7 +17,7 @@ const POWER = [ * Power Management Section */ export default class PowerManagement extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, onChange: React.PropTypes.func.isRequired diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx new file mode 100644 index 00000000..93505b8d --- /dev/null +++ b/src/app/components/ShipPicker.jsx @@ -0,0 +1,126 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import Ship from '../shipyard/Ship'; +import { Ships } from 'coriolis-data/dist'; +import { Rocket } from './SvgIcons'; +import Persist from '../stores/Persist'; +import cn from 'classnames'; + +/** + * Ship picker + * Requires an onChange() function of the form onChange(ship), providing the ship, which is triggered on ship change + */ +export default class ShipPicker extends TranslatedComponent { + static propTypes = { + onChange: React.PropTypes.func.isRequired, + ship: React.PropTypes.string.isRequired, + build: React.PropTypes.string + }; + + static defaultProps = { + ship: 'eagle' + } + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.shipOrder = Object.keys(Ships).sort(); + this._toggleMenu = this._toggleMenu.bind(this); + this._closeMenu = this._closeMenu.bind(this); + + this.state = { menuOpen: false }; + } + + /** + * Update ship + * @param {object} ship the ship + * @param {string} build the build, if present + */ + _shipChange(ship, build) { + this._closeMenu(); + + // Ensure that the ship has changed + if (ship !== this.props.ship || build !== this.props.build) { + this.props.onChange(ship, build); + } + } + + /** + * Render the menu for the picker + * @returns {object} the picker menu + */ + _renderPickerMenu() { + const { ship, build } = this.props; + const _shipChange = this._shipChange; + const builds = Persist.getBuilds(); + const buildList = []; + for (let shipId of this.shipOrder) { + const shipBuilds = []; + // Add stock build + const stockSelected = (ship == shipId && !build); + shipBuilds.push(
  • Stock
  • ); + if (builds[shipId]) { + let buildNameOrder = Object.keys(builds[shipId]).sort(); + for (let buildName of buildNameOrder) { + const buildSelected = ship === shipId && build === buildName; + shipBuilds.push(
  • {buildName}
  • ); + } + } + buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); + } + + return buildList; + } + + /** + * Toggle the menu state + */ + _toggleMenu() { + const { menuOpen } = this.state; + this.setState({ menuOpen: !menuOpen }); + } + + /** + * Close the menu + */ + _closeMenu() { + const { menuOpen } = this.state; + if (menuOpen) { + this._toggleMenu(); + } + } + + /** + * Render picker + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { ship, build } = this.props; + const { menuOpen } = this.state; + + const shipString = ship + ': ' + (build ? build : translate('stock')); + return ( +
    e.stopPropagation() }> +
    +
    + + {shipString} +
    + { menuOpen ? +
    e.stopPropagation() }> +
    + {this._renderPickerMenu()} +
    +
    : null } +
    +
    + ); + } +} diff --git a/src/app/components/ShipSelector.jsx b/src/app/components/ShipSelector.jsx index 34f11786..a48ba4a8 100644 --- a/src/app/components/ShipSelector.jsx +++ b/src/app/components/ShipSelector.jsx @@ -8,7 +8,7 @@ import { Rocket } from './SvgIcons'; * Selector for ships */ export default class ShipSelector extends TranslatedComponent { - static PropTypes = { + static propTypes = { initial: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index 651f9a9f..9212bb1b 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -2,6 +2,7 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import cn from 'classnames'; import { Warning } from './SvgIcons'; +import * as Calc from '../shipyard/Calculations'; /** * Ship Summary Table / Stats @@ -9,7 +10,8 @@ import { Warning } from './SvgIcons'; export default class ShipSummaryTable extends TranslatedComponent { static propTypes = { - ship: React.PropTypes.object.isRequired + ship: React.PropTypes.object.isRequired, + marker: React.PropTypes.string.isRequired, }; /** @@ -17,77 +19,75 @@ export default class ShipSummaryTable extends TranslatedComponent { * @return {React.Component} Summary table */ render() { - let ship = this.props.ship; + const { ship } = this.props; let { language, tooltip, termtip } = this.context; let translate = language.translate; let u = language.units; let formats = language.formats; let { time, int, round, f1, f2 } = formats; - let sgClassNames = cn({ warning: ship.findInternalByGroup('sg') && !ship.shield, muted: !ship.findInternalByGroup('sg') }); - let sgRecover = '-'; - let sgRecharge = '-'; let hide = tooltip.bind(null, null); - if (ship.shield) { - sgRecover = time(ship.calcShieldRecovery()); - sgRecharge = time(ship.calcShieldRecharge()); - } + const shieldGenerator = ship.findInternalByGroup('sg'); + const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator }); + const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL'; + const timeToDrain = Calc.timeToDrainWep(ship, 4); + const canThrust = ship.canThrust(); + const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL'; + const canBoost = ship.canBoost(); + const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL'; return
    - - - - - - - - - - + + + + + + + + + {/* */} - - + + - + - - - - + - + + + - - - - - - + + + + + + + + + + + + + {/* */} + + + + + - - - - - - - - - - - - - diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 2dade7e1..30d5502f 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -8,6 +8,7 @@ import ModificationsMenu from './ModificationsMenu'; import { ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Standard Slot @@ -53,6 +54,12 @@ export default class StandardSlot extends TranslatedComponent { let modTT = translate('modified'); if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + modTT = ( +
    +
    {modTT}
    + {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)} +
    + ); } if (!selected) { @@ -93,7 +100,7 @@ export default class StandardSlot extends TranslatedComponent { { m.getMinMass() ?
    {translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}
    : null } { m.getOptMass() ?
    {translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}
    : null } { m.getMaxMass() ?
    {translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
    : null } - { m.getRange() ?
    {translate('range')}: {formats.f2(m.getRange())}{units.km}
    : null } + { m.getRange() ?
    {translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}
    : null } { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } { m.getThermalEfficiency() ?
    {translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}
    : null } { m.getPowerGeneration() > 0 ?
    {translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}
    : null } diff --git a/src/app/components/SvgIcons.jsx b/src/app/components/SvgIcons.jsx index af119679..4593f173 100644 --- a/src/app/components/SvgIcons.jsx +++ b/src/app/components/SvgIcons.jsx @@ -708,6 +708,24 @@ export class Switch extends SvgIcon { } } +/** + * Pip + */ +export class Pip extends SvgIcon { + /** + * Overriden view box + * @return {String} view box + */ + viewBox() { return '0 0 200 200'; } + /** + * Generate the SVG + * @return {React.Component} SVG Contents + */ + svg() { + return ; + } +} + /** * In-game Coriolis Station logo */ diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx new file mode 100644 index 00000000..b8a709cf --- /dev/null +++ b/src/app/components/VerticalBarChart.jsx @@ -0,0 +1,105 @@ +import TranslatedComponent from './TranslatedComponent'; +import React, { PropTypes } from 'react'; +import Measure from 'react-measure'; +import { BarChart, Bar, XAxis, YAxis } from 'recharts'; + +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; +const AXIS_COLOUR = '#C06400'; + +const ASPECT = 1; + +const merge = function(one, two) { + return Object.assign({}, one, two); +}; + +/** + * A vertical bar chart + */ +export default class VerticalBarChart extends TranslatedComponent { + + static propTypes = { + data : PropTypes.array.isRequired, + yMax : PropTypes.number + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._termtip = this._termtip.bind(this); + + this.state = { + dimensions: { + width: 300, + height: 300 + } + }; + } + + /** + * Render the bar chart + * @returns {Object} the markup + */ + render() { + const { width, height } = this.state.dimensions; + const { tooltip, termtip } = this.context; + + // Calculate maximum for Y + let dataMax = Math.max(...this.props.data.map(d => d.value)); + if (dataMax == -Infinity) dataMax = 0; + let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0; + const localMax = Math.max(dataMax, yMax); + + return ( + this.setState({ dimensions }) }> +
    + + + + } fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/> + +
    +
    + ); + } + + /** + * Generate a term tip + * @param {Object} d the data + * @param {number} i the index + * @param {Object} e the event + * @returns {Object} termtip markup + */ + _termtip(d, i, e) { + if (this.props.data[i].tooltip) { + return this.context.termtip(this.props.data[i].tooltip, e); + } else { + return null; + } + } +} + +/** + * A label that displays the value within the bar of the chart + */ +const ValueLabel = React.createClass({ + propTypes: { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + value: PropTypes.number + }, + + render() { + const { x, y, payload, value } = this.props; + + return ( + {value} + ); + } +}); diff --git a/src/app/components/WeaponDamageChart.jsx b/src/app/components/WeaponDamageChart.jsx new file mode 100644 index 00000000..9c04fbef --- /dev/null +++ b/src/app/components/WeaponDamageChart.jsx @@ -0,0 +1,204 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import ShipSelector from './ShipSelector'; +import { nameComparator } from '../utils/SlotFunctions'; +import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as Calc from '../shipyard/Calculations'; +import Module from '../shipyard/Module'; + +const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777']; + +/** + * Weapon damage chart + */ +export default class WeaponDamageChart extends TranslatedComponent { + static propTypes = { + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + hull: React.PropTypes.bool.isRequired, + engagementRange: React.PropTypes.number.isRequired, + opponentSys: React.PropTypes.number.isRequired, + marker: React.PropTypes.string.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + } + + /** + * Set the initial weapons state + */ + componentWillMount() { + const weaponNames = this._weaponNames(this.props.ship, this.context); + const opponentShields = Calc.shieldMetrics(this.props.opponent, this.props.opponentSys); + const opponentArmour = Calc.armourMetrics(this.props.opponent); + const maxRange = this._calcMaxRange(this.props.ship); + const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour); + + this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, this.props.hull) }); + } + + /** + * Set the updated weapons state if our ship changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps, nextContext) { + if (nextProps.marker != this.props.marker) { + const weaponNames = this._weaponNames(nextProps.ship, nextContext); + const opponentShields = Calc.shieldMetrics(nextProps.opponent, nextProps.opponentSys); + const opponentArmour = Calc.armourMetrics(nextProps.opponent); + const maxRange = this._calcMaxRange(nextProps.ship); + const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour); + this.setState({ weaponNames, + opponentShields, + opponentArmour, + maxRange, + maxDps, + calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, nextProps.hull) + }); + } + return true; + } + + /** + * Calculate the maximum range of a ship's weapons + * @param {Object} ship The ship + * @returns {int} The maximum range, in metres + */ + _calcMaxRange(ship) { + let maxRange = 1000; // Minimum + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const thisRange = ship.hardpoints[i].m.getRange(); + if (thisRange > maxRange) { + maxRange = thisRange; + } + } + } + + return maxRange; + } + + /** + * Calculate the maximum sustained single-weapon DPS for this ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shields + * @param {Object} opponentArmour The opponent's armour + * @return {number} The maximum sustained single-weapon DPS + */ + _calcMaxSDps(ship, opponent, opponentShields, opponentArmour) { + // Additional information to allow effectiveness calculations + let maxSDps = 0; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + + const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, 0); + const thisSDps = sustainedDps.damage.armour.total > sustainedDps.damage.shields.total ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total; + if (thisSDps > maxSDps) { + maxSDps = thisSDps; + } + } + } + return maxSDps; + } + + /** + * Obtain the weapon names for this ship + * @param {Object} ship The ship + * @param {Object} context The context + * @return {array} The weapon names + */ + _weaponNames(ship, context) { + const translate = context.language.translate; + let names = []; + let num = 1; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp); + let engineering; + if (m.blueprint && m.blueprint.name) { + engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + if (m.blueprint.special && m.blueprint.special.id) { + engineering += ', ' + translate(m.blueprint.special.name); + } + } + if (engineering) { + name = name + ' (' + engineering + ')'; + } + names.push(name); + } + } + return names; + } + + /** + * Calculate the per-weapon sustained DPS for this ship against another ship at a given range + * @param {Object} ship The ship + * @param {Object} weaponNames The names of the weapons for which to calculate DPS + * @param {Object} opponent The target + * @param {Object} opponentShields The opponent's shields + * @param {Object} opponentArmour The opponent's armour + * @param {bool} hull true if to calculate against hull, false if to calculate against shields + * @param {Object} engagementRange The engagement range + * @return {array} The array of weapon DPS + */ + _calcSDps(ship, weaponNames, opponent, opponentShields, opponentArmour, hull, engagementRange) { + let results = {}; + let weaponNum = 0; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange); + results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total; + } + } + return results; + } + + /** + * Render damage dealt + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { maxRange } = this.state; + const { ship, opponent, engagementRange } = this.props; + + const sortOrder = this._sortOrder; + const onCollapseExpand = this._onCollapseExpand; + + const code = `${ship.toString()}:${opponent.toString()}`; + + return ( + + + + ); + } +} diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx index f22c0d42..e8d81da0 100644 --- a/src/app/i18n/Language.jsx +++ b/src/app/i18n/Language.jsx @@ -58,21 +58,21 @@ export function getLanguage(langCode) { }, translate, units: { - CR: {translate('CR')}, // Credits - kg: {translate('kg')}, // Kilograms - kgs: {translate('kg/s')}, // Kilograms per second - km: {translate('km')}, // Kilometers - Ls: {translate('Ls')}, // Light Seconds - LY: {translate('LY')}, // Light Years - MJ: {translate('MJ')}, // Mega Joules - 'm/s': {translate('m/s')}, // Meters per second - '°/s': {translate('°/s')}, // Degrees per second - MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) + CR: {translate('CR')}, // Credits + kg: {translate('kg')}, // Kilograms + kgs: {translate('kg/s')}, // Kilograms per second + km: {translate('km')}, // Kilometers + Ls: {translate('Ls')}, // Light Seconds + LY: {translate('LY')}, // Light Years + MJ: {translate('MJ')}, // Mega Joules + 'm/s': {translate('m/s')}, // Meters per second + '°/s': {translate('°/s')}, // Degrees per second + MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) mps: {translate('m/s')}, // Metres per second ps: {translate('/s')}, // per second pm: {translate('/min')}, // per minute s: {translate('secs')}, // Seconds - T: {translate('T')}, // Metric Tons + T: {translate('T')}, // Metric Tons } }; } diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 4b338ae4..5c68a1f8 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -24,8 +24,8 @@ export const terms = { PHRASE_NO_BUILDS: 'No builds added to comparison!', PHRASE_NO_RETROCH: 'No Retrofitting changes', PHRASE_SELECT_BUILDS: 'Select builds to compare', - PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge', - PHRASE_SG_RECOVER: 'Recovery (to 50%) after collapse', + PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge, assuming full SYS capacitor to start with', + PHRASE_SG_RECOVER: 'Time from 0% to 50% charge, assuming full SYS capacitor to start with', PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo', PHRASE_UPDATE_RDY: 'Update Available! Click to refresh', PHRASE_ENGAGEMENT_RANGE: 'The distance between your ship and its target', @@ -39,6 +39,55 @@ export const terms = { PHRASE_NO_SPECIAL: 'No experimental effect', PHRASE_SHOPPING_LIST: 'Stations that sell this build', PHRASE_TOTAL_EFFECTIVE_SHIELD: 'Total amount of damage that can be taken from each damage type, if using all shield cells', + PHRASE_TIME_TO_LOSE_SHIELDS: 'Shields will hold for', + PHRASE_TIME_TO_RECOVER_SHIELDS: 'Shields will recover in', + PHRASE_TIME_TO_RECHARGE_SHIELDS: 'Shields will recharge in', + PHRASE_SHIELD_SOURCES: 'Breakdown of the supply of shield energy', + PHRASE_EFFECTIVE_SHIELD: 'Effective shield strength against different damage types', + PHRASE_ARMOUR_SOURCES: 'Breakdown of the supply of armour', + PHRASE_EFFECTIVE_ARMOUR: 'Effective armour strength against different damage types', + PHRASE_DAMAGE_TAKEN: '% of raw damage taken for different damage types', + PHRASE_TIME_TO_LOSE_ARMOUR: 'Armour will hold for', + PHRASE_MODULE_PROTECTION_EXTERNAL: 'Protection for hardpoints', + PHRASE_MODULE_PROTECTION_INTERNAL: 'Protection for all other modules', + PHRASE_SHIELD_DAMAGE: 'Breakdown of sources for sustained DPS against shields', + PHRASE_ARMOUR_DAMAGE: 'Breakdown of sources for sustained DPS against armour', + + PHRASE_TIME_TO_REMOVE_SHIELDS: 'Will remove shields in', + TT_TIME_TO_REMOVE_SHIELDS: 'With sustained fire by all weapons', + PHRASE_TIME_TO_REMOVE_ARMOUR: 'Will remove armour in', + TT_TIME_TO_REMOVE_ARMOUR: 'With sustained fire by all weapons', + PHRASE_TIME_TO_DRAIN_WEP: 'Will drain WEP in', + TT_TIME_TO_DRAIN_WEP: 'Time to drain WEP capacitor with all weapons firing', + TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons', + TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons', + TT_MODULE_ARMOUR: 'Armour protecting against module damage', + TT_MODULE_PROTECTION_EXTERNAL: 'Percentage of damage diverted from hardpoints to module reinforcement packages', + TT_MODULE_PROTECTION_INTERNAL: 'Percentage of damage diverted from non-hardpoint modules to module reinforcement packages', + + TT_EFFECTIVE_SDPS_SHIELDS: 'Actual sustained DPS whilst WEP capacitor is not empty', + TT_EFFECTIVENESS_SHIELDS: 'Effectivness compared to hitting a 0-resistance target with 0 pips to SYS at 0m', + TT_EFFECTIVE_SDPS_ARMOUR: 'Actual sustained DPS whilst WEP capacitor is not empty', + TT_EFFECTIVENESS_ARMOUR: 'Effectivness compared to hitting a 0-resistance target at 0m', + + TT_SUMMARY_SPEED: 'With full fuel tank and 4 pips to ENG', + TT_SUMMARY_SPEED_NONFUNCTIONAL: 'Thrusters powered off or over maximum mass', + TT_SUMMARY_BOOST: 'With full fuel tank and 4 pips to ENG', + TT_SUMMARY_BOOST_NONFUNCTIONAL: 'Power distributor not able to supply enough power to boost', + TT_SUMMARY_SHIELDS: 'Raw shield strength, including boosters', + TT_SUMMARY_SHIELDS_NONFUNCTIONAL: 'No shield generator or shield generator powered off', + TT_SUMMARY_INTEGRITY: 'Ship integrity, including bulkheads and hull reinforcement packages', + TT_SUMMARY_HULL_MASS: 'Mass of the hull prior to any modules being installed', + TT_SUMMARY_UNLADEN_MASS: 'Mass of the hull and modules prior to any fuel or cargo', + TT_SUMMARY_LADEN_MASS: 'Mass of the hull and modules with full fuel and cargo', + TT_SUMMARY_DPS: 'Damage per second with all weapons firing', + TT_SUMMARY_EPS: 'WEP capacitor consumed per second with all weapons firing', + TT_SUMMARY_TTD: 'Time to drain WEP capacitor with all weapons firing and 4 pips to WEP', + TT_SUMMARY_MAX_SINGLE_JUMP: 'Farthest possible jump range with no cargo and only enough fuel for the jump itself', + TT_SUMMARY_UNLADEN_SINGLE_JUMP: 'Farthest possible jump range with no cargo and a full fuel tank', + TT_SUMMARY_LADEN_SINGLE_JUMP: 'Farthest possible jump range with full cargo and a full fuel tank', + TT_SUMMARY_UNLADEN_TOTAL_JUMP: 'Farthest possible range with no cargo, a full fuel tank, and jumping as far as possible each time', + TT_SUMMARY_LADEN_TOTAL_JUMP: 'Farthest possible range with full cargo, a full fuel tank, and jumping as far as possible each time', HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', @@ -181,8 +230,7 @@ export const terms = { regen: 'Regeneration rate', reload: 'Reload', rof: 'Rate of fire', - scanangle: 'Scan angle', - scanrange: 'Scan range', + angle: 'Scan angle', scantime: 'Scan time', shield: 'Shield', shieldboost: 'Shield boost', @@ -204,6 +252,23 @@ export const terms = { optmul_sg: 'Optimal strength', maxmul_sg: 'Minimum strength', + range_s: 'Typical emission range', + + // Damage types + absolute: 'Absolute', + explosive: 'Explosive', + kinetic: 'Kinetic', + thermal: 'Thermal', + + // Shield sources + generator: 'Generator', + boosters: 'Boosters', + cells: 'Cells', + + // Armour sources + bulkheads: 'Bulkheads', + reinforcement: 'Reinforcement', + // Help text HELP_TEXT: `

    Introduction

    @@ -234,7 +299,7 @@ Along the top of the screen are some of the key values for your build. This is Here, along with most places in Coriolis, acronyms will have tooltips explaining what they mean. Hover over the acronym to obtain more detail, or look in the glossary at the end of this help.

    -All values are the highest possible, assuming that you have maximum pips in the relevant capacitor (ENG for speed, WEP for time to drain, etc.).

    +All values are the highest possible, assuming that you an optimal setup for that particular value (maximum pips in ENG for speed, minimum fuel for jump range, etc.). Details of the specific setup for each value are listed in the associated tootip.

    Modules

    The next set of panels laid out horizontally across the screen contain the modules you have put in your build. From left to right these are the core modules, the internal modules, the hardpoints and the utility mounts. These represent the available slots in your ship and cannot be altered. Each slot has a class, or size, and in general any module up to a given size can fit in a given slot (exceptions being bulkheads, life support and sensors in core modules and restricted internal slots, which can only take a subset of module depending on their restrictions).

    @@ -247,30 +312,39 @@ To move a module from one slot to another drag it. If you instead want to copy Clicking on the headings for each set of modules gives you the ability to either select an overall role for your ship (when clicking the core internal header) or a specific module with which you want to fill all applicable slots (when clicking the other headers).

    -

    Offence Summary

    -The offence summary panel provides information about the damage that you deal with your weapons.

    +

    Ship Controls

    +The ship controls allow you to set your pips, boost, and amount of fuel and cargo that your build carries. The changes made here will effect the information supplied in the subsequent panels, giving you a clearer view of what effect different changing these items will have.

    -The first headline gives an overall damage per second rating: this is the optimum amount of damage the build will do per second according to weapon statistics. After that is a breakdown of the damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +Ship control settings are saved as part of a build.

    -The next headline gives an overall sustained damage per second rating: this is the optimum amount of damage the build will do per second over a longer period of time, taking in to account ammunition clip capacities and reload times. After that is a breakdown of the sustained damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +

    Opponent

    +The opponet selection allows you to choose your opponent. The opponent can be either a stock build of a ship or one of your own saved builds. You can also set the engagement range between you and your opponent. Your selection here will effect the information supplied in the subsequent panels, specifically the Offence and Defence panels.

    -The final headline gives an overall damage per energy rating: this is the amount of damage the build will do per unit of weapon capacitor energy expended. After that is a breakdown of the damage per energy the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +Opponent settings are saved as part of a build.

    -

    Defence Summary

    -The defence summary panel provides information about the strength of your defences and the damage that you receive from opponents.

    +

    Power and Costs Sub-panels

    +

    Power

    +The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module. Disabled modules will not be included in the build's statistics, with the exception of Shield Cell Banks as they are usually disabled when not in use and only enabled when required.

    -The first headline gives your total shield strength (if you have shields), taking in to account your base shield plus boosters. After that are the details of how long it will take for your shields to recover from 0 to 50% (recovery time) and from 50% to 100% (recharge time). The next line provides a breakdown of the shield damage taken from different damage types. For example, if you damage from kinetic is 60% then it means that a weapon usually dealing 10 points of damage will only deal 6, the rest being resisted by the shield. Note that this does not include any resistance alterations due to pips in your SYS capacitor.

    +

    Costs

    +The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the 'Settings' at the top-right of the page.

    -The second headline gives your total shield cell strength (if you have shield cell banks). This is the sum of the recharge of all of equipped shield cell banks.

    +The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.

    -The third headline gives your total armour strength, taking in to account your base armour plus hull reinforcement packages. The next line provides a breakdown of the hull damage taken from different damage types. For example, if you damage from kinetic is 120% then it means that a weapon usually dealing 10 points of damage will deal 12.

    +The reload costs provides information about the costs of reloading your current build.

    -The fourth headline gives your total module protection strength from module reinforcement packages. The next line provides a breakdown of the protection for both internal and external modules whilst all module reinforcement packages are functioning. For example, if external module protection is 20% then 10 points of damage will 2 points of damage to the module reinforcement packages and 8 points of damage to the module

    +

    Profiles

    +Profiles provide graphs that show the general performance of modules in your build -

    Movement Summary

    -The movement summary panel provides information about the build's speed and agility.

    +

    Engine Profile

    +The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed alters with the overall mass of your build. The vertical dashed line on the graph shows your current mass. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your maximum speed by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your speed by hitting the boost button.

    + +

    FSD Profile

    +The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range alters with the overall mass of your build. The vertical dashed line on the graph shows your current maximum single jump range. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD, and you can increase your maximum jump range by reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build,

    + +

    Movement Profile

    +The movement profile panel provides information about the capabilities of your current thrusters with your current overall mass and ENG pips settings. The diagram shows your ability to move and rotate in the different axes: -Along the top of this panel are the number of pips you put in to your ENG capacitor, from 0 to 4 and also include 4 pips and boost (4b). Along the side of this panel are the names of the metrics. These are:
    Speed
    The fastest the ship can move, in metres per second
    Pitch
    The fastest the ship can raise or lower its nose, in degrees per second
    @@ -278,51 +352,84 @@ Along the top of this panel are the number of pips you put in to your ENG capaci
    Yaw
    The fastest the ship can turn its nose left or right, in degrees per second
    -

    Power Management

    -The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module.

    +Your movement profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your movement values by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your movement profile by hitting the boost button.

    -

    Costs

    -The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the 'Settings' at the top-right of the page.

    +

    Damage Profile

    +The damage profile provides two graphs showing how the the build's damage to the opponent's shields and hull change with engagement range. The vertical dashed line on the graph shows your current engagement range. This combines information about the build's weapons with the opponent's shields and hull to provide an accurate picture of sustained damage that can be inflicted on the opponent.

    -The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.

    +

    Offence

    +

    Summary

    +The offence summary provides per-weapon information about sustained damage per second inflicted to shields and hull, along with a measure of effectiveness of that weapon. The effectiveness value has a tooltip that provides a breakdown of the effectiveness, and can include reductions or increases due to range, resistance, and either power distributor (for shields) or hardness (for hull). The final effectiveness value is calculated by multiplying these percentages together.

    -The reload costs provides information about the costs of reloading your current build.

    +

    Offence Metrics

    +The offence metrics panel provides information about your offence.

    -

    Engine Profile

    -The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed (with a full fuel tank and 4 pips to engines) alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters.

    +Time to drain is a measure of how quickly your WEP capacitor will drain when firing all weapons. It is affected by the number of pips you have in your WEP capacitor, with more pips resulting in a higher WEP recharge rate and hence a longer time to drain.

    -

    FSD Profile

    -The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range (with a full fuel tank) alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD.

    +The next value is the time it will take you to remove your opponent's shields. This assumes that you have 100% time on target and that your engagement range stays constant. Note that if your time to remove shields is longer than your time to drain this assumes that you continue firing throughout, inflicting lower damage due to the reduced energy in your WEP capacitor.

    -

    Jump Range

    -The jump range panel provides information about the build' jump range. The graph shows how the build's jump range changes with the amount of cargo on-board. The slider can be altered to change the amount of fuel you have on-board.

    +The next value is the time it will take you to remove your opponent's armour. This follows the same logic as the time to remove shields.

    -

    Damage Dealt

    -The damage dealt panel provides information about the effectiveness of your build's weapons against opponents' shields and hull at different engagement distances.

    +

    Shield Damage Sources

    +The shield damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided.

    -The ship against which you want to check damage dealt can be selected by clicking on the red ship icon or the red ship name at the top of this panel.

    +

    Hull Damage Sources

    +The hull damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided.

    -The main section of this panel is a table showing your weapons and their effectiveness. Effectiveness against shields takes in to account the weapon and its engagement range, and assumes standard shield resistances. Effectiveness against hull takes in to account the weapon and, its engagement range and the target's hardness, and assumes military grade armour resistances.

    +

    Defence

    +

    Shield Metrics

    +The shield metrics provides information about your shield defence.

    -Effective DPS and effective SDPS are the equivalent of DPS and SDPS for the weapon. Effectiveness is a percentage value that shows how effective the DPS of the weapon is compared in reality against the given target compared to the weapon's stated DPS. Effectiveness can never go above 100%.

    +Raw shield strength is the sum of the shield from your generator, boosters and shield cell banks. A tooltip provides a breakdown of these values.

    -Total effective DPS, SDPS and effectiveness against both shields and hull are provided at the bottom of the table.

    +The time the shields will hold for is the time it will take your opponent' to remove your shields. This assumes that they have 100% time on target and that the engagement range stays constant. It also assumes that you fire all of your shield cell banks prior to your shields being lost.

    -At the bottom of this panel you can change your engagement range. The engagement range is the distance between your ship and your target. Many weapons suffer from what is known as damage falloff, where their effectiveness decreases the further the distance between your ship and your target. This allows you to model the effect of engaging at different ranges. +The time the shields will recover in is the time it will take your shields to go from collapsed (0%) to recovered (50%). This is affected by the number of pips you have in your SYS capacitor.

    -Note that this panel only shows enabled weapons, so if you want to see your overall effectiveness for a subset of your weapons you can disable the undesired weapons in the power management panel.

    +The time the shields will recharge in is the time it will take your shields to go from recovered (50%) to full (100%). This is affected by the number of pips you have in your SYS capacitor.

    -At the bottom of this panel are two graphs showing how your sustained DPS changes with engagement range. This shows at a glance how effective each weapon is at different distances.

    +Shield Sources +This chart provides information about the sources of your shields. For each applicable source of shields (generator, boosters, shield cell banks) a value is provided.

    -

    Damage Received

    -The damage received panel provides information about the effectiveness of your build's defences against opponent's weapons at different engagement range. Features and functions are the same as the damage dealt panel, except that it does take in to account your build's resistances.

    +Damage Taken +This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the shields. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values.

    + +Effective Shield +This graph shows the effective shield for each damage type, found by dividing the raw shield value by the damage taken for that type.

    + +

    Amour Metrics

    +The armour metrics provides information about your armour defence.

    + +Raw armour strength is the sum of the armour from your bulkheads and hull reinforcement packages. A tooltip provides a breakdown of these values.

    + +The time the armour will hold for is the time it will take your opponent' to take your armour to 0. This assumes that they have 100% time on target, the engagement range stays constant, and that all damage is dealt to the armour rather than modules.

    + +Raw module armour is the sum of the protection from your module reinforcement packages.

    + +Protection for hardpoints is the amount of protection that your module reinforcement packages provide to hardpoints. This percentage of damage to the hardpoints will be diverted to the module reinforcement packages.

    + +Protection for all other modules is the amount of protection that your module reinforcement packages provide to everything other than hardpoints. This percentage of damage to the modules will be diverted to the module reinforcement packages.

    + +Armour Sources +This chart provides information about the sources of your armour. For each applicable source of shields (bulkheads, hull reinforcement packages) a value is provided.

    + +Damage Taken +This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the armour. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values.

    + +Effective Armour +This graph shows the effective armour for each damage type, found by dividing the raw armour value by the damage taken for that type.

    Keyboard Shortcuts

    +
    Ctrl-b
    toggle boost
    Ctrl-e
    open export dialogue (outfitting page only)
    Ctrl-h
    open help dialogue
    Ctrl-i
    open import dialogue
    Ctrl-o
    open shortlink dialogue
    +
    Ctrl-left-arrow
    increase SYS capacitor
    +
    Ctrl-up-arrow
    increase ENG capacitor
    +
    Ctrl-right-arrow
    increase WEP capacitor
    +
    Ctrl-down-arrow
    reset power distributor
    Esc
    close any open dialogue

    Glossary

    diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 0e50fc63..81372ec8 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -1,32 +1,31 @@ import React from 'react'; +// import Perf from 'react-addons-perf'; import { findDOMNode } from 'react-dom'; import { Ships } from 'coriolis-data/dist'; import cn from 'classnames'; import Page from './Page'; import Router from '../Router'; import Persist from '../stores/Persist'; +import * as Utils from '../utils/UtilityFunctions'; import Ship from '../shipyard/Ship'; import { toDetailedBuild } from '../shipyard/Serializer'; import { outfitURL } from '../utils/UrlGenerators'; import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons'; +import LZString from 'lz-string'; import ShipSummaryTable from '../components/ShipSummaryTable'; import StandardSlotSection from '../components/StandardSlotSection'; import HardpointsSlotSection from '../components/HardpointsSlotSection'; import InternalSlotSection from '../components/InternalSlotSection'; import UtilitySlotSection from '../components/UtilitySlotSection'; -import OffenceSummary from '../components/OffenceSummary'; -import DefenceSummary from '../components/DefenceSummary'; -import MovementSummary from '../components/MovementSummary'; -import EngineProfile from '../components/EngineProfile'; -import FSDProfile from '../components/FSDProfile'; -import JumpRange from '../components/JumpRange'; -import DamageDealt from '../components/DamageDealt'; -import DamageReceived from '../components/DamageReceived'; -import PowerManagement from '../components/PowerManagement'; -import CostSection from '../components/CostSection'; +import Pips from '../components/Pips'; +import Boost from '../components/Boost'; +import Fuel from '../components/Fuel'; +import Cargo from '../components/Cargo'; +import ShipPicker from '../components/ShipPicker'; +import EngagementRange from '../components/EngagementRange'; +import OutfittingSubpages from '../components/OutfittingSubpages'; import ModalExport from '../components/ModalExport'; import ModalPermalink from '../components/ModalPermalink'; -import Slider from '../components/Slider'; /** * Document Title Generator @@ -50,17 +49,25 @@ export default class OutfittingPage extends Page { */ constructor(props, context) { super(props, context); - this.state = this._initState(context); + // window.Perf = Perf; + this.state = this._initState(props, context); this._keyDown = this._keyDown.bind(this); this._exportBuild = this._exportBuild.bind(this); + this._pipsUpdated = this._pipsUpdated.bind(this); + this._boostUpdated = this._boostUpdated.bind(this); + this._cargoUpdated = this._cargoUpdated.bind(this); + this._fuelUpdated = this._fuelUpdated.bind(this); + this._opponentUpdated = this._opponentUpdated.bind(this); + this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this); } /** * [Re]Create initial state from context + * @param {Object} props React component properties * @param {context} context React component context * @return {Object} New state object */ - _initState(context) { + _initState(props, context) { let params = context.route.params; let shipId = params.ship; let code = params.code; @@ -82,6 +89,8 @@ export default class OutfittingPage extends Page { this._getTitle = getTitle.bind(this, data.properties.name); + // Obtain ship control from code + const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = this._obtainControlFromCode(ship, code); return { error: null, title: this._getTitle(buildName), @@ -91,7 +100,19 @@ export default class OutfittingPage extends Page { shipId, ship, code, - savedCode + savedCode, + sys, + eng, + wep, + boost, + fuel, + cargo, + opponent, + opponentBuild, + opponentSys, + opponentEng, + opponentWep, + engagementRange }; } @@ -113,35 +134,199 @@ export default class OutfittingPage extends Page { this.setState(stateChanges); } + /** + * Update the control part of the route + */ + _updateRouteOnControlChange() { + const { ship, shipId, buildName } = this.state; + const code = this._fullCode(ship); + this._updateRoute(shipId, buildName, code); + this.setState({ code }); + } + + /** + * Provide a full code for this ship, including any additions due to the outfitting page + * @param {Object} ship the ship + * @param {number} fuel the fuel carried by the ship (if different from that in state) + * @param {number} cargo the cargo carried by the ship (if different from that in state) + * @returns {string} the code for this ship + */ + _fullCode(ship, fuel, cargo) { + return `${ship.toString()}.${LZString.compressToBase64(this._controlCode(fuel, cargo))}`; + } + + /** + * Obtain the control information from the build code + * @param {Object} ship The ship + * @param {string} code The build code + * @returns {Object} The control information + */ + _obtainControlFromCode(ship, code) { + // Defaults + let sys = 2; + let eng = 2; + let wep = 2; + let boost = false; + let fuel = ship.fuelCapacity; + let cargo = ship.cargoCapacity; + let opponent = new Ship('eagle', Ships['eagle'].properties, Ships['eagle'].slots).buildWith(Ships['eagle'].defaults); + let opponentSys = 2; + let opponentEng = 2; + let opponentWep = 2; + let opponentBuild; + let engagementRange = 1000; + + // Obtain updates from code, if available + if (code) { + const parts = code.split('.'); + if (parts.length >= 5) { + // We have control information in the code + const control = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[4])).split('/'); + sys = parseFloat(control[0]); + eng = parseFloat(control[1]); + wep = parseFloat(control[2]); + boost = control[3] == 1 ? true : false; + fuel = parseFloat(control[4]); + cargo = parseInt(control[5]); + if (control[6]) { + const shipId = control[6]; + opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots); + if (control[7] && Persist.getBuild(shipId, control[7])) { + // Ship is a particular build + const opponentCode = Persist.getBuild(shipId, control[7]); + opponent.buildFrom(opponentCode); + opponentBuild = control[7]; + if (opponentBuild) { + // Obtain opponent's sys/eng/wep pips from their code + const opponentParts = opponentCode.split('.'); + if (opponentParts.length >= 5) { + const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/'); + opponentSys = parseFloat(opponentControl[0]); + opponentEng = parseFloat(opponentControl[1]); + opponentWep = parseFloat(opponentControl[2]); + } + } + } else { + // Ship is a stock build + opponent.buildWith(Ships[shipId].defaults); + } + } + engagementRange = parseInt(control[8]); + } + } + + return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange }; + } + + /** + * Triggered when pips have been updated + * @param {number} sys SYS pips + * @param {number} eng ENG pips + * @param {number} wep WEP pips + */ + _pipsUpdated(sys, eng, wep) { + this.setState({ sys, eng, wep }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when boost has been updated + * @param {boolean} boost true if boosting + */ + _boostUpdated(boost) { + this.setState({ boost }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when fuel has been updated + * @param {number} fuel the amount of fuel, in T + */ + _fuelUpdated(fuel) { + this.setState({ fuel }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when cargo has been updated + * @param {number} cargo the amount of cargo, in T + */ + _cargoUpdated(cargo) { + this.setState({ cargo }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when engagement range has been updated + * @param {number} engagementRange the engagement range, in m + */ + _engagementRangeUpdated(engagementRange) { + this.setState({ engagementRange }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when target ship has been updated + * @param {string} opponent the opponent's ship model + * @param {string} opponentBuild the name of the opponent's build + */ + _opponentUpdated(opponent, opponentBuild) { + const opponentShip = new Ship(opponent, Ships[opponent].properties, Ships[opponent].slots); + if (opponentBuild && Persist.getBuild(opponent, opponentBuild)) { + // Ship is a particular build + opponentShip.buildFrom(Persist.getBuild(opponent, opponentBuild)); + } else { + // Ship is a stock build + opponentShip.buildWith(Ships[opponent].defaults); + } + + this.setState({ opponent: opponentShip, opponentBuild }, () => this._updateRouteOnControlChange()); + } + + /** + * Set the control code for this outfitting page + * @param {number} fuel the fuel carried by the ship (if different from that in state) + * @param {number} cargo the cargo carried by the ship (if different from that in state) + * @returns {string} The control code + */ + _controlCode(fuel, cargo) { + const { sys, eng, wep, boost, opponent, opponentBuild, engagementRange } = this.state; + const code = `${sys}/${eng}/${wep}/${boost ? 1 : 0}/${fuel || this.state.fuel}/${cargo || this.state.cargo}/${opponent.id}/${opponentBuild ? opponentBuild : ''}/${engagementRange}`; + return code; + } + /** * Save the current build */ _saveBuild() { - let code = this.state.ship.toString(); - let { buildName, newBuildName, shipId } = this.state; + const { code, buildName, newBuildName, shipId } = this.state; - if (buildName === newBuildName) { - Persist.saveBuild(shipId, buildName, code); - this._updateRoute(shipId, buildName, code); + Persist.saveBuild(shipId, newBuildName, code); + this._updateRoute(shipId, newBuildName, code); + + let opponent, opponentBuild, opponentSys, opponentEng, opponentWep; + if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) { + // This is a save of our current opponent build; update it + opponentBuild = newBuildName; + opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code); + opponentSys = this.state.sys; + opponentEng = this.state.eng; + opponentWep = this.state.wep; } else { - Persist.saveBuild(shipId, newBuildName, code); - this._updateRoute(shipId, newBuildName, code); + opponentBuild = this.state.opponentBuild; + opponent = this.state.opponent; + opponentSys = this.state.opponentSys; + opponentEng = this.state.opponentEng; + opponentWep = this.state.opponentWep; } - - this.setState({ buildName: newBuildName, code, savedCode: code, title: this._getTitle(newBuildName) }); + this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, title: this._getTitle(newBuildName) }); } /** * Rename the current build */ _renameBuild() { - let { buildName, newBuildName, shipId, ship } = this.state; + const { code, buildName, newBuildName, shipId, ship } = this.state; if (buildName != newBuildName && newBuildName.length) { - let code = ship.toString(); Persist.deleteBuild(shipId, buildName); Persist.saveBuild(shipId, newBuildName, code); this._updateRoute(shipId, newBuildName, code); - this.setState({ buildName: newBuildName, code, savedCode: code }); + this.setState({ buildName: newBuildName, code, savedCode: code, opponentBuild: newBuildName }); } } @@ -149,24 +334,50 @@ export default class OutfittingPage extends Page { * Reload build from last save */ _reloadBuild() { - this.state.ship.buildFrom(this.state.savedCode); - this._shipUpdated(); + this.setState({ code: this.state.savedCode }, () => this._codeUpdated()); } /** * Reset build to Stock/Factory defaults */ _resetBuild() { - this.state.ship.buildWith(Ships[this.state.shipId].defaults); - this._shipUpdated(); + const { ship, shipId, buildName } = this.state; + // Rebuild ship + ship.buildWith(Ships[shipId].defaults); + // Reset controls + const code = ship.toString(); + const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code); + // Update state, and refresh the ship + this.setState({ + sys, + eng, + wep, + boost, + fuel, + cargo, + opponent, + opponentBuild, + engagementRange + }, () => this._updateRoute(shipId, buildName, code)); } /** * Delete the build */ _deleteBuild() { - Persist.deleteBuild(this.state.shipId, this.state.buildName); + const { shipId, buildName } = this.state; + Persist.deleteBuild(shipId, buildName); + + let opponentBuild; + if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) { + // Our current opponent has been deleted; revert to stock + opponentBuild = null; + } else { + opponentBuild = this.state.opponentBuild; + } Router.go(outfitURL(this.state.shipId)); + + this.setState({ opponentBuild }); } /** @@ -183,14 +394,43 @@ export default class OutfittingPage extends Page { } /** - * Trigger render on ship model change + * Called when the code for the ship has been updated, to synchronise the rest of the data + */ + _codeUpdated() { + const { code, ship, shipId, buildName } = this.state; + + // Rebuild ship from the code + this.state.ship.buildFrom(code); + + // Obtain controls from the code + const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code); + // Update state, and refresh the route when complete + this.setState({ + sys, + eng, + wep, + boost, + fuel, + cargo, + opponent, + opponentBuild, + engagementRange + }, () => this._updateRoute(shipId, buildName, code)); + } + + /** + * Called when the ship has been updated, to set the code and then update accordingly */ _shipUpdated() { - let { shipId, buildName, ship } = this.state; - let code = ship.toString(); - - this._updateRoute(shipId, buildName, code); - this.setState({ code }); + let { ship, shipId, buildName, cargo, fuel } = this.state; + if (cargo > ship.cargoCapacity) { + cargo = ship.cargoCapacity; + } + if (fuel > ship.fuelCapacity) { + fuel = ship.fuelCapacity; + } + const code = this._fullCode(ship, fuel, cargo); + this.setState({ code, cargo, fuel }, () => this._updateRoute(shipId, buildName, code)); } /** @@ -203,20 +443,6 @@ export default class OutfittingPage extends Page { Router.replace(outfitURL(shipId, code, buildName)); } - /** - * Update dimenions from rendered DOM - */ - _updateDimensions() { - let elem = findDOMNode(this.refs.chartThird); - - if (elem) { - this.setState({ - thirdChartWidth: findDOMNode(this.refs.chartThird).offsetWidth, - halfChartWidth: findDOMNode(this.refs.chartThird).offsetWidth * 3 / 2 - }); - } - } - /** * Update state based on context changes * @param {Object} nextProps Incoming/Next properties @@ -224,7 +450,7 @@ export default class OutfittingPage extends Page { */ componentWillReceiveProps(nextProps, nextContext) { if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed - this.setState(this._initState(nextContext)); + this.setState(this._initState(nextProps, nextContext)); } } @@ -232,22 +458,14 @@ export default class OutfittingPage extends Page { * Add listeners when about to mount */ componentWillMount() { - this.resizeListener = this.context.onWindowResize(this._updateDimensions); document.addEventListener('keydown', this._keyDown); } - /** - * Trigger DOM updates on mount - */ - componentDidMount() { - this._updateDimensions(); - } - /** * Remove listeners on unmount */ componentWillUnmount() { - this.resizeListener.remove(); + document.removeEventListener('keydown', this._keyDown); } /** @@ -295,19 +513,30 @@ export default class OutfittingPage extends Page { let state = this.state, { language, termtip, tooltip, sizeRatio, onWindowResize } = this.context, { translate, units, formats } = language, - { ship, code, savedCode, buildName, newBuildName, halfChartWidth, thirdChartWidth } = state, + { ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = state, hide = tooltip.bind(null, null), menu = this.props.currentMenu, shipUpdated = this._shipUpdated, canSave = (newBuildName || buildName) && code !== savedCode, canRename = buildName && newBuildName && buildName != newBuildName, - canReload = savedCode && canSave, - hStr = ship.getHardpointsString() + '.' + ship.getModificationsString(), - iStr = ship.getInternalString() + '.' + ship.getModificationsString(); + canReload = savedCode && canSave; // Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up code = ship.name + (code || ''); + // Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time + const _sStr = ship.getStandardString(); + const _iStr = ship.getInternalString(); + const _hStr = ship.getHardpointsString(); + const _pStr = `${ship.getPowerEnabledString()}${ship.getPowerPrioritiesString()}`; + const _mStr = ship.getModificationsString(); + + const standardSlotMarker = `${ship.name}${_sStr}${_pStr}${_mStr}`; + const internalSlotMarker = `${ship.name}${_iStr}${_pStr}${_mStr}`; + const hardpointsSlotMarker = `${ship.name}${_hStr}${_pStr}${_mStr}`; + const boostMarker = `${ship.canBoost()}`; + const shipSummaryMarker = `${ship.name}${_sStr}${_iStr}${_hStr}${_pStr}${_mStr}`; + return (
    @@ -341,46 +570,63 @@ export default class OutfittingPage extends Page {
    - - - - - + {/* Main tables */} + + + + + -
    - + {/* Control of ship and opponent */} +
    +
    +

    {translate('ship control')}

    +
    +
    + +
    -
    - +
    +
    -
    - +
    + +
    +
    + { ship.cargoCapacity > 0 ? : null } +
    +
    +
    +

    {translate('opponent')}

    +
    +
    + +
    +
    +
    +
    - - - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    + {/* Tabbed subpages */} +
    - ); } } diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 47b74b7f..3b806710 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -9,33 +9,33 @@ import Module from './Module'; * @return {number} Distance in Light Years */ export function jumpRange(mass, fsd, fuel) { - let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; - let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; + const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; } /** - * Calculate the fastest (total) range based on mass and a specific FSD, and all fuel available + * Calculate the total jump range based on mass and a specific FSD, and all fuel available * * @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass * @param {number} fuel The total fuel available * @return {number} Distance in Light Years */ -export function fastestRange(mass, fsd, fuel) { - let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; - let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; - let fuelRemaining = fuel % fsdMaxFuelPerJump; // Fuel left after making N max jumps - let jumps = Math.floor(fuel / fsdMaxFuelPerJump); - mass += fuelRemaining; - // Going backwards, start with the last jump using the remaining fuel - let fastestRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass : 0; - // For each max fuel jump, calculate the max jump range based on fuel mass left in the tank - for (let j = 0; j < jumps; j++) { - mass += fsd.maxfuel; - fastestRange += Math.pow(fsdMaxFuelPerJump / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; +export function totalJumpRange(mass, fsd, fuel) { + const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; + + let fuelRemaining = fuel; + let totalRange = 0; + while (fuelRemaining > 0) { + const fuelForThisJump = Math.min(fuelRemaining, fsdMaxFuelPerJump); + totalRange += this.jumpRange(mass, fsd, fuelForThisJump); + // Mass is reduced + mass -= fuelForThisJump; + fuelRemaining -= fuelForThisJump; } - return fastestRange; + return totalRange; }; /** @@ -173,3 +173,681 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas res * (1 - (engpip * 1)), res]; } + +/** + * Calculate a single value + * @param {number} minMass the minimum mass of the thrusters + * @param {number} optMass the optimum mass of the thrusters + * @param {number} maxMass the maximum mass of the thrusters + * @param {number} minMul the minimum multiplier of the thrusters + * @param {number} optMul the optimum multiplier of the thrusters + * @param {number} maxMul the maximum multiplier of the thrusters + * @param {number} mass the mass of the ship + * @param {base} base the base value from which to calculate + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @returns {number} the resultant value + */ +function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) { + const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); + const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); + const ynorm = Math.pow(xnorm, exponent); + const mul = minMul + ynorm * (maxMul - minMul); + const res = base * mul; + + return res * (1 - (engpip * (4 - eng))); +} + +/** + * Calculate speed for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseSpeed the base speed of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant speed + */ +export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + const optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + const maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + const minMul = thrusters instanceof Module ? thrusters.getMinMul('speed') : (thrusters.minmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const optMul = thrusters instanceof Module ? thrusters.getOptMul('speed') : (thrusters.optmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const maxMul = thrusters instanceof Module ? thrusters.getMaxMul('speed') : (thrusters.maxmulspeed ? thrusters.minmulspeed : thrusters.minmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseSpeed, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate pitch for a given setup + * @param {number} mass the mass of the ship + * @param {number} basePitch the base pitch of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant pitch + */ +export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, basePitch, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate roll for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseRoll the base roll of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant roll + */ +export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseRoll, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate yaw for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseYaw the base yaw of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant yaw + */ +export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseYaw, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate shield metrics + * @param {Object} ship The ship + * @param {int} sys The pips to SYS + * @returns {Object} Shield metrics + */ +export function shieldMetrics(ship, sys) { + const sysResistance = this.sysResistance(sys); + const maxSysResistance = this.sysResistance(4); + + let shield = {}; + + const shieldGeneratorSlot = ship.findInternalByGroup('sg'); + if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) { + const shieldGenerator = shieldGeneratorSlot.m; + + // Boosters + let boost = 1; + let boosterExplDmg = 1; + let boosterKinDmg = 1; + let boosterThermDmg = 1; + for (let slot of ship.hardpoints) { + if (slot.enabled && slot.m && slot.m.grp == 'sb') { + boost += slot.m.getShieldBoost(); + boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance()); + boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance()); + boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance()); + } + } + + // Calculate diminishing returns for boosters + boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5); + // Remove base shield generator strength + boost -= 1; + // Apply diminishing returns + boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2; + boosterKinDmg = boosterKinDmg > 0.7 ? boosterKinDmg : 0.7 - (0.7 - boosterKinDmg) / 2; + boosterThermDmg = boosterThermDmg > 0.7 ? boosterThermDmg : 0.7 - (0.7 - boosterThermDmg) / 2; + + const generatorStrength = this.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1); + const boostersStrength = generatorStrength * boost; + + // Recover time is the time taken to go from 0 to 50%. It includes a 16-second wait before shields start to recover + const shieldToRecover = (generatorStrength + boostersStrength) / 2; + const powerDistributor = ship.standard[4].m; + const sysRechargeRate = this.sysRechargeRate(powerDistributor, sys); + + // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes + // 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration + let capacitorDrain = (shieldGenerator.getBrokenRegenerationRate() * 0.6) - sysRechargeRate; + let capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + + let recover = 16; + if (capacitorDrain <= 0 || shieldToRecover < capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()) { + // We can recover the entire shield from the capacitor store + recover += shieldToRecover / shieldGenerator.getBrokenRegenerationRate(); + } else { + // We can recover some of the shield from the capacitor store + recover += capacitorLifetime; + const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate(); + if (sys === 0) { + // No system pips so will never recover shields + recover = Math.Inf; + } else { + // Recover remaining shields at the rate of the power distributor's recharge + recover += remainingShieldToRecover / (sysRechargeRate / 0.6); + } + } + + // Recharge time is the time taken to go from 50% to 100% + const shieldToRecharge = (generatorStrength + boostersStrength) / 2; + + // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes + // 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration + capacitorDrain = (shieldGenerator.getRegenerationRate() * 0.6) - sysRechargeRate; + capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + + let recharge = 0; + if (capacitorDrain <= 0 || shieldToRecharge < capacitorLifetime * shieldGenerator.getRegenerationRate()) { + // We can recharge the entire shield from the capacitor store + recharge += shieldToRecharge / shieldGenerator.getRegenerationRate(); + } else { + // We can recharge some of the shield from the capacitor store + recharge += capacitorLifetime; + const remainingShieldToRecharge = shieldToRecharge - capacitorLifetime * shieldGenerator.getRegenerationRate(); + if (sys === 0) { + // No system pips so will never recharge shields + recharge = Math.Inf; + } else { + // Recharge remaining shields at the rate of the power distributor's recharge + recharge += remainingShieldToRecharge / (sysRechargeRate / 0.6); + } + } + + shield = { + generator: generatorStrength, + boosters: boostersStrength, + cells: ship.shieldCells, + total: generatorStrength + boostersStrength + ship.shieldCells, + recover, + recharge, + }; + + // Shield resistances have three components: the shield generator, the shield boosters and the SYS pips. + // We re-cast these as damage percentages + shield.absolute = { + generator: 1, + boosters: 1, + sys: 1 - sysResistance, + total: 1 - sysResistance, + max: 1 - maxSysResistance + }; + + shield.explosive = { + generator: 1 - shieldGenerator.getExplosiveResistance(), + boosters: boosterExplDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - maxSysResistance) + }; + + shield.kinetic = { + generator: 1 - shieldGenerator.getKineticResistance(), + boosters: boosterKinDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - maxSysResistance) + }; + + shield.thermal = { + generator: 1 - shieldGenerator.getThermalResistance(), + boosters: boosterThermDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance) + }; + } + + return shield; +} + +/** + * Calculate armour metrics + * @param {Object} ship The ship + * @returns {Object} Armour metrics + */ +export function armourMetrics(ship) { + // Armour from bulkheads + const armourBulkheads = ship.baseArmour + (ship.baseArmour * ship.bulkheads.m.getHullBoost()); + let armourReinforcement = 0; + + let moduleArmour = 0; + let moduleProtection = 1; + + let hullExplDmg = 1; + let hullKinDmg = 1; + let hullThermDmg = 1; + + // Armour from HRPs and module armour from MRPs + for (let slot of ship.internal) { + if (slot.m && slot.m.grp == 'hr') { + armourReinforcement += slot.m.getHullReinforcement(); + // Hull boost for HRPs is applied against the ship's base armour + armourReinforcement += ship.baseArmour * slot.m.getModValue('hullboost') / 10000; + + hullExplDmg = hullExplDmg * (1 - slot.m.getExplosiveResistance()); + hullKinDmg = hullKinDmg * (1 - slot.m.getKineticResistance()); + hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance()); + } + if (slot.m && slot.m.grp == 'mrp') { + moduleArmour += slot.m.getIntegrity(); + moduleProtection = moduleProtection * (1 - slot.m.getProtection()); + } + } + moduleProtection = 1 - moduleProtection; + + // Apply diminishing returns + hullExplDmg = hullExplDmg > 0.7 ? hullExplDmg : 0.7 - (0.7 - hullExplDmg) / 2; + hullKinDmg = hullKinDmg > 0.7 ? hullKinDmg : 0.7 - (0.7 - hullKinDmg) / 2; + hullThermDmg = hullThermDmg > 0.7 ? hullThermDmg : 0.7 - (0.7 - hullThermDmg) / 2; + + const armour = { + bulkheads: armourBulkheads, + reinforcement: armourReinforcement, + modulearmour: moduleArmour, + moduleprotection: moduleProtection, + total: armourBulkheads + armourReinforcement + }; + + // Armour resistances have two components: bulkheads and HRPs + // We re-cast these as damage percentages + armour.absolute = { + bulkheads: 1, + reinforcement: 1, + total: 1 + }; + + armour.explosive = { + bulkheads: 1 - ship.bulkheads.m.getExplosiveResistance(), + reinforcement: hullExplDmg, + total: (1 - ship.bulkheads.m.getExplosiveResistance()) * hullExplDmg + }; + + armour.kinetic = { + bulkheads: 1 - ship.bulkheads.m.getKineticResistance(), + reinforcement: hullKinDmg, + total: (1 - ship.bulkheads.m.getKineticResistance()) * hullKinDmg + }; + + armour.thermal = { + bulkheads: 1 - ship.bulkheads.m.getThermalResistance(), + reinforcement: hullThermDmg, + total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg + }; + + return armour; +} + +/** + * Calculate defence metrics for a ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The pips to SYS + * @param {int} opponentWep The pips to pponent's WEP + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Defence metrics + */ +export function defenceMetrics(ship, opponent, sys, opponentWep, engagementrange) { + // Obtain the shield metrics + const shield = this.shieldMetrics(ship, sys); + + // Obtain the armour metrics + const armour = this.armourMetrics(ship); + + // Obtain the opponent's sustained DPS on us + const sustainedDps = this.sustainedDps(opponent, ship, sys, engagementrange); + + const shielddamage = shield.generator ? { + absolutesdps: sustainedDps.shieldsdps.absolute, + explosivesdps: sustainedDps.shieldsdps.explosive, + kineticsdps: sustainedDps.shieldsdps.kinetic, + thermalsdps: sustainedDps.shieldsdps.thermal, + totalsdps: sustainedDps.shieldsdps.absolute + sustainedDps.shieldsdps.explosive + sustainedDps.shieldsdps.kinetic + sustainedDps.shieldsdps.thermal, + totalseps: sustainedDps.eps + } : {}; + + const armourdamage = { + absolutesdps: sustainedDps.armoursdps.absolute, + explosivesdps: sustainedDps.armoursdps.explosive, + kineticsdps: sustainedDps.armoursdps.kinetic, + thermalsdps: sustainedDps.armoursdps.thermal, + totalsdps: sustainedDps.armoursdps.absolute + sustainedDps.armoursdps.explosive + sustainedDps.armoursdps.kinetic + sustainedDps.armoursdps.thermal, + totalseps: sustainedDps.eps + }; + + return { shield, armour, shielddamage, armourdamage }; +} + +/** + * Calculate offence metrics for a ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} wep The pips to WEP + * @param {int} opponentSys The pips to opponent's SYS + * @param {int} engagementrange The range between the ship and opponent + * @returns {array} Offence metrics + */ +export function offenceMetrics(ship, opponent, wep, opponentSys, engagementrange) { + // Per-weapon and total damage + const damage = []; + + // Obtain the opponent's shield and armour metrics + const opponentShields = this.shieldMetrics(opponent, opponentSys); + const opponentArmour = this.armourMetrics(opponent); + + // Per-weapon and total damage to shields + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + + const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; + let engineering; + if (m.blueprint && m.blueprint.name) { + engineering = m.blueprint.name + ' ' + 'grade' + ' ' + m.blueprint.grade; + if (m.blueprint.special && m.blueprint.special.id >= 0) { + engineering += ', ' + m.blueprint.special.name; + } + } + + const weaponSustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange); + damage.push({ + id: i, + mount: m.mount, + name: m.name || m.grp, + classRating, + engineering, + sdps: weaponSustainedDps.damage, + seps: weaponSustainedDps.eps, + effectiveness: weaponSustainedDps.effectiveness + }); + } + } + + return damage; +} + +/** + * Calculate the resistance provided by SYS pips + * @param {integer} sys the value of the SYS pips + * @returns {integer} the resistance for the given pips + */ +export function sysResistance(sys) { + return Math.pow(sys, 0.85) * 0.6 / Math.pow(4, 0.85); +} + +/** + * Obtain the recharge rate of the SYS capacitor of a power distributor given pips + * @param {Object} pd The power distributor + * @param {number} sys The number of pips to SYS + * @returns {number} The recharge rate in MJ/s + */ +export function sysRechargeRate(pd, sys) { + return pd.getSystemsRechargeRate() * Math.pow(sys, 1.1) / Math.pow(4, 1.1); +} + +/** + * Calculate the sustained DPS for a ship against an opponent at a given range + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {number} sys Pips to opponent's SYS + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function sustainedDps(ship, opponent, sys, engagementrange) { + // Obtain the opponent's shield and armour metrics + const opponentShields = this.shieldMetrics(opponent, sys); + const opponentArmour = this.armourMetrics(opponent); + + return this._sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange); +} + +/** + * Calculate the sustained DPS for a ship against an opponent at a given range + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shield resistances + * @param {Object} opponentArmour The opponent's armour resistances + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange) { + const shieldsdps = { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0 + }; + + const armoursdps = { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0 + }; + + let eps = 0; + + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].m && ship.hardpoints[i].enabled && ship.hardpoints[i].maxClass > 0) { + const m = ship.hardpoints[i].m; + const sustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange); + shieldsdps.absolute += sustainedDps.damage.shields.absolute; + shieldsdps.explosive += sustainedDps.damage.shields.explosive; + shieldsdps.kinetic += sustainedDps.damage.shields.kinetic; + shieldsdps.thermal += sustainedDps.damage.shields.thermal; + armoursdps.absolute += sustainedDps.damage.armour.absolute; + armoursdps.explosive += sustainedDps.damage.armour.explosive; + armoursdps.kinetic += sustainedDps.damage.armour.kinetic; + armoursdps.thermal += sustainedDps.damage.armour.thermal; + eps += sustainedDps.eps; + } + } + + return { shieldsdps, armoursdps, eps }; +} + +/** + * Calculate the sustained DPS for a weapon at a given range + * @param {Object} m The weapon + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shield resistances + * @param {Object} opponentArmour The opponent's armour resistances + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) { + const opponentHasShields = opponentShields.generator ? true : false; + const weapon = { + eps: 0, + damage: { + shields: { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0, + total: 0 + }, + armour: { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0, + total: 0 + }, + }, + effectiveness: { + shields: { + range: 1, + sys: opponentHasShields ? opponentShields.absolute.sys : 1, + resistance: 1 + }, + armour: { + range: 1, + hardness: 1, + resistance: 1 + } + } + }; + + // EPS + weapon.eps = m.getClip() ? (m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getEps(); + + // Initial sustained DPS + let sDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps(); + + // Take fall-off in to account + const falloff = m.getFalloff(); + if (falloff && engagementrange > falloff) { + const dropoffRange = m.getRange() - falloff; + const dropoff = 1 - Math.min((engagementrange - falloff) / dropoffRange, 1); + weapon.effectiveness.shields.range = weapon.effectiveness.armour.range = dropoff; + sDps *= dropoff; + } + + // Piercing/hardness modifier (for armour only) + const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness; + weapon.effectiveness.armour.hardness = armourMultiple; + + // Break out the damage according to type + let shieldsResistance = 0; + let armourResistance = 0; + if (m.getDamageDist().A) { + weapon.damage.shields.absolute += sDps * m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.total : 1); + weapon.damage.armour.absolute += sDps * m.getDamageDist().A * armourMultiple * opponentArmour.absolute.total; + shieldsResistance += m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.generator * opponentShields.absolute.boosters : 1); + armourResistance += m.getDamageDist().A * opponentArmour.absolute.bulkheads * opponentArmour.absolute.reinforcement; + } + if (m.getDamageDist().E) { + weapon.damage.shields.explosive += sDps * m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.total : 1); + weapon.damage.armour.explosive += sDps * m.getDamageDist().E * armourMultiple * opponentArmour.explosive.total; + shieldsResistance += m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.generator * opponentShields.explosive.boosters : 1); + armourResistance += m.getDamageDist().E * opponentArmour.explosive.bulkheads * opponentArmour.explosive.reinforcement; + } + if (m.getDamageDist().K) { + weapon.damage.shields.kinetic += sDps * m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.total : 1); + weapon.damage.armour.kinetic += sDps * m.getDamageDist().K * armourMultiple * opponentArmour.kinetic.total; + shieldsResistance += m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.generator * opponentShields.kinetic.boosters : 1); + armourResistance += m.getDamageDist().K * opponentArmour.kinetic.bulkheads * opponentArmour.kinetic.reinforcement; + } + if (m.getDamageDist().T) { + weapon.damage.shields.thermal += sDps * m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.total : 1); + weapon.damage.armour.thermal += sDps * m.getDamageDist().T * armourMultiple * opponentArmour.thermal.total; + shieldsResistance += m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.generator * opponentShields.thermal.boosters : 1); + armourResistance += m.getDamageDist().T * opponentArmour.thermal.bulkheads * opponentArmour.thermal.reinforcement; + } + weapon.damage.shields.total = weapon.damage.shields.absolute + weapon.damage.shields.explosive + weapon.damage.shields.kinetic + weapon.damage.shields.thermal; + weapon.damage.armour.total = weapon.damage.armour.absolute + weapon.damage.armour.explosive + weapon.damage.armour.kinetic + weapon.damage.armour.thermal; + + weapon.effectiveness.shields.resistance *= shieldsResistance; + weapon.effectiveness.armour.resistance *= armourResistance; + + weapon.effectiveness.shields.total = weapon.effectiveness.shields.range * weapon.effectiveness.shields.sys * weapon.effectiveness.shields.resistance; + weapon.effectiveness.armour.total = weapon.effectiveness.armour.range * weapon.effectiveness.armour.resistance * weapon.effectiveness.armour.hardness; + return weapon; +} + +/** + * Calculate time to drain WEP capacitor + * @param {object} ship The ship + * @param {number} wep Pips to WEP + * @returns {number} The time to drain the WEP capacitor, in seconds + */ +export function timeToDrainWep(ship, wep) { + let totalSEps = 0; + + for (let slotNum in ship.hardpoints) { + const slot = ship.hardpoints[slotNum]; + if (slot.maxClass > 0 && slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) { + totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps(); + } + } + + // Calculate the drain time + const drainPerSecond = totalSEps - ship.standard[4].m.getWeaponsRechargeRate() * wep / 4; + if (drainPerSecond <= 0) { + // Can fire forever + return Infinity; + } else { + const initialCharge = ship.standard[4].m.getWeaponsCapacity(); + return initialCharge / drainPerSecond; + } +} + +/** + * Calculate the time to deplete an amount of shields or armour + * @param {number} amount The amount to be depleted + * @param {number} dps The depletion per second + * @param {number} eps The energy drained per second + * @param {number} capacity The initial energy capacity + * @param {number} recharge The energy recharged per second + * @returns {number} The number of seconds to deplete to 0 + */ +export function timeToDeplete(amount, dps, eps, capacity, recharge) { + const drainPerSecond = eps - recharge; + if (drainPerSecond <= 0) { + // Simple result + return amount / dps; + } else { + // We are draining the capacitor, but can we deplete before we run out + const timeToDrain = capacity / drainPerSecond; + const depletedBeforeDrained = dps * timeToDrain; + if (depletedBeforeDrained >= amount) { + return amount / dps; + } else { + const restToDeplete = amount - depletedBeforeDrained; + // We delete the rest at the reduced rate + const reducedDps = dps * (recharge / eps); + return timeToDrain + (restToDeplete / reducedDps); + } + } +} diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js index a273c578..4e2d8012 100755 --- a/src/app/shipyard/Constants.js +++ b/src/app/shipyard/Constants.js @@ -47,6 +47,7 @@ export const ModuleGroupToName = { pcm: 'First Class Passenger Cabin', pcq: 'Luxury Passenger Cabin', cc: 'Collector Limpet Controller', + ss: 'Surface Scanner', // Hard Points bl: 'Beam Laser', diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index e013609b..2ea29321 100755 --- a/src/app/shipyard/Module.js +++ b/src/app/shipyard/Module.js @@ -554,10 +554,10 @@ export default class Module { getEps() { // EPS is a synthetic value let distdraw = this.getDistDraw(); - let rpshot = this.roundspershot || 1; + // We don't use rpshot here as dist draw is per combined shot let rof = this.getRoF() || 1; - return distdraw * rpshot * rof; + return distdraw * rof; } /** @@ -567,10 +567,10 @@ export default class Module { getHps() { // HPS is a synthetic value let heat = this.getThermalLoad(); - let rpshot = this.roundspershot || 1; + // We don't use rpshot here as dist draw is per combined shot let rof = this.getRoF() || 1; - return heat * rpshot * rof; + return heat * rof; } /** diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 2370a295..d207279e 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1,6 +1,7 @@ import * as Calc from './Calculations'; import * as ModuleUtils from './ModuleUtils'; import * as Utils from '../utils/UtilityFunctions'; +import { getBlueprint } from '../utils/BlueprintFunctions'; import Module from './Module'; import LZString from 'lz-string'; import * as _ from 'lodash'; @@ -138,17 +139,6 @@ export default class Ship { this.standard[4].m.getEnginesCapacity() > this.boostEnergy; // PD capacitor is sufficient for boost } - /** - * Calculate hypothetical jump range using the installed FSD and the - * specified mass which can be more or less than ships actual mass - * @param {Number} fuel Fuel available in tons - * @param {Number} cargo Cargo in tons - * @return {Number} Jump range in Light Years - */ - calcJumpRangeWith(fuel, cargo) { - return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); - } - /** * Calculate the hypothetical laden jump range based on a potential change in mass, fuel, or FSD * @param {Number} massDelta Optional - Change in laden mass (mass + cargo + fuel) @@ -173,17 +163,6 @@ export default class Ship { return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel); } - /** - * Calculate cumulative (total) jump range when making longest jumps using the installed FSD and the - * specified mass which can be more or less than ships actual mass - * @param {Number} fuel Fuel available in tons - * @param {Number} cargo Cargo in tons - * @return {Number} Total/Cumulative Jump range in Light Years - */ - calcFastestRangeWith(fuel, cargo) { - return Calc.fastestRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); - } - /** * Calculate the hypothetical top speeds at cargo and fuel tonnage * @param {Number} fuel Fuel available in tons @@ -194,6 +173,54 @@ export default class Ship { return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed); } + /** + * Calculate the speed for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Speed + */ + calcSpeed(eng, fuel, cargo, boost) { + return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost); + } + + /** + * Calculate the pitch for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Pitch + */ + calcPitch(eng, fuel, cargo, boost) { + return Calc.calcPitch(this.unladenMass + fuel + cargo, this.pitch, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the roll for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Roll + */ + calcRoll(eng, fuel, cargo, boost) { + return Calc.calcRoll(this.unladenMass + fuel + cargo, this.roll, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the yaw for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Yaw + */ + calcYaw(eng, fuel, cargo, boost) { + return Calc.calcYaw(this.unladenMass + fuel + cargo, this.yaw, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + /** * Calculate the recovery time after losing or turning on shields * Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas @@ -427,7 +454,6 @@ export default class Ship { .recalculateDps() .recalculateEps() .recalculateHps() - .recalculateTtd() .updateMovement(); } @@ -496,15 +522,15 @@ export default class Ship { this.recalculateDps(); this.recalculateHps(); this.recalculateEps(); - this.recalculateTtd(); } else if (name === 'explres' || name === 'kinres' || name === 'thermres') { m.setModValue(name, value, sentfromui); // Could be for shields or armour this.recalculateArmour(); this.recalculateShield(); - } else if (name === 'wepcap' || name === 'weprate') { + } else if (name === 'engcap') { m.setModValue(name, value, sentfromui); - this.recalculateTtd(); + // Might have resulted in a change in boostability + this.updateMovement(); } else { // Generic m.setModValue(name, value, sentfromui); @@ -563,7 +589,13 @@ export default class Ship { this.bulkheads.m = null; this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true); this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {}; - this.bulkheads.m.blueprint = blueprints && blueprints[0] ? blueprints[0] : {}; + if (blueprints && blueprints[0]) { + this.bulkheads.m.blueprint = getBlueprint(blueprints[0].fdname, this.bulkheads.m); + this.bulkheads.m.blueprint.grade = blueprints[0].grade; + this.bulkheads.m.blueprint.special = blueprints[0].special; + } else { + this.bulkheads.m.blueprint = {}; + } this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0; this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true; @@ -577,7 +609,13 @@ export default class Ship { let module = ModuleUtils.standard(i, comps.standard[i]); if (module != null) { module.mods = mods && mods[i + 1] ? mods[i + 1] : {}; - module.blueprint = blueprints && blueprints[i + 1] ? blueprints[i + 1] : {}; + if (blueprints && blueprints[i + 1]) { + module.blueprint = getBlueprint(blueprints[i + 1].fdname, module); + module.blueprint.grade = blueprints[i + 1].grade; + module.blueprint.special = blueprints[i + 1].special; + } else { + module.blueprint = {}; + } } this.use(standard[i], module, true); } @@ -599,7 +637,13 @@ export default class Ship { let module = ModuleUtils.hardpoints(comps.hardpoints[i]); if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; - module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; + if (blueprints && blueprints[cl + i]) { + module.blueprint = getBlueprint(blueprints[cl + i].fdname, module); + module.blueprint.grade = blueprints[cl + i].grade; + module.blueprint.special = blueprints[cl + i].special; + } else { + module.blueprint = {}; + } } this.use(hps[i], module, true); } @@ -619,7 +663,13 @@ export default class Ship { let module = ModuleUtils.internal(comps.internal[i]); if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; - module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; + if (blueprints && blueprints[cl + i]) { + module.blueprint = getBlueprint(blueprints[cl + i].fdname, module); + module.blueprint.grade = blueprints[cl + i].grade; + module.blueprint.special = blueprints[cl + i].special; + } else { + module.blueprint = {}; + } } this.use(internal[i], module, true); } @@ -638,7 +688,6 @@ export default class Ship { .recalculateDps() .recalculateEps() .recalculateHps() - .recalculateTtd() .updateMovement(); } @@ -820,7 +869,6 @@ export default class Ship { if (slot.m.getEps()) { this.recalculateEps(); - this.recalculateTtd(); } } } @@ -896,7 +944,6 @@ export default class Ship { } if (epsChanged) { this.recalculateEps(); - this.recalculateTtd(); } if (hpsChanged) { this.recalculateHps(); @@ -904,9 +951,6 @@ export default class Ship { if (powerGeneratedChange) { this.updatePowerGenerated(); } - if (powerDistributorChange) { - this.recalculateTtd(); - } if (powerUsedChange) { this.updatePowerUsed(); } @@ -944,33 +988,6 @@ export default class Ship { return val; } - /** - * Calculate time to drain WEP capacitor - * @return {this} The ship instance (for chaining operations) - */ - recalculateTtd() { - let totalSEps = 0; - - for (let slotNum in this.hardpoints) { - const slot = this.hardpoints[slotNum]; - if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) { - totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps(); - } - } - - // Calculate the drain time - const drainPerSecond = totalSEps - this.standard[4].m.getWeaponsRechargeRate(); - if (drainPerSecond <= 0) { - // Can fire forever - this.timeToDrain = Infinity; - } else { - const initialCharge = this.standard[4].m.getWeaponsCapacity(); - this.timeToDrain = initialCharge / drainPerSecond; - } - - return this; - } - /** * Calculate damage per second and related items for weapons * @return {this} The ship instance (for chaining operations) @@ -1238,13 +1255,13 @@ export default class Ship { shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); shieldExplRes = 1 - sgSlot.m.getExplosiveResistance(); shieldExplDRStart = shieldExplRes * 0.7; - shieldExplDREnd = shieldExplRes * 0; // Currently don't know where this is + shieldExplDREnd = 0; shieldKinRes = 1 - sgSlot.m.getKineticResistance(); shieldKinDRStart = shieldKinRes * 0.7; - shieldKinDREnd = shieldKinRes * 0; // Currently don't know where this is + shieldKinDREnd = 0; shieldThermRes = 1 - sgSlot.m.getThermalResistance(); shieldThermDRStart = shieldThermRes * 0.7; - shieldThermDREnd = shieldThermRes * 0; // Currently don't know where this is + shieldThermDREnd = 0; // Shield from boosters for (let slot of this.hardpoints) { @@ -1300,13 +1317,13 @@ export default class Ship { let moduleprotection = 1; let hullExplRes = 1 - bulkhead.getExplosiveResistance(); const hullExplResDRStart = hullExplRes * 0.7; - const hullExplResDREnd = hullExplRes * 0; // Currently don't know where this is + const hullExplResDREnd = hullExplRes * 0; let hullKinRes = 1 - bulkhead.getKineticResistance(); const hullKinResDRStart = hullKinRes * 0.7; - const hullKinResDREnd = hullKinRes * 0; // Currently don't know where this is + const hullKinResDREnd = hullKinRes * 0; let hullThermRes = 1 - bulkhead.getThermalResistance(); const hullThermResDRStart = hullThermRes * 0.7; - const hullThermResDREnd = hullThermRes * 0; // Currently don't know where this is + const hullThermResDREnd = hullThermRes * 0; // Armour from HRPs and module armour from MRPs for (let slot of this.internal) { @@ -1346,9 +1363,9 @@ export default class Ship { this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank this.ladenRange = this.calcLadenRange(); // Includes full tank and caro - this.unladenFastestRange = Calc.fastestRange(unladenMass, fsd, fuelCapacity); - this.ladenFastestRange = Calc.fastestRange(unladenMass + this.cargoCapacity, fsd, fuelCapacity); - this.maxJumpCount = Math.ceil(fuelCapacity / fsd.maxfuel); + this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity); + this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity); + this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump()); return this; } diff --git a/src/app/stores/Persist.js b/src/app/stores/Persist.js index 4ef740f6..e1ea7b82 100644 --- a/src/app/stores/Persist.js +++ b/src/app/stores/Persist.js @@ -5,6 +5,7 @@ const LS_KEY_BUILDS = 'builds'; const LS_KEY_COMPARISONS = 'comparisons'; const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY'; const LS_KEY_COST_TAB = 'costTab'; +const LS_KEY_OUTFITTING_TAB = 'outfittingTab'; const LS_KEY_INSURANCE = 'insurance'; const LS_KEY_SHIP_DISCOUNT = 'shipDiscount'; const LS_KEY_MOD_DISCOUNT = 'moduleDiscount'; @@ -98,6 +99,7 @@ export class Persist extends EventEmitter { this.builds = buildJson && typeof buildJson == 'object' ? buildJson : {}; this.comparisons = comparisonJson && typeof comparisonJson == 'object' ? comparisonJson : {}; this.costTab = _getString(LS_KEY_COST_TAB); + this.outfittingTab = _getString(LS_KEY_OUTFITTING_TAB); this.state = _get(LS_KEY_STATE); this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1; this.tooltipsEnabled = tips === null ? true : tips; @@ -472,6 +474,22 @@ export class Persist extends EventEmitter { return this.costTab; } + /** + * Persist selected outfitting tab + * @param {string} tabName Cost tab name + */ + setOutfittingTab(tabName) { + this.outfittingTab = tabName; + _put(LS_KEY_OUTFITTING_TAB, tabName); + } + /** + * Get the current outfitting tab + * @return {string} the current outfitting tab + */ + getOutfittingTab() { + return this.outfittingTab; + } + /** * Retrieve the last router state from local storage * @return {Object} state State object containing state name and params diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js new file mode 100644 index 00000000..7dcddf04 --- /dev/null +++ b/src/app/utils/BlueprintFunctions.js @@ -0,0 +1,145 @@ +import React from 'react'; +import { Modifications } from 'coriolis-data/dist'; + +/** + * Generate a tooltip with details of a blueprint's effects + * @param {Object} features The features of the blueprint + * @param {Object} m The module to compare with + * @returns {Object} The react components + */ +export function blueprintTooltip(translate, features, m) +{ + const results = []; + for (const feature in features) { + const featureIsBeneficial = isBeneficial(feature, features[feature]); + const featureDef = Modifications.modifications[feature]; + if (!featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let lowerBound = features[feature][0]; + let upperBound = features[feature][1]; + if (featureDef.type === 'percentage') { + lowerBound = Math.round(lowerBound * 1000) / 10; + upperBound = Math.round(upperBound * 1000) / 10; + } + const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); + const upperIsBeneficial = isValueBeneficial(feature, upperBound); + if (m) { + // We have a module - add in the current value + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + results.push( +
    + + + + + + ); + } else { + // We do not have a module, no value + results.push( + + + + + + ); + } + } + } + + return ( +
    {translate('speed')}{translate('boost')}{translate('DPS')}{translate('EPS')}{translate('TTD')}{translate('HPS')}{translate('hrd')}{translate('arm')}{translate('shld')}{translate('mass')}{translate('speed')}{translate('boost')}{translate('jump range')}{translate('shield')}{translate('integrity')}{translate('DPS')}{translate('EPS')}{translate('TTD')}{translate('HPS')}{translate('cargo')} {translate('fuel')}{translate('jump range')}{translate('fastest range')}{translate('mass')}{translate('hrd')} {translate('crew')}{translate('MLF')}{translate('MLF')}
    {translate('hull')}{translate('unladen')}{translate('laden')} {translate('max')}{translate('full tank')}{translate('unladen')} {translate('laden')}{translate('jumps')}{translate('total unladen')}{translate('total laden')}{translate('hull')} {translate('unladen')} {translate('laden')}
    { ship.canThrust() ? {int(ship.topSpeed)} {u['m/s']} : 0 }{ ship.canBoost() ? {int(ship.topBoost)} {u['m/s']} : 0 }{f1(ship.totalDps)}{f1(ship.totalEps)}{ship.timeToDrain === Infinity ? '∞' : time(ship.timeToDrain)}{f1(ship.totalHps)}{ canThrust ? {int(ship.calcSpeed(4, ship.fuelCapacity, 0, false))}{u['m/s']} : 0 }{ canBoost ? {int(ship.calcSpeed(4, ship.fuelCapacity, 0, true))}{u['m/s']} : 0 }{f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump()))}{u.LY}{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{int(ship.shield)}{u.MJ}{int(ship.armour)}{f1(ship.totalDps)}{f1(ship.totalEps)}{timeToDrain === Infinity ? '∞' : time(timeToDrain)}{f1(ship.totalHps)}{round(ship.cargoCapacity)}{u.T}{round(ship.fuelCapacity)}{u.T}{ship.hullMass}{u.T}{int(ship.unladenMass)}{u.T}{int(ship.ladenMass)}{u.T} {int(ship.hardness)}{int(ship.armour)}{int(ship.shield)} {u.MJ}{ship.hullMass} {u.T}{int(ship.unladenMass)} {u.T}{int(ship.ladenMass)} {u.T}{round(ship.cargoCapacity)} {u.T}{round(ship.fuelCapacity)} {u.T}{f2(ship.unladenRange)} {u.LY}{f2(ship.fullTankRange)} {u.LY}{f2(ship.ladenRange)} {u.LY}{int(ship.maxJumpCount)}{f2(ship.unladenFastestRange)} {u.LY}{f2(ship.ladenFastestRange)} {u.LY} {ship.crew} {ship.masslock}
    {translate(feature)}{lowerBound}{symbol}{current}{symbol}{upperBound}{symbol}
    {translate(feature)}{lowerBound}{symbol}{upperBound}{symbol}
    + + + + + {m ? : null } + + + + + {results} + +
    {translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
    + ); +} + +/** + * Is this blueprint feature beneficial? + * + */ +export function isBeneficial(feature, values) { + const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); + if (Modifications.modifications[feature].higherbetter) { + return !fact; + } else { + return fact; + } +} + +/** + * Is this feature value beneficial? + * + */ +export function isValueBeneficial(feature, value) { + if (Modifications.modifications[feature].higherbetter) { + return value > 0; + } else { + return value < 0; + } +} + +/** + * Get a blueprint with a given name and an optional module + * @param {string} name The name of the blueprint + * @param {Object} module The module for which to obtain this blueprint + * @returns {Object} The matching blueprint + */ +export function getBlueprint(name, module) { + // Start with a copy of the blueprint + const blueprint = JSON.parse(JSON.stringify(Modifications.blueprints[name])); + if (module) { + if (module.grp === 'bh' || module.grp === 'hr') { + // Bulkheads and hull reinforcements need to have their resistances altered by the base values + for (const grade in blueprint.grades) { + for (const feature in blueprint.grades[grade].features) { + if (feature === 'explres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.explres); + blueprint.grades[grade].features[feature][1] *= (1 - module.explres); + } + if (feature === 'kinres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.kinres); + blueprint.grades[grade].features[feature][1] *= (1 - module.kinres); + } + if (feature === 'thermres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.thermres); + blueprint.grades[grade].features[feature][1] *= (1 - module.thermres); + } + } + } + } + if (module.grp === 'sb') { + // Shield boosters are treated internally as straight modifiers, so rather than (for example) + // being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look + // accurate as per the information in Elite + for (const grade in blueprint.grades) { + for (const feature in blueprint.grades[grade].features) { + if (feature === 'shieldboost') { + blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1)/ module.shieldboost - 1; + blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1)/ module.shieldboost - 1; + } + } + } + } + } + return blueprint; +} diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js index 461886cb..47a1db01 100644 --- a/src/app/utils/CompanionApiUtils.js +++ b/src/app/utils/CompanionApiUtils.js @@ -2,6 +2,7 @@ import React from 'react'; import { Modifications, Modules, Ships } from 'coriolis-data/dist'; import Module from '../shipyard/Module'; import Ship from '../shipyard/Ship'; +import { getBlueprint } from '../utils/BlueprintFunctions'; // mapping from fd's ship model names to coriolis' const SHIP_FD_NAME_TO_CORIOLIS_NAME = { @@ -335,9 +336,9 @@ function _addModifications(module, modifiers, blueprint, grade) { } } - // Add the blueprint ID, grade and special + // Add the blueprint definition, grade and special if (blueprint) { - module.blueprint = Object.assign({}, Modifications.blueprints[blueprint]); + module.blueprint = getBlueprint(blueprint, module); if (grade) { module.blueprint.grade = Number(grade); } @@ -384,15 +385,16 @@ function _addModifications(module, modifiers, blueprint, grade) { } // Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted. + // In addition, the modification is based off the inherent resistance of the module if (module.grp === 'hr') { if (module.getModValue('explres')) { - module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000); + module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000); } if (module.getModValue('kinres')) { - module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000); + module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000); } if (module.getModValue('thermres')) { - module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000); + module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000); } } diff --git a/src/less/app.less b/src/less/app.less index 24ade30c..5c015eff 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -19,6 +19,12 @@ @import 'shipselector'; @import 'sortable'; @import 'loader'; +@import 'pips'; +@import 'boost'; +@import 'movement'; +@import 'shippicker'; +@import 'defence'; +@import 'offence'; html, body { height: 100%; diff --git a/src/less/boost.less b/src/less/boost.less new file mode 100755 index 00000000..75db11be --- /dev/null +++ b/src/less/boost.less @@ -0,0 +1,14 @@ +#boost { + button { + font-size: 1.2em; + background: @primary-bg; + color: @primary; + border: 1px solid @primary; + &.selected { + // Shown when button is selected + background: @primary; + color: @primary-bg; + } + } +} + diff --git a/src/less/charts.less b/src/less/charts.less index 556e15d8..22006184 100755 --- a/src/less/charts.less +++ b/src/less/charts.less @@ -44,6 +44,9 @@ svg { .label, .text-tip { text-transform: capitalize; + } + + .x { fill: @fg; } diff --git a/src/less/defence.less b/src/less/defence.less new file mode 100755 index 00000000..2c5c730e --- /dev/null +++ b/src/less/defence.less @@ -0,0 +1,14 @@ +#defence { + table { + background-color: @bgBlack; + color: @primary; + margin: 0 auto; + } + + .icon { + stroke: @primary; + stroke-width: 20; + fill: transparent; + } +} + diff --git a/src/less/movement.less b/src/less/movement.less new file mode 100644 index 00000000..796468f5 --- /dev/null +++ b/src/less/movement.less @@ -0,0 +1,14 @@ + +#movement { + svg { + width: 75%; + height: 75%; + stroke: @primary-disabled; + fill: @primary-disabled; + + text { + stroke: @primary; + font-size: 2em; + } + } +} diff --git a/src/less/offence.less b/src/less/offence.less new file mode 100755 index 00000000..39679f27 --- /dev/null +++ b/src/less/offence.less @@ -0,0 +1,14 @@ +#offence { + table { + background-color: @bgBlack; + color: @fg; + margin: 0 auto; + } + + .icon { + stroke: @fg; + stroke-width: 20; + fill: transparent; + } +} + diff --git a/src/less/outfit.less b/src/less/outfit.less index 8d5e7f0e..092fd499 100755 --- a/src/less/outfit.less +++ b/src/less/outfit.less @@ -190,9 +190,47 @@ }); } + &.quarter { + width: 25%; + + .tablet({ + td { + line-height: 2em; + } + }); + + .smallTablet({ + width: 50% !important; + }); + } + &.third { width: 33%; + .smallTablet({ + width: 50% !important; + }); + } + + &.twothirds { + width: 67%; + + .smallTablet({ + width: 100% !important; + }); + } + + &.threequarters { + width: 75%; + + .smallTablet({ + width: 100% !important; + }); + } + + &.full { + width: 100%; + .smallTablet({ width: 100% !important; }); diff --git a/src/less/pips.less b/src/less/pips.less new file mode 100755 index 00000000..2d3dccbd --- /dev/null +++ b/src/less/pips.less @@ -0,0 +1,33 @@ +// The pips table - keep the background black +#pips { + + table { + background-color: @bgBlack; + color: @primary; + margin: 0 auto; + } + + // A clickable entity in the pips table + .clickable { + cursor: pointer; + } + + // A full pip + .full { + stroke: @primary; + fill: @primary; + } + + // A half pip + .half { + stroke: @primary-disabled; + fill: @primary-disabled; + } + + // An empty pip + .empty { + stroke: @primary-bg; + fill: @primary-bg; + } +} + diff --git a/src/less/shippicker.less b/src/less/shippicker.less new file mode 100755 index 00000000..bd5c4c17 --- /dev/null +++ b/src/less/shippicker.less @@ -0,0 +1,176 @@ +.shippicker { + background-color: @bgBlack; + margin: 0; + height: 3em; + font-family: @fTitle; + vertical-align: middle; + position: relative; + display: block; + + .user-select-none(); + + .menu { + position: relative; + cursor: default; + + &.r { + .menu-list { + right: 0; + } + } + + .smallTablet({ + position: static; + position: initial; + }); + } + + .menu-header { + height: 100%; + z-index: 2; + padding : 0 1em; + cursor: pointer; + color: @warning; + text-transform: uppercase; + + &.disabled { + color: @warning-disabled; + cursor: default; + } + + &.selected { + background-color: @bgBlack; + } + + .menu-item-label { + margin-left: 1em; + + .smallTablet({ + display: none; + }); + } + } + + .menu-list { + font-family: @fStandard; + position: absolute; + padding: 0.5em 1em; + box-sizing: border-box; + min-width: 100%; + overflow-x: hidden; + background-color: @bgBlack; + font-size: 0.9em; + overflow-y: auto; + z-index: 1; + -webkit-overflow-scrolling: touch; + max-height: 500px; + + &::-webkit-scrollbar { + width: 0.5em; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: @warning-disabled; + } + + input { + border: none; + background-color: transparent; + text-align: right; + font-size: 1em; + font-family: @fStandard; + } + + .smallTablet({ + max-height: 400px; + left: 0; + right: 0; + border-bottom: 1px solid @bg; + }); + + .tablet({ + li, a { + padding: 0.3em 0; + } + }); + } + + .quad { + -webkit-column-count: 4; /* Chrome, Safari, Opera */ + -moz-column-count: 4; /* Firefox */ + column-count: 4; + ul { + min-width: 10em; + } + + .smallTablet({ + -webkit-column-count: 3; /* Chrome, Safari, Opera */ + -moz-column-count: 3; /* Firefox */ + column-count: 3; + + ul { + min-width: 20em; + } + }); + + .largePhone({ + -webkit-column-count: 2; /* Chrome, Safari, Opera */ + -moz-column-count: 2; /* Firefox */ + column-count: 2; + }); + + .smallPhone({ + -webkit-column-count: 1; /* Chrome, Safari, Opera */ + -moz-column-count: 1; /* Firefox */ + column-count: 1; + }); + } + + ul { + display: inline-block; + white-space: nowrap; + margin: 0 0 0.5em; + padding: 0; + line-height: 1.3em; + color: @fg; + } + + li { + white-space: normal; + list-style: none; + margin-left: 1em; + line-height: 1.1em; + color: @warning; + cursor: pointer; + + &.selected { + color: @primary; + } + } + + hr { + border: none; + border-top: 1px solid @disabled; + } + + .no-wrap { + overflow-x: auto; + white-space: nowrap; + } + + .block { + display: block; + line-height: 1.5em; + } + + .title { + font-size: 1.3em; + display: inline-block; + margin:0px; + text-transform: uppercase; + } +}