Fixing old errors

This commit is contained in:
Alex Williams
2024-10-31 21:43:29 +00:00
parent fde4bba714
commit f22b7ff577
7 changed files with 1305 additions and 1305 deletions

View File

@@ -1,281 +1,281 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ContainerDimensions from 'react-container-dimensions'; import ContainerDimensions from 'react-container-dimensions';
import * as d3 from 'd3'; import * as d3 from 'd3';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
/** /**
* Line Chart * Line Chart
*/ */
export default class LineChart extends TranslatedComponent { export default class LineChart extends TranslatedComponent {
static defaultProps = { static defaultProps = {
code: '', code: '',
xMin: 0, xMin: 0,
yMin: 0, yMin: 0,
points: 20, points: 20,
colors: ['#ff8c0d'], colors: ['#ff8c0d'],
aspect: 0.5 aspect: 0.5
}; };
static propTypes = { static propTypes = {
func: PropTypes.func.isRequired, func: PropTypes.func.isRequired,
xLabel: PropTypes.string.isRequired, xLabel: PropTypes.string.isRequired,
xMin: PropTypes.number, xMin: PropTypes.number,
xMax: PropTypes.number.isRequired, xMax: PropTypes.number.isRequired,
xUnit: PropTypes.string.isRequired, xUnit: PropTypes.string.isRequired,
xMark: PropTypes.number, xMark: PropTypes.number,
yLabel: PropTypes.string.isRequired, yLabel: PropTypes.string.isRequired,
yMin: PropTypes.number, yMin: PropTypes.number,
yMax: PropTypes.number.isRequired, yMax: PropTypes.number.isRequired,
yUnit: PropTypes.string, yUnit: PropTypes.string,
series: PropTypes.array, series: PropTypes.array,
colors: PropTypes.array, colors: PropTypes.array,
points: PropTypes.number, points: PropTypes.number,
aspect: PropTypes.number, aspect: PropTypes.number,
code: PropTypes.string, code: PropTypes.string,
}; };
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {Object} context React Component context * @param {Object} context React Component context
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this._updateDimensions = this._updateDimensions.bind(this); this._updateDimensions = this._updateDimensions.bind(this);
this._updateSeries = this._updateSeries.bind(this); this._updateSeries = this._updateSeries.bind(this);
this._tooltip = this._tooltip.bind(this); this._tooltip = this._tooltip.bind(this);
this._showTip = this._showTip.bind(this); this._showTip = this._showTip.bind(this);
this._hideTip = this._hideTip.bind(this); this._hideTip = this._hideTip.bind(this);
this._moveTip = this._moveTip.bind(this); this._moveTip = this._moveTip.bind(this);
const series = props.series; const series = props.series;
let xScale = d3.scaleLinear(); let xScale = d3.scaleLinear();
let yScale = d3.scaleLinear(); let yScale = d3.scaleLinear();
let xAxisScale = d3.scaleLinear(); let xAxisScale = d3.scaleLinear();
this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0); this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0);
this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0);
this.state = { this.state = {
xScale, xScale,
xAxisScale, xAxisScale,
yScale, yScale,
tipHeight: 2 + (1.2 * (series ? series.length : 0.8)), tipHeight: 2 + (1.2 * (series ? series.length : 0.8)),
}; };
} }
/** /**
* Update tooltip content * Update tooltip content
* @param {number} xPos x coordinate * @param {number} xPos x coordinate
* @param {number} width current container width * @param {number} width current container width
*/ */
_tooltip(xPos, width) { _tooltip(xPos, width) {
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
let { xScale, yScale } = this.state; let { xScale, yScale } = this.state;
let { formats, translate } = this.context.language; let { formats, translate } = this.context.language;
let x0 = xScale.invert(xPos), let x0 = xScale.invert(xPos),
y0 = func(x0), y0 = func(x0),
tips = this.tipContainer, tips = this.tipContainer,
yTotal = 0, yTotal = 0,
flip = (xPos / width > 0.50), flip = (xPos / width > 0.50),
tipWidth = 0, tipWidth = 0,
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
xPos = xScale(x0); // Clamp xPos xPos = xScale(x0); // Clamp xPos
tips.selectAll('text.text-tip.y').text(function(d, i) { tips.selectAll('text.text-tip.y').text(function(d, i) {
let yVal = series ? y0[series[i]] : y0; let yVal = series ? y0[series[i]] : y0;
yTotal += yVal; yTotal += yVal;
return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal); return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal);
}).append('tspan').attr('class', 'metric').text(yUnit ? ' ' + yUnit : ''); }).append('tspan').attr('class', 'metric').text(yUnit ? ' ' + yUnit : '');
tips.selectAll('text').each(function() { tips.selectAll('text').each(function() {
if (this.getBBox().width > tipWidth) { if (this.getBBox().width > tipWidth) {
tipWidth = Math.ceil(this.getBBox().width); tipWidth = Math.ceil(this.getBBox().width);
} }
}); });
let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2)); let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2));
tipWidth += 8; tipWidth += 8;
tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')'); tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')');
tips.selectAll('text.text-tip').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start'); tips.selectAll('text.text-tip').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start');
tips.selectAll('text.text-tip.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit); tips.selectAll('text.text-tip.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit);
tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start'); tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start');
this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0));
} }
/** /**
* Update dimensions based on properties and scale * 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 * @param {number} scale size ratio / scale
* @param {number} width current width of the container * @param {number} width current width of the container
* @returns {Object} calculated dimensions * @returns {Object} calculated dimensions
*/ */
_updateDimensions(props, scale, width) { _updateDimensions(props, scale, width) {
const { xMax, xMin, yMin, yMax } = props; const { xMax, xMin, yMin, yMax } = props;
const innerWidth = width - MARGIN.left - MARGIN.right; const innerWidth = width - MARGIN.left - MARGIN.right;
const outerHeight = Math.round(width * props.aspect); const outerHeight = Math.round(width * props.aspect);
const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); 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.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.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility
return { innerWidth, outerHeight, innerHeight }; return { innerWidth, outerHeight, innerHeight };
} }
/** /**
* Show tooltip * Show tooltip
* @param {SyntheticEvent} e Event * @param {SyntheticEvent} e Event
*/ */
_showTip(e) { _showTip(e) {
e.preventDefault(); e.preventDefault();
this.tipContainer.style('display', null); this.tipContainer.style('display', null);
this.markersContainer.style('display', null); this.markersContainer.style('display', null);
this._moveTip(e); this._moveTip(e);
} }
/** /**
* Move and update tooltip * Move and update tooltip
* @param {SyntheticEvent} e Event * @param {SyntheticEvent} e Event
* @param {number} width current container width * @param {number} width current container width
*/ */
_moveTip(e, width) { _moveTip(e, width) {
let clientX = e.touches ? e.touches[0].clientX : e.clientX; let clientX = e.touches ? e.touches[0].clientX : e.clientX;
this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width); this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width);
} }
/** /**
* Hide tooltip * Hide tooltip
* @param {SyntheticEvent} e Event * @param {SyntheticEvent} e Event
*/ */
_hideTip(e) { _hideTip(e) {
e.preventDefault(); e.preventDefault();
this.tipContainer.style('display', 'none'); this.tipContainer.style('display', 'none');
this.markersContainer.style('display', 'none'); this.markersContainer.style('display', 'none');
} }
/** /**
* Update series generated from props * Update series generated from props
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {Object} state React Component state * @param {Object} state React Component state
*/ */
_updateSeries(props, state) { _updateSeries(props, state) {
let { func, xMin, xMax, series, points } = props; let { func, xMin, xMax, series, points } = props;
let delta = (xMax - xMin) / points; let delta = (xMax - xMin) / points;
let seriesData = new Array(points); let seriesData = new Array(points);
if (delta) { if (delta) {
seriesData = new Array(points); seriesData = new Array(points);
for (let i = 0, x = xMin; i < points; i++) { for (let i = 0, x = xMin; i < points; i++) {
seriesData[i] = [x, func(x)]; seriesData[i] = [x, func(x)];
x += delta; x += delta;
} }
seriesData[points - 1] = [xMax, func(xMax)]; seriesData[points - 1] = [xMax, func(xMax)];
} else { } else {
let yVal = func(xMin); let yVal = func(xMin);
seriesData = [[0, yVal], [1, yVal]]; seriesData = [[0, yVal], [1, yVal]];
} }
const markerElems = []; const markerElems = [];
const detailElems = [<text key='lbl' className='text-tip x' y='1.25em'/>]; const detailElems = [<text key='lbl' className='text-tip x' y='1.25em'/>];
const seriesLines = []; const seriesLines = [];
for (let i = 0, l = series ? series.length : 1; i < l; i++) { 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]); 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)); seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor));
detailElems.push(<text key={i} className='text-tip y' strokeWidth={0} fill={props.colors[i]} y={1.25 * (i + 2) + 'em'}/>); detailElems.push(<text key={i} className='text-tip y' strokeWidth={0} fill={props.colors[i]} y={1.25 * (i + 2) + 'em'}/>);
markerElems.push(<circle key={i} className='marker' r='4' />); markerElems.push(<circle key={i} className='marker' r='4' />);
} }
const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8)); const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8));
this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight }); this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight });
} }
/** /**
* Update dimensions and series data based on props and context. * Update dimensions and series data based on props and context.
*/ */
componentWillMount() { componentWillMount() {
this._updateSeries(this.props, this.state); this._updateSeries(this.props, this.state);
} }
/** /**
* Update state based on property and context changes * Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties * @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext * @param {Object} nextContext Incoming/Next conext
*/ */
componentWillReceiveProps(nextProps, nextContext) { componentWillReceiveProps(nextProps, nextContext) {
const props = this.props; const props = this.props;
if (props.code != nextProps.code) { if (props.code != nextProps.code) {
this._updateSeries(nextProps, this.state); this._updateSeries(nextProps, this.state);
} }
} }
/** /**
* Render the chart * Render the chart
* @return {React.Component} Chart SVG * @return {React.Component} Chart SVG
*/ */
render() { render() {
return ( return (
<ContainerDimensions> <ContainerDimensions>
{ ({ width, height }) => { { ({ width, height }) => {
const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio, width, height); const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio, width, height);
const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props;
const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state;
const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse(); const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse();
const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0;
const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : ''; const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : '';
return ( return (
<div width={width} height={height}> <div width={width} height={height}>
<svg style={{ width: '100%', height: outerHeight }}> <svg style={{ width: '100%', height: outerHeight }}>
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}> <g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
<g>{xmark}</g> <g>{xmark}</g>
<g>{lines}</g> <g>{lines}</g>
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> <g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}> <text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
<tspan>{xLabel}</tspan> <tspan>{xLabel}</tspan>
<tspan className='metric'> ({xUnit})</tspan> <tspan className='metric'> ({xUnit})</tspan>
</text> </text>
</g> </g>
<g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}> <g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}>
<text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}> <text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}>
<tspan>{yLabel}</tspan> <tspan>{yLabel}</tspan>
{ yUnit && <tspan className='metric'> ({yUnit})</tspan> } { yUnit && <tspan className='metric'> ({yUnit})</tspan> }
</text> </text>
</g> </g>
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}> <g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
<rect className='tooltip' height={tipHeight + 'em'}></rect> <rect className='tooltip' height={tipHeight + 'em'}></rect>
{detailElems} {detailElems}
</g> </g>
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}> <g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
{markerElems} {markerElems}
</g> </g>
<rect <rect
fillOpacity='0' fillOpacity='0'
height={innerHeight} height={innerHeight}
width={innerWidth + 1} width={innerWidth + 1}
onMouseEnter={this._showTip} onMouseEnter={this._showTip}
onTouchStart={this._showTip} onTouchStart={this._showTip}
onMouseLeave={this._hideTip} onMouseLeave={this._hideTip}
onTouchEnd={this._hideTip} onTouchEnd={this._hideTip}
onMouseMove={e => this._moveTip(e, width)} onMouseMove={e => this._moveTip(e, width)}
onTouchMove={e => this._moveTip(e, width)} onTouchMove={e => this._moveTip(e, width)}
/> />
</g> </g>
</svg> </svg>
</div> </div>
); );
}} }}
</ContainerDimensions> </ContainerDimensions>
); );
} }
} }

View File

@@ -1,92 +1,92 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ContainerDimensions from 'react-container-dimensions'; import ContainerDimensions from 'react-container-dimensions';
import * as d3 from 'd3'; import * as d3 from 'd3';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
const LABEL_COLOUR = '#000000'; const LABEL_COLOUR = '#000000';
/** /**
* A pie chart * A pie chart
*/ */
export default class PieChart extends Component { export default class PieChart extends Component {
static propTypes = { static propTypes = {
data : PropTypes.array.isRequired data : PropTypes.array.isRequired
}; };
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {Object} context React Component context * @param {Object} context React Component context
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.pie = d3.pie().value((d) => d.value); this.pie = d3.pie().value((d) => d.value);
this.colors = CORIOLIS_COLOURS; this.colors = CORIOLIS_COLOURS;
this.arc = d3.arc(); this.arc = d3.arc();
this.arc.innerRadius(0); this.arc.innerRadius(0);
} }
/** /**
* Generate a slice of the pie chart * Generate a slice of the pie chart
* @param {Object} d the data for this slice * @param {Object} d the data for this slice
* @param {number} i the index of this slice * @param {number} i the index of this slice
* @param {number} width the current width of the parent container * @param {number} width the current width of the parent container
* @returns {Object} the SVG for the slice * @returns {Object} the SVG for the slice
*/ */
sliceGenerator(d, i, width) { sliceGenerator(d, i, width) {
if (!d || d.value == 0) { if (!d || d.value == 0) {
// Ignore 0 values // Ignore 0 values
return null; return null;
} }
const { data } = this.props; const { data } = this.props;
// Push the labels further out from the centre of the slice // Push the labels further out from the centre of the slice
let [labelX, labelY] = this.arc.centroid(d); let [labelX, labelY] = this.arc.centroid(d);
const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`; const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`;
// Put the keys in a line with equal spacing // Put the keys in a line with equal spacing
const nonZeroItems = data.filter(d => d.value != 0).length; const nonZeroItems = data.filter(d => d.value != 0).length;
const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1; const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1;
const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5); const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5);
const keyTranslate = `translate(${keyX}, ${width * 0.45})`; const keyTranslate = `translate(${keyX}, ${width * 0.45})`;
return ( return (
<g key={`group-${i}`}> <g key={`group-${i}`}>
<path key={`arc-${i}`} d={this.arc(d)} style={{ fill: this.colors[i] }} /> <path key={`arc-${i}`} d={this.arc(d)} style={{ fill: this.colors[i] }} />
<text key={`label-${i}`} transform={labelTranslate} style={{ strokeWidth: '0px', fill: LABEL_COLOUR }} textAnchor='middle'>{d.value}</text> <text key={`label-${i}`} transform={labelTranslate} style={{ strokeWidth: '0px', fill: LABEL_COLOUR }} textAnchor='middle'>{d.value}</text>
<text key={`key-${i}`} transform={keyTranslate} style={{ strokeWidth:'0px', fill: this.colors[i] }} textAnchor='middle'>{d.data.label}</text> <text key={`key-${i}`} transform={keyTranslate} style={{ strokeWidth:'0px', fill: this.colors[i] }} textAnchor='middle'>{d.data.label}</text>
</g> </g>
); );
} }
/** /**
* Render the component * Render the component
* @returns {object} Markup * @returns {object} Markup
*/ */
render() { render() {
return ( return (
<ContainerDimensions> <ContainerDimensions>
{ ({ width }) => { { ({ width }) => {
const pie = this.pie(this.props.data), const pie = this.pie(this.props.data),
translate = `translate(${width / 2}, ${width * 0.4})`; translate = `translate(${width / 2}, ${width * 0.4})`;
this.arc.outerRadius(width * 0.4); this.arc.outerRadius(width * 0.4);
return ( return (
<div width={width} height={width}> <div width={width} height={width}>
<svg style={{ stroke: 'None' }} width={width} height={width * 0.9}> <svg style={{ stroke: 'None' }} width={width} height={width * 0.9}>
<g transform={translate}> <g transform={translate}>
{pie.map((d, i) => this.sliceGenerator(d, i, width))} {pie.map((d, i) => this.sliceGenerator(d, i, width))}
</g> </g>
</svg> </svg>
</div> </div>
); );
}} }}
</ContainerDimensions> </ContainerDimensions>
); );
} }
} }

View File

@@ -1,386 +1,386 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
const MARGIN_LR = 8; // Left/ Right margin const MARGIN_LR = 8; // Left/ Right margin
/** /**
* Horizontal Slider * Horizontal Slider
*/ */
export default class Slider extends React.Component { export default class Slider extends React.Component {
static defaultProps = { static defaultProps = {
axis: false, axis: false,
min: 0, min: 0,
max: 1, max: 1,
scale: 1 // SVG render scale scale: 1 // SVG render scale
}; };
static propTypes = { static propTypes = {
axis: PropTypes.bool, axis: PropTypes.bool,
axisUnit: PropTypes.string,// units (T, M, etc.) axisUnit: PropTypes.string,// units (T, M, etc.)
max: PropTypes.number, max: PropTypes.number,
min: PropTypes.number, min: PropTypes.number,
onChange: PropTypes.func.isRequired,// function which determins percent value onChange: PropTypes.func.isRequired,// function which determins percent value
onResize: PropTypes.func, onResize: PropTypes.func,
percent: PropTypes.number.isRequired,// value of slider percent: PropTypes.number.isRequired,// value of slider
scale: PropTypes.number scale: PropTypes.number
}; };
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this._down = this._down.bind(this); this._down = this._down.bind(this);
this._move = this._move.bind(this); this._move = this._move.bind(this);
this._up = this._up.bind(this); this._up = this._up.bind(this);
this._keyup = this._keyup.bind(this); this._keyup = this._keyup.bind(this);
this._keydown = this._keydown.bind(this); this._keydown = this._keydown.bind(this);
this._touchstart = this._touchstart.bind(this); this._touchstart = this._touchstart.bind(this);
this._touchend = this._touchend.bind(this); this._touchend = this._touchend.bind(this);
this._updatePercent = this._updatePercent.bind(this); this._updatePercent = this._updatePercent.bind(this);
this._updateDimensions = this._updateDimensions.bind(this); this._updateDimensions = this._updateDimensions.bind(this);
this.state = { width: 0 }; this.state = { width: 0 };
} }
/** /**
* On Mouse/Touch down handler * On Mouse/Touch down handler
* @param {SyntheticEvent} event Event * @param {SyntheticEvent} event Event
*/ */
_down(event) { _down(event) {
let rect = event.currentTarget.getBoundingClientRect(); let rect = event.currentTarget.getBoundingClientRect();
this.left = rect.left; this.left = rect.left;
this.width = rect.width; this.width = rect.width;
this._move(event); this._move(event);
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
} }
/** /**
* Update the slider percentage on move * Update the slider percentage on move
* @param {SyntheticEvent} event Event * @param {SyntheticEvent} event Event
*/ */
_move(event) { _move(event) {
if(this.width !== null && this.left != null) { if(this.width !== null && this.left != null) {
let clientX = event.touches ? event.touches[0].clientX : event.clientX; let clientX = event.touches ? event.touches[0].clientX : event.clientX;
event.preventDefault(); event.preventDefault();
this._updatePercent(clientX - this.left, this.width); this._updatePercent(clientX - this.left, this.width);
} }
} }
/** /**
* On Mouse/Touch up handler * On Mouse/Touch up handler
* @param {Event} event DOM Event * @param {Event} event DOM Event
*/ */
_up(event) { _up(event) {
this.sliderInputBox.sliderVal.focus(); this.sliderInputBox.sliderVal.focus();
clearTimeout(this.touchStartTimer); clearTimeout(this.touchStartTimer);
event.preventDefault(); event.preventDefault();
this.left = null; this.left = null;
this.width = null; this.width = null;
} }
/** /**
* Key up handler for keyboard. * Key up handler for keyboard.
* display the number field then set focus to it * display the number field then set focus to it
* when "Enter" key is pressed * when "Enter" key is pressed
* @param {Event} event Keyboard event * @param {Event} event Keyboard event
*/ */
_keyup(event) { _keyup(event) {
switch (event.key) { switch (event.key) {
case 'Enter': case 'Enter':
event.preventDefault(); event.preventDefault();
this.sliderInputBox._setDisplay('block'); this.sliderInputBox._setDisplay('block');
return; return;
default: default:
return; return;
} }
} }
/** /**
* Key down handler * Key down handler
* increment slider position by +/- 1 when right/left arrow key is pressed or held * increment slider position by +/- 1 when right/left arrow key is pressed or held
* @param {Event} event Keyboard even * @param {Event} event Keyboard even
*/ */
_keydown(event) { _keydown(event) {
let newVal = this.props.percent * this.props.max; let newVal = this.props.percent * this.props.max;
switch (event.key) { switch (event.key) {
case 'ArrowRight': case 'ArrowRight':
newVal += 1; newVal += 1;
if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max); if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max);
return; return;
case 'ArrowLeft': case 'ArrowLeft':
newVal -= 1; newVal -= 1;
if (newVal >= 0) this.props.onChange(newVal / this.props.max); if (newVal >= 0) this.props.onChange(newVal / this.props.max);
return; return;
default: default:
return; return;
} }
} }
/** /**
* Touch start handler * Touch start handler
* @param {Event} event DOM Event * @param {Event} event DOM Event
* *
*/ */
_touchstart(event) { _touchstart(event) {
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
} }
/** /**
* Touch end handler * Touch end handler
* @param {Event} event DOM Event * @param {Event} event DOM Event
* *
*/ */
_touchend(event) { _touchend(event) {
this.sliderInputBox.sliderVal.focus(); this.sliderInputBox.sliderVal.focus();
clearTimeout(this.touchStartTimer); clearTimeout(this.touchStartTimer);
} }
/** /**
* Determine if the user is still dragging * Determine if the user is still dragging
* @param {SyntheticEvent} event Event * @param {SyntheticEvent} event Event
*/ */
_enter(event) { _enter(event) {
if(event.buttons !== 1) { if(event.buttons !== 1) {
this.left = null; this.left = null;
this.width = null; this.width = null;
} }
} }
/** /**
* Update the slider percentage * Update the slider percentage
* @param {number} pos Slider drag position * @param {number} pos Slider drag position
* @param {number} width Slider width * @param {number} width Slider width
* @param {Event} event DOM Event * @param {Event} event DOM Event
*/ */
_updatePercent(pos, width) { _updatePercent(pos, width) {
this.props.onChange(Math.min(Math.max(pos / width, 0), 1)); this.props.onChange(Math.min(Math.max(pos / width, 0), 1));
} }
/** /**
* Update dimenions from rendered DOM * Update dimenions from rendered DOM
*/ */
_updateDimensions() { _updateDimensions() {
this.setState({ this.setState({
outerWidth: this.node.getBoundingClientRect().width outerWidth: this.node.getBoundingClientRect().width
}); });
} }
/** /**
* Add listeners when about to mount * Add listeners when about to mount
*/ */
componentWillMount() { componentWillMount() {
if (this.props.onResize) { if (this.props.onResize) {
this.resizeListener = this.props.onResize(this._updateDimensions); this.resizeListener = this.props.onResize(this._updateDimensions);
} }
} }
/** /**
* Trigger DOM updates on mount * Trigger DOM updates on mount
*/ */
componentDidMount() { componentDidMount() {
this._updateDimensions(); this._updateDimensions();
} }
/** /**
* Remove listeners on unmount * Remove listeners on unmount
*/ */
componentWillUnmount() { componentWillUnmount() {
if (this.resizeListener) { if (this.resizeListener) {
this.resizeListener.remove(); this.resizeListener.remove();
} }
} }
/** /**
* Render the slider * Render the slider
* @return {React.Component} The slider * @return {React.Component} The slider
*/ */
render() { render() {
let outerWidth = this.state.outerWidth; let outerWidth = this.state.outerWidth;
let { axis, axisUnit, min, max, scale } = this.props; let { axis, axisUnit, min, max, scale } = this.props;
let style = { let style = {
width: '100%', width: '100%',
height: axis ? '2.5em' : '1.5em', height: axis ? '2.5em' : '1.5em',
boxSizing: 'border-box' boxSizing: 'border-box'
}; };
if (!outerWidth) { if (!outerWidth) {
return <svg style={style} ref={node => this.node = node} />; return <svg style={style} ref={node => this.node = node} />;
} }
let margin = MARGIN_LR * scale; let margin = MARGIN_LR * scale;
let width = outerWidth - (margin * 2); let width = outerWidth - (margin * 2);
let pctPos = width * this.props.percent; let pctPos = width * this.props.percent;
return <div><svg return <div><svg
onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onKeyUp={this._keyup} onKeyDown={this._keydown} style={style} ref={node => this.node = node} tabIndex="0"> onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onKeyUp={this._keyup} onKeyDown={this._keydown} style={style} ref={node => this.node = node} tabIndex="0">
<rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' /> <rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
<rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' /> <rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
<circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} /> <circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} />
<rect x={margin} width={width} height='100%' fillOpacity='0' style={{ cursor: 'col-resize' }} onMouseDown={this._down} onTouchMove={this._move} onTouchStart={this._down} onTouchEnd={this._touchend} /> <rect x={margin} width={width} height='100%' fillOpacity='0' style={{ cursor: 'col-resize' }} onMouseDown={this._down} onTouchMove={this._move} onTouchStart={this._down} onTouchEnd={this._touchend} />
{axis && <g style={{ fontSize: '.7em' }}> {axis && <g style={{ fontSize: '.7em' }}>
<text className='primary-disabled' y='3em' x={margin} style={{ textAnchor: 'middle' }}>{min + axisUnit}</text> <text className='primary-disabled' y='3em' x={margin} style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
<text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text> <text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text> <text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
</g>} </g>}
</svg> </svg>
<TextInputBox ref={(tb) => this.sliderInputBox = tb} <TextInputBox ref={(tb) => this.sliderInputBox = tb}
onChange={this.props.onChange} onChange={this.props.onChange}
percent={this.props.percent} percent={this.props.percent}
axisUnit={this.props.axisUnit} axisUnit={this.props.axisUnit}
scale={this.props.scale} scale={this.props.scale}
max={this.props.max} max={this.props.max}
/> />
</div>; </div>;
} }
} }
/** /**
* New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android) * New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android)
**/ **/
class TextInputBox extends React.Component { class TextInputBox extends React.Component {
static propTypes = { static propTypes = {
axisUnit: PropTypes.string,// units (T, M, etc.) axisUnit: PropTypes.string,// units (T, M, etc.)
max: PropTypes.number, max: PropTypes.number,
onChange: PropTypes.func.isRequired,// function which determins percent value onChange: PropTypes.func.isRequired,// function which determins percent value
percent: PropTypes.number.isRequired,// value of slider percent: PropTypes.number.isRequired,// value of slider
scale: PropTypes.number scale: PropTypes.number
}; };
/** /**
* Determine if the user is still dragging * Determine if the user is still dragging
* @param {Object} props React Component properties * @param {Object} props React Component properties
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this._handleFocus = this._handleFocus.bind(this); this._handleFocus = this._handleFocus.bind(this);
this._handleBlur = this._handleBlur.bind(this); this._handleBlur = this._handleBlur.bind(this);
this._handleChange = this._handleChange.bind(this); this._handleChange = this._handleChange.bind(this);
this._keyup = this._keyup.bind(this); this._keyup = this._keyup.bind(this);
this.state = this._getInitialState(); this.state = this._getInitialState();
} }
/** /**
* Update input value if slider changes will change props/state * Update input value if slider changes will change props/state
* @param {Object} nextProps React Component properites * @param {Object} nextProps React Component properites
* @param {Object} nextState React Component state values * @param {Object} nextState React Component state values
*/ */
componentWillReceiveProps(nextProps, nextState) { componentWillReceiveProps(nextProps, nextState) {
let nextValue = nextProps.percent * nextProps.max; let nextValue = nextProps.percent * nextProps.max;
// See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form // See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form
if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) { if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) {
this.setState({ inputValue: nextValue }); this.setState({ inputValue: nextValue });
} }
} }
/** /**
* Update slider textbox visibility/values if changes are made to slider * Update slider textbox visibility/values if changes are made to slider
* @param {Object} prevProps React Component properites * @param {Object} prevProps React Component properites
* @param {Object} prevState React Component state values * @param {Object} prevState React Component state values
*/ */
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') { if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') {
this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10); this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10);
} }
if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) { if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) {
// they chose a different module // they chose a different module
this.setState({ inputValue: this.props.max }); this.setState({ inputValue: this.props.max });
} }
if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) { if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) {
this.props.onChange(this.state.inputValue / this.props.max); this.props.onChange(this.state.inputValue / this.props.max);
} }
} }
/** /**
* Set initial state for the textbox. * Set initial state for the textbox.
* We may want to rethink this to * We may want to rethink this to
* try and make it a stateless component * try and make it a stateless component
* @returns {object} React state object with initial values set * @returns {object} React state object with initial values set
*/ */
_getInitialState() { _getInitialState() {
return { return {
divStyle: { display:'none' }, divStyle: { display:'none' },
inputStyle: { width:'4em' }, inputStyle: { width:'4em' },
labelStyle: { marginLeft: '.1em' }, labelStyle: { marginLeft: '.1em' },
maxLength:5, maxLength:5,
size:5, size:5,
min:0, min:0,
tabIndex:-1, tabIndex:-1,
type:'number', type:'number',
readOnly: true, readOnly: true,
inputValue: this.props.percent * this.props.max inputValue: this.props.percent * this.props.max
}; };
} }
/** /**
* *
* @param {string} val block or none * @param {string} val block or none
*/ */
_setDisplay(val) { _setDisplay(val) {
this.setState({ this.setState({
divStyle: { display:val } divStyle: { display:val }
}); });
} }
/** /**
* Update the input value * Update the input value
* when textbox gets focus * when textbox gets focus
*/ */
_handleFocus() { _handleFocus() {
this.setState({ this.setState({
inputValue:this._getValue() inputValue:this._getValue()
}); });
} }
/** /**
* Update inputValue when textbox loses focus * Update inputValue when textbox loses focus
*/ */
_handleBlur() { _handleBlur() {
this._setDisplay('none'); this._setDisplay('none');
if (this.state.inputValue !== '') { if (this.state.inputValue !== '') {
this.props.onChange(this.state.inputValue / this.props.max); this.props.onChange(this.state.inputValue / this.props.max);
} else { } else {
this.setState({ this.setState({
inputValue: this.props.percent * this.props.max inputValue: this.props.percent * this.props.max
}); });
} }
} }
/** /**
* Get the value in the text box * Get the value in the text box
* @returns {number} inputValue Value of the input box * @returns {number} inputValue Value of the input box
*/ */
_getValue() { _getValue() {
return this.state.inputValue; return this.state.inputValue;
} }
/** /**
* Update and set limits on input box * Update and set limits on input box
* values depending on what user * values depending on what user
* has selected * has selected
* *
* @param {SyntheticEvent} event ReactJs onChange event * @param {SyntheticEvent} event ReactJs onChange event
*/ */
_handleChange(event) { _handleChange(event) {
if (event.target.value < 0) { if (event.target.value < 0) {
this.setState({ inputValue: 0 }); this.setState({ inputValue: 0 });
} else if (event.target.value <= this.props.max) { } else if (event.target.value <= this.props.max) {
this.setState({ inputValue: event.target.value }); this.setState({ inputValue: event.target.value });
} else { } else {
this.setState({ inputValue: this.props.max }); this.setState({ inputValue: this.props.max });
} }
} }
/** /**
* Key up handler for input field. * Key up handler for input field.
* If user hits Enter key, blur/close the input field * If user hits Enter key, blur/close the input field
* @param {Event} event Keyboard event * @param {Event} event Keyboard event
*/ */
_keyup(event) { _keyup(event) {
switch (event.key) { switch (event.key) {
case 'Enter': case 'Enter':
this.sliderVal.blur(); this.sliderVal.blur();
return; return;
default: default:
return; return;
} }
} }
/** /**
* Get the value in the text box * Get the value in the text box
* @return {React.Component} Text Input component for Slider * @return {React.Component} Text Input component for Slider
*/ */
render() { render() {
let { axisUnit, onChange, percent, scale } = this.props; let { axisUnit, onChange, percent, scale } = this.props;
return <div style={this.state.divStyle}><input style={this.state.inputStyle} value={this._getValue()} min={this.state.min} max={this.props.max} onChange={this._handleChange} onKeyUp={this._keyup} tabIndex={this.state.tabIndex} maxLength={this.state.maxLength} size={this.state.size} onBlur={() => {this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/><text className="primary upp" style={this.state.labelStyle}>{this.props.axisUnit}</text></div>; return <div style={this.state.divStyle}><input style={this.state.inputStyle} value={this._getValue()} min={this.state.min} max={this.props.max} onChange={this._handleChange} onKeyUp={this._keyup} tabIndex={this.state.tabIndex} maxLength={this.state.maxLength} size={this.state.size} onBlur={() => {this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/><text className="primary upp" style={this.state.labelStyle}>{this.props.axisUnit}</text></div>;
} }
} }

View File

@@ -1,80 +1,80 @@
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ContainerDimensions from 'react-container-dimensions'; import ContainerDimensions from 'react-container-dimensions';
import { BarChart, Bar, XAxis, YAxis, LabelList } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, LabelList } from 'recharts';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
const LABEL_COLOUR = '#000000'; const LABEL_COLOUR = '#000000';
const AXIS_COLOUR = '#C06400'; const AXIS_COLOUR = '#C06400';
const ASPECT = 1; const ASPECT = 1;
const merge = function(one, two) { const merge = function(one, two) {
return Object.assign({}, one, two); return Object.assign({}, one, two);
}; };
/** /**
* A vertical bar chart * A vertical bar chart
*/ */
export default class VerticalBarChart extends TranslatedComponent { export default class VerticalBarChart extends TranslatedComponent {
static propTypes = { static propTypes = {
data : PropTypes.array.isRequired, data : PropTypes.array.isRequired,
yMax : PropTypes.number yMax : PropTypes.number
}; };
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {Object} context React Component context * @param {Object} context React Component context
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this._termtip = this._termtip.bind(this); this._termtip = this._termtip.bind(this);
} }
/** /**
* Render the bar chart * Render the bar chart
* @returns {Object} the markup * @returns {Object} the markup
*/ */
render() { render() {
const { tooltip, termtip } = this.context; const { tooltip, termtip } = this.context;
// Calculate maximum for Y // Calculate maximum for Y
let dataMax = Math.max(...this.props.data.map(d => d.value)); let dataMax = Math.max(...this.props.data.map(d => d.value));
if (dataMax == -Infinity) dataMax = 0; if (dataMax == -Infinity) dataMax = 0;
let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0; let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0;
const localMax = Math.max(dataMax, yMax); const localMax = Math.max(dataMax, yMax);
return ( return (
<ContainerDimensions> <ContainerDimensions>
{ ({ width }) => ( { ({ width }) => (
<div width='100%'> <div width='100%'>
<BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}> <BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' /> <XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' />
<YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/> <YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/>
<Bar dataKey='value' fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}> <Bar dataKey='value' fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}>
<LabelList dataKey='value' position='insideTop'/> <LabelList dataKey='value' position='insideTop'/>
</Bar> </Bar>
</BarChart> </BarChart>
</div> </div>
)} )}
</ContainerDimensions> </ContainerDimensions>
); );
} }
/** /**
* Generate a term tip * Generate a term tip
* @param {Object} d the data * @param {Object} d the data
* @param {number} i the index * @param {number} i the index
* @param {Object} e the event * @param {Object} e the event
* @returns {Object} termtip markup * @returns {Object} termtip markup
*/ */
_termtip(d, i, e) { _termtip(d, i, e) {
if (this.props.data[i].tooltip) { if (this.props.data[i].tooltip) {
return this.context.termtip(this.props.data[i].tooltip, e); return this.context.termtip(this.props.data[i].tooltip, e);
} else { } else {
return null; return null;
} }
} }
} }

View File

@@ -1,16 +1,16 @@
export const formats = { export const formats = {
decimal: ',', decimal: ',',
thousands: '.', thousands: '.',
grouping: [3], grouping: [3],
currency: ['', ' €'], currency: ['', ' €'],
dateTime: '%A, der %e. %B %Y, %X', dateTime: '%A, der %e. %B %Y, %X',
date: '%d.%m.%Y', date: '%d.%m.%Y',
time: '%H:%M:%S', time: '%H:%M:%S',
periods: ['AM', 'PM'], // unused periods: ['AM', 'PM'], // unused
days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
shortDays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], shortDays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
shortMonths: ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'] shortMonths: ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
}; };
export { default as terms } from './de.json'; export { default as terms } from './de.json';

View File

@@ -1,16 +1,16 @@
export const formats = { export const formats = {
decimal: '.', decimal: '.',
thousands: ',', thousands: ',',
grouping: [3], grouping: [3],
currency: ['₩', ''], currency: ['₩', ''],
dateTime: '%a %b %e %X %Y', dateTime: '%a %b %e %X %Y',
date: '%Y/%m/%d', date: '%Y/%m/%d',
time: '%H:%M:%S', time: '%H:%M:%S',
periods: ['오전', '오후'], periods: ['오전', '오후'],
days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'], days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
shortDays: ['일', '월', '화', '수', '목', '금', '토'], shortDays: ['일', '월', '화', '수', '목', '금', '토'],
months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
shortMonths: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'] shortMonths: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']
}; };
export { default as terms } from './ko.json'; export { default as terms } from './ko.json';

View File

@@ -1,435 +1,435 @@
import React from 'react'; import React from 'react';
import { Modifications } from 'coriolis-data/dist'; import { Modifications } from 'coriolis-data/dist';
import { STATS_FORMATTING } from '../shipyard/StatsFormatting'; import { STATS_FORMATTING } from '../shipyard/StatsFormatting';
/** /**
* Generate a tooltip with details of a blueprint's specials * Generate a tooltip with details of a blueprint's specials
* @param {Object} translate The translate object * @param {Object} translate The translate object
* @param {Object} blueprint The blueprint at the required grade * @param {Object} blueprint The blueprint at the required grade
* @param {string} grp The group of the module * @param {string} grp The group of the module
* @param {Object} m The module to compare with * @param {Object} m The module to compare with
* @param {string} specialName The name of the special * @param {string} specialName The name of the special
* @returns {Object} The react components * @returns {Object} The react components
*/ */
export function specialToolTip(translate, blueprint, grp, m, specialName) { export function specialToolTip(translate, blueprint, grp, m, specialName) {
const effects = []; const effects = [];
if (!blueprint || !blueprint.features) { if (!blueprint || !blueprint.features) {
return undefined; return undefined;
} }
if (m) { if (m) {
// We also add in any benefits from specials that aren't covered above // We also add in any benefits from specials that aren't covered above
if (m.blueprint) { if (m.blueprint) {
for (const feature in Modifications.modifierActions[specialName]) { for (const feature in Modifications.modifierActions[specialName]) {
// if (!blueprint.features[feature] && !m.mods.feature) { // if (!blueprint.features[feature] && !m.mods.feature) {
const featureDef = Modifications.modifications[feature]; const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) { if (featureDef && !featureDef.hidden) {
let symbol = ''; let symbol = '';
if (feature === 'jitter') { if (feature === 'jitter') {
symbol = '°'; symbol = '°';
} else if (featureDef.type === 'percentage') { } else if (featureDef.type === 'percentage') {
symbol = '%'; symbol = '%';
} }
let current = m.getModValue(feature) - m.getModValue(feature, true); let current = m.getModValue(feature) - m.getModValue(feature, true);
if (featureDef.type === 'percentage') { if (featureDef.type === 'percentage') {
current = Math.round(current / 10) / 10; current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') { } else if (featureDef.type === 'numeric') {
current /= 100; current /= 100;
} }
const currentIsBeneficial = isValueBeneficial(feature, current); const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push( effects.push(
<tr key={feature + '_specialTT'}> <tr key={feature + '_specialTT'}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td> <td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td> <td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} <td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'}
style={{ textAlign: 'right' }}>{current}{symbol}</td> style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
); );
} }
} }
} }
} }
return ( return (
<div> <div>
<table width='100%'> <table width='100%'>
<tbody> <tbody>
{effects} {effects}
</tbody> </tbody>
</table> </table>
</div> </div>
); );
} }
/** /**
* Generate a tooltip with details of a blueprint's effects * Generate a tooltip with details of a blueprint's effects
* @param {Object} translate The translate object * @param {Object} translate The translate object
* @param {Object} blueprint The blueprint at the required grade * @param {Object} blueprint The blueprint at the required grade
* @param {Array} engineers The engineers supplying this blueprint * @param {Array} engineers The engineers supplying this blueprint
* @param {string} grp The group of the module * @param {string} grp The group of the module
* @param {Object} m The module to compare with * @param {Object} m The module to compare with
* @returns {Object} The react components * @returns {Object} The react components
*/ */
export function blueprintTooltip(translate, blueprint, engineers, grp, m) { export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
const effects = []; const effects = [];
if (!blueprint || !blueprint.features) { if (!blueprint || !blueprint.features) {
return undefined; return undefined;
} }
for (const feature in blueprint.features) { for (const feature in blueprint.features) {
const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]);
const featureDef = Modifications.modifications[feature]; const featureDef = Modifications.modifications[feature];
if (!featureDef.hidden) { if (!featureDef.hidden) {
let symbol = ''; let symbol = '';
if (feature === 'jitter') { if (feature === 'jitter') {
symbol = '°'; symbol = '°';
} else if (featureDef.type === 'percentage') { } else if (featureDef.type === 'percentage') {
symbol = '%'; symbol = '%';
} }
let lowerBound = blueprint.features[feature][0]; let lowerBound = blueprint.features[feature][0];
let upperBound = blueprint.features[feature][1]; let upperBound = blueprint.features[feature][1];
if (featureDef.type === 'percentage') { if (featureDef.type === 'percentage') {
lowerBound = Math.round(lowerBound * 1000) / 10; lowerBound = Math.round(lowerBound * 1000) / 10;
upperBound = Math.round(upperBound * 1000) / 10; upperBound = Math.round(upperBound * 1000) / 10;
} }
const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); const lowerIsBeneficial = isValueBeneficial(feature, lowerBound);
const upperIsBeneficial = isValueBeneficial(feature, upperBound); const upperIsBeneficial = isValueBeneficial(feature, upperBound);
if (m) { if (m) {
// We have a module - add in the current value // We have a module - add in the current value
let current = m.getModValue(feature); let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10; current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') { } else if (featureDef.type === 'numeric') {
current /= 100; current /= 100;
} }
const currentIsBeneficial = isValueBeneficial(feature, current); const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push( effects.push(
<tr key={feature}> <tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td> <td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td> <td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td> <td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td> <td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr> </tr>
); );
} else { } else {
// We do not have a module, no value // We do not have a module, no value
effects.push( effects.push(
<tr key={feature}> <tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td> <td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td> <td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td> <td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr> </tr>
); );
} }
} }
} }
if (m) { if (m) {
// Because we have a module add in any benefits that aren't part of the primary blueprint // Because we have a module add in any benefits that aren't part of the primary blueprint
for (const feature in m.mods) { for (const feature in m.mods) {
if (!blueprint.features[feature]) { if (!blueprint.features[feature]) {
const featureDef = Modifications.modifications[feature]; const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) { if (featureDef && !featureDef.hidden) {
let symbol = ''; let symbol = '';
if (feature === 'jitter') { if (feature === 'jitter') {
symbol = '°'; symbol = '°';
} else if (featureDef.type === 'percentage') { } else if (featureDef.type === 'percentage') {
symbol = '%'; symbol = '%';
} }
let current = m.getModValue(feature); let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10; current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') { } else if (featureDef.type === 'numeric') {
current /= 100; current /= 100;
} }
const currentIsBeneficial = isValueBeneficial(feature, current); const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push( effects.push(
<tr key={feature}> <tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td> <td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td> <td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td> <td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
); );
} }
} }
} }
// We also add in any benefits from specials that aren't covered above // We also add in any benefits from specials that aren't covered above
if (m.blueprint && m.blueprint.special) { if (m.blueprint && m.blueprint.special) {
for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) { for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) {
if (!blueprint.features[feature] && !m.mods.feature) { if (!blueprint.features[feature] && !m.mods.feature) {
const featureDef = Modifications.modifications[feature]; const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) { if (featureDef && !featureDef.hidden) {
let symbol = ''; let symbol = '';
if (feature === 'jitter') { if (feature === 'jitter') {
symbol = '°'; symbol = '°';
} else if (featureDef.type === 'percentage') { } else if (featureDef.type === 'percentage') {
symbol = '%'; symbol = '%';
} }
let current = m.getModValue(feature); let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10; current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') { } else if (featureDef.type === 'numeric') {
current /= 100; current /= 100;
} }
const currentIsBeneficial = isValueBeneficial(feature, current); const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push( effects.push(
<tr key={feature}> <tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td> <td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td> <td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td> <td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
); );
} }
} }
} }
} }
} }
let components; let components;
if (!m) { if (!m) {
components = []; components = [];
for (const component in blueprint.components) { for (const component in blueprint.components) {
components.push( components.push(
<tr key={component}> <tr key={component}>
<td style={{ textAlign: 'left' }}>{translate(component)}</td> <td style={{ textAlign: 'left' }}>{translate(component)}</td>
<td style={{ textAlign: 'right' }}>{blueprint.components[component]}</td> <td style={{ textAlign: 'right' }}>{blueprint.components[component]}</td>
</tr> </tr>
); );
} }
} }
let engineersList; let engineersList;
if (engineers) { if (engineers) {
engineersList = []; engineersList = [];
for (const engineer of engineers) { for (const engineer of engineers) {
engineersList.push( engineersList.push(
<tr key={engineer}> <tr key={engineer}>
<td style={{ textAlign: 'left' }}>{engineer}</td> <td style={{ textAlign: 'left' }}>{engineer}</td>
</tr> </tr>
); );
} }
} }
return ( return (
<div> <div>
<table width='100%'> <table width='100%'>
<thead> <thead>
<tr> <tr>
<td>{translate('feature')}</td> <td>{translate('feature')}</td>
<td>{translate('worst')}</td> <td>{translate('worst')}</td>
{m ? <td>{translate('current')}</td> : null } {m ? <td>{translate('current')}</td> : null }
<td>{translate('best')}</td> <td>{translate('best')}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{effects} {effects}
</tbody> </tbody>
</table> </table>
{ components ? <table width='100%'> { components ? <table width='100%'>
<thead> <thead>
<tr> <tr>
<td>{translate('component')}</td> <td>{translate('component')}</td>
<td>{translate('amount')}</td> <td>{translate('amount')}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{components} {components}
</tbody> </tbody>
</table> : null } </table> : null }
{ engineersList ? <table width='100%'> { engineersList ? <table width='100%'>
<thead> <thead>
<tr> <tr>
<td>{translate('engineers')}</td> <td>{translate('engineers')}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{engineersList} {engineersList}
</tbody> </tbody>
</table> : null } </table> : null }
</div> </div>
); );
} }
/** /**
* Is this blueprint feature beneficial? * Is this blueprint feature beneficial?
* @param {string} feature The name of the feature * @param {string} feature The name of the feature
* @param {array} values The value of the feature * @param {array} values The value of the feature
* @returns {boolean} True if this feature is beneficial * @returns {boolean} True if this feature is beneficial
*/ */
export function isBeneficial(feature, values) { export function isBeneficial(feature, values) {
const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0));
if (Modifications.modifications[feature].higherbetter) { if (Modifications.modifications[feature].higherbetter) {
return !fact; return !fact;
} else { } else {
return fact; return fact;
} }
} }
/** /**
* Is this feature value beneficial? * Is this feature value beneficial?
* @param {string} feature The name of the feature * @param {string} feature The name of the feature
* @param {number} value The value of the feature * @param {number} value The value of the feature
* @returns {boolean} True if this value is beneficial * @returns {boolean} True if this value is beneficial
*/ */
export function isValueBeneficial(feature, value) { export function isValueBeneficial(feature, value) {
if (Modifications.modifications[feature].higherbetter) { if (Modifications.modifications[feature].higherbetter) {
return value > 0; return value > 0;
} else { } else {
return value < 0; return value < 0;
} }
} }
/** /**
* Is the change as shown beneficial? * Is the change as shown beneficial?
* @param {string} feature The name of the feature * @param {string} feature The name of the feature
* @param {number} value The value of the feature as percentage change * @param {number} value The value of the feature as percentage change
* @returns True if the value is beneficial * @returns True if the value is beneficial
*/ */
export function isChangeValueBeneficial(feature, value) { export function isChangeValueBeneficial(feature, value) {
let changeHigherBetter = STATS_FORMATTING[feature].higherbetter; let changeHigherBetter = STATS_FORMATTING[feature].higherbetter;
if (changeHigherBetter === undefined) { if (changeHigherBetter === undefined) {
return isValueBeneficial(feature, value); return isValueBeneficial(feature, value);
} }
if (changeHigherBetter) { if (changeHigherBetter) {
return value > 0; return value > 0;
} else { } else {
return value < 0; return value < 0;
} }
} }
/** /**
* Get a blueprint with a given name and an optional module * Get a blueprint with a given name and an optional module
* @param {string} name The name of the blueprint * @param {string} name The name of the blueprint
* @param {Object} module The module for which to obtain this blueprint * @param {Object} module The module for which to obtain this blueprint
* @returns {Object} The matching blueprint * @returns {Object} The matching blueprint
*/ */
export function getBlueprint(name, module) { export function getBlueprint(name, module) {
// Start with a copy of the blueprint // Start with a copy of the blueprint
const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0); const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0);
const found = Modifications.blueprints[findMod(name)]; const found = Modifications.blueprints[findMod(name)];
if (!found || !found.fdname) { if (!found || !found.fdname) {
return {}; return {};
} }
const blueprint = JSON.parse(JSON.stringify(found)); const blueprint = JSON.parse(JSON.stringify(found));
return blueprint; return blueprint;
} }
/** /**
* Provide 'percent' primary modifications * Provide 'percent' primary modifications
* @param {Object} ship The ship for which to perform the modifications * @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications * @param {Object} m The module for which to perform the modifications
* @param {Number} percent The percent to set values to of full. * @param {Number} percent The percent to set values to of full.
*/ */
export function setPercent(ship, m, percent) { export function setPercent(ship, m, percent) {
ship.clearModifications(m); ship.clearModifications(m);
// Pick given value as multiplier // Pick given value as multiplier
const mult = percent / 100; const mult = percent / 100;
setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value)); setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value));
} }
/** /**
* Sets the blueprint quality and fires a callback for each property affected. * Sets the blueprint quality and fires a callback for each property affected.
* @param {Object} blueprint The ship for which to perform the modifications * @param {Object} blueprint The ship for which to perform the modifications
* @param {Number} quality The quality to apply - float number 0 to 1. * @param {Number} quality The quality to apply - float number 0 to 1.
* @param {Function} cb The Callback to run for each property. Function (featureName, value) * @param {Function} cb The Callback to run for each property. Function (featureName, value)
*/ */
export function setQualityCB(blueprint, quality, cb) { export function setQualityCB(blueprint, quality, cb) {
// Pick given value as multiplier // Pick given value as multiplier
const features = blueprint.grades[blueprint.grade].features; const features = blueprint.grades[blueprint.grade].features;
for (const featureName in features) { for (const featureName in features) {
let value; let value;
if (Modifications.modifications[featureName].higherbetter) { if (Modifications.modifications[featureName].higherbetter) {
// Higher is better, but is this making it better or worse? // Higher is better, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality);
} else { } else {
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality);
} }
} else { } else {
// Higher is worse, but is this making it better or worse? // Higher is worse, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality);
} else { } else {
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality);
} }
} }
if (Modifications.modifications[featureName].type == 'percentage') { if (Modifications.modifications[featureName].type == 'percentage') {
value = value * 10000; value = value * 10000;
} else if (Modifications.modifications[featureName].type == 'numeric') { } else if (Modifications.modifications[featureName].type == 'numeric') {
value = value * 100; value = value * 100;
} }
cb(featureName, value); cb(featureName, value);
} }
} }
/** /**
* Provide 'random' primary modifications * Provide 'random' primary modifications
* @param {Object} ship The ship for which to perform the modifications * @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications * @param {Object} m The module for which to perform the modifications
*/ */
export function setRandom(ship, m) { export function setRandom(ship, m) {
// Pick a single value for our randomness // Pick a single value for our randomness
setPercent(ship, m, Math.random() * 100); setPercent(ship, m, Math.random() * 100);
} }
/** /**
* Provide 'percent' primary query * Provide 'percent' primary query
* @param {Object} m The module for which to perform the query * @param {Object} m The module for which to perform the query
* @returns {Number} percent The percentage indicator of current applied values. * @returns {Number} percent The percentage indicator of current applied values.
*/ */
export function getPercent(m) { export function getPercent(m) {
let result = null; let result = null;
const features = m.blueprint.grades[m.blueprint.grade].features; const features = m.blueprint.grades[m.blueprint.grade].features;
for (const featureName in features) { for (const featureName in features) {
if (features[featureName][0] === features[featureName][1]) { if (features[featureName][0] === features[featureName][1]) {
continue; continue;
} }
let value = _getValue(m, featureName); let value = _getValue(m, featureName);
let mult; let mult;
if (Modifications.modifications[featureName].higherbetter) { if (Modifications.modifications[featureName].higherbetter) {
// Higher is better, but is this making it better or worse? // Higher is better, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
} else { } else {
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
} }
} else { } else {
// Higher is worse, but is this making it better or worse? // Higher is worse, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
} else { } else {
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
} }
} }
if (result && result != mult) { if (result && result != mult) {
return null; return null;
} else if (result != mult) { } else if (result != mult) {
result = mult; result = mult;
} }
} }
return result; return result;
} }
/** /**
* Query a feature value * Query a feature value
* @param {Object} m The module for which to perform the query * @param {Object} m The module for which to perform the query
* @param {string} featureName The feature being queried * @param {string} featureName The feature being queried
* @returns {number} The value of the modification as a % * @returns {number} The value of the modification as a %
*/ */
function _getValue(m, featureName) { function _getValue(m, featureName) {
if (Modifications.modifications[featureName].type == 'percentage') { if (Modifications.modifications[featureName].type == 'percentage') {
return m.getModValue(featureName, true) / 10000; return m.getModValue(featureName, true) / 10000;
} else if (Modifications.modifications[featureName].type == 'numeric') { } else if (Modifications.modifications[featureName].type == 'numeric') {
return m.getModValue(featureName, true) / 100; return m.getModValue(featureName, true) / 100;
} else { } else {
return m.getModValue(featureName, true); return m.getModValue(featureName, true);
} }
} }