From fde4bba7141eb25d2e9b4c3b9ad58e83f2ba7e30 Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Thu, 31 Oct 2024 21:00:05 +0000 Subject: [PATCH 1/2] Create FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..64d09ca6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [Brighter-Applications] From f22b7ff577049f116d4ae627ea99d598099aeddd Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Thu, 31 Oct 2024 21:43:29 +0000 Subject: [PATCH 2/2] Fixing old errors --- src/app/components/LineChart.jsx | 562 +++++++-------- src/app/components/PieChart.jsx | 184 ++--- src/app/components/Slider.jsx | 772 ++++++++++----------- src/app/components/VerticalBarChart.jsx | 160 ++--- src/app/i18n/de.js | 30 +- src/app/i18n/ko.js | 32 +- src/app/utils/BlueprintFunctions.js | 870 ++++++++++++------------ 7 files changed, 1305 insertions(+), 1305 deletions(-) diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index b4113ee6..76078aa6 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -1,281 +1,281 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ContainerDimensions from 'react-container-dimensions'; -import * as d3 from 'd3'; -import TranslatedComponent from './TranslatedComponent'; - -const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; - -/** - * Line Chart - */ -export default class LineChart extends TranslatedComponent { - - static defaultProps = { - code: '', - xMin: 0, - yMin: 0, - points: 20, - colors: ['#ff8c0d'], - aspect: 0.5 - }; - - static propTypes = { - func: PropTypes.func.isRequired, - xLabel: PropTypes.string.isRequired, - xMin: PropTypes.number, - xMax: PropTypes.number.isRequired, - xUnit: PropTypes.string.isRequired, - xMark: PropTypes.number, - yLabel: PropTypes.string.isRequired, - yMin: PropTypes.number, - yMax: PropTypes.number.isRequired, - yUnit: PropTypes.string, - series: PropTypes.array, - colors: PropTypes.array, - points: PropTypes.number, - aspect: PropTypes.number, - code: PropTypes.string, - }; - - /** - * Constructor - * @param {Object} props React Component properties - * @param {Object} context React Component context - */ - constructor(props, context) { - super(props); - - this._updateDimensions = this._updateDimensions.bind(this); - this._updateSeries = this._updateSeries.bind(this); - this._tooltip = this._tooltip.bind(this); - this._showTip = this._showTip.bind(this); - this._hideTip = this._hideTip.bind(this); - this._moveTip = this._moveTip.bind(this); - - const series = props.series; - - let xScale = d3.scaleLinear(); - let yScale = d3.scaleLinear(); - let xAxisScale = d3.scaleLinear(); - - this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0); - this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); - - this.state = { - xScale, - xAxisScale, - yScale, - tipHeight: 2 + (1.2 * (series ? series.length : 0.8)), - }; - } - - /** - * Update tooltip content - * @param {number} xPos x coordinate - * @param {number} width current container width - */ - _tooltip(xPos, width) { - let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; - let { xScale, yScale } = this.state; - let { formats, translate } = this.context.language; - let x0 = xScale.invert(xPos), - y0 = func(x0), - tips = this.tipContainer, - yTotal = 0, - flip = (xPos / width > 0.50), - tipWidth = 0, - tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; - - - xPos = xScale(x0); // Clamp xPos - - tips.selectAll('text.text-tip.y').text(function(d, i) { - let yVal = series ? y0[series[i]] : y0; - yTotal += yVal; - return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal); - }).append('tspan').attr('class', 'metric').text(yUnit ? ' ' + yUnit : ''); - - tips.selectAll('text').each(function() { - if (this.getBBox().width > tipWidth) { - tipWidth = Math.ceil(this.getBBox().width); - } - }); - - let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2)); - - tipWidth += 8; - 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.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'); - this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); - } - - /** - * Update dimensions based on properties and scale - * @param {Object} props React Component properties - * @param {number} scale size ratio / scale - * @param {number} width current width of the container - * @returns {Object} calculated dimensions - */ - _updateDimensions(props, scale, width) { - const { xMax, xMin, yMin, yMax } = props; - 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 - return { innerWidth, outerHeight, innerHeight }; - } - - /** - * Show tooltip - * @param {SyntheticEvent} e Event - */ - _showTip(e) { - e.preventDefault(); - this.tipContainer.style('display', null); - this.markersContainer.style('display', null); - this._moveTip(e); - } - - /** - * Move and update tooltip - * @param {SyntheticEvent} e Event - * @param {number} width current container width - */ - _moveTip(e, width) { - let clientX = e.touches ? e.touches[0].clientX : e.clientX; - this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width); - } - - /** - * Hide tooltip - * @param {SyntheticEvent} e Event - */ - _hideTip(e) { - e.preventDefault(); - this.tipContainer.style('display', 'none'); - this.markersContainer.style('display', 'none'); - } - - /** - * Update series generated from props - * @param {Object} props React Component properties - * @param {Object} state React Component state - */ - _updateSeries(props, state) { - let { func, xMin, xMax, series, points } = props; - let delta = (xMax - xMin) / points; - let seriesData = new Array(points); - - if (delta) { - seriesData = new Array(points); - for (let i = 0, x = xMin; i < points; i++) { - seriesData[i] = [x, func(x)]; - x += delta; - } - seriesData[points - 1] = [xMax, func(xMax)]; - } else { - let yVal = func(xMin); - seriesData = [[0, yVal], [1, yVal]]; - } - - const markerElems = []; - const detailElems = []; - const seriesLines = []; - 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(); - markerElems.push(); - } - - const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8)); - - this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight }); - } - - /** - * Update dimensions and series data based on props and context. - */ - componentWillMount() { - this._updateSeries(this.props, this.state); - } - - /** - * Update state based on property and context changes - * @param {Object} nextProps Incoming/Next properties - * @param {Object} nextContext Incoming/Next conext - */ - componentWillReceiveProps(nextProps, nextContext) { - const props = this.props; - - if (props.code != nextProps.code) { - this._updateSeries(nextProps, this.state); - } - } - - /** - * Render the chart - * @return {React.Component} Chart SVG - */ - render() { - return ( - - { ({ 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 { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; - const lines = seriesLines.map((line, i) => ).reverse(); - - const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; - 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} - - this._moveTip(e, width)} - onTouchMove={e => this._moveTip(e, width)} - /> - - -
- ); - }} -
- ); - } -} +import React from 'react'; +import PropTypes from 'prop-types'; +import ContainerDimensions from 'react-container-dimensions'; +import * as d3 from 'd3'; +import TranslatedComponent from './TranslatedComponent'; + +const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; + +/** + * Line Chart + */ +export default class LineChart extends TranslatedComponent { + + static defaultProps = { + code: '', + xMin: 0, + yMin: 0, + points: 20, + colors: ['#ff8c0d'], + aspect: 0.5 + }; + + static propTypes = { + func: PropTypes.func.isRequired, + xLabel: PropTypes.string.isRequired, + xMin: PropTypes.number, + xMax: PropTypes.number.isRequired, + xUnit: PropTypes.string.isRequired, + xMark: PropTypes.number, + yLabel: PropTypes.string.isRequired, + yMin: PropTypes.number, + yMax: PropTypes.number.isRequired, + yUnit: PropTypes.string, + series: PropTypes.array, + colors: PropTypes.array, + points: PropTypes.number, + aspect: PropTypes.number, + code: PropTypes.string, + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._updateDimensions = this._updateDimensions.bind(this); + this._updateSeries = this._updateSeries.bind(this); + this._tooltip = this._tooltip.bind(this); + this._showTip = this._showTip.bind(this); + this._hideTip = this._hideTip.bind(this); + this._moveTip = this._moveTip.bind(this); + + const series = props.series; + + let xScale = d3.scaleLinear(); + let yScale = d3.scaleLinear(); + let xAxisScale = d3.scaleLinear(); + + this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0); + this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); + + this.state = { + xScale, + xAxisScale, + yScale, + tipHeight: 2 + (1.2 * (series ? series.length : 0.8)), + }; + } + + /** + * Update tooltip content + * @param {number} xPos x coordinate + * @param {number} width current container width + */ + _tooltip(xPos, width) { + let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; + let { xScale, yScale } = this.state; + let { formats, translate } = this.context.language; + let x0 = xScale.invert(xPos), + y0 = func(x0), + tips = this.tipContainer, + yTotal = 0, + flip = (xPos / width > 0.50), + tipWidth = 0, + tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; + + + xPos = xScale(x0); // Clamp xPos + + tips.selectAll('text.text-tip.y').text(function(d, i) { + let yVal = series ? y0[series[i]] : y0; + yTotal += yVal; + return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal); + }).append('tspan').attr('class', 'metric').text(yUnit ? ' ' + yUnit : ''); + + tips.selectAll('text').each(function() { + if (this.getBBox().width > tipWidth) { + tipWidth = Math.ceil(this.getBBox().width); + } + }); + + let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2)); + + tipWidth += 8; + 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.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'); + this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); + } + + /** + * Update dimensions based on properties and scale + * @param {Object} props React Component properties + * @param {number} scale size ratio / scale + * @param {number} width current width of the container + * @returns {Object} calculated dimensions + */ + _updateDimensions(props, scale, width) { + const { xMax, xMin, yMin, yMax } = props; + 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 + return { innerWidth, outerHeight, innerHeight }; + } + + /** + * Show tooltip + * @param {SyntheticEvent} e Event + */ + _showTip(e) { + e.preventDefault(); + this.tipContainer.style('display', null); + this.markersContainer.style('display', null); + this._moveTip(e); + } + + /** + * Move and update tooltip + * @param {SyntheticEvent} e Event + * @param {number} width current container width + */ + _moveTip(e, width) { + let clientX = e.touches ? e.touches[0].clientX : e.clientX; + this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width); + } + + /** + * Hide tooltip + * @param {SyntheticEvent} e Event + */ + _hideTip(e) { + e.preventDefault(); + this.tipContainer.style('display', 'none'); + this.markersContainer.style('display', 'none'); + } + + /** + * Update series generated from props + * @param {Object} props React Component properties + * @param {Object} state React Component state + */ + _updateSeries(props, state) { + let { func, xMin, xMax, series, points } = props; + let delta = (xMax - xMin) / points; + let seriesData = new Array(points); + + if (delta) { + seriesData = new Array(points); + for (let i = 0, x = xMin; i < points; i++) { + seriesData[i] = [x, func(x)]; + x += delta; + } + seriesData[points - 1] = [xMax, func(xMax)]; + } else { + let yVal = func(xMin); + seriesData = [[0, yVal], [1, yVal]]; + } + + const markerElems = []; + const detailElems = []; + const seriesLines = []; + 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(); + markerElems.push(); + } + + const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8)); + + this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight }); + } + + /** + * Update dimensions and series data based on props and context. + */ + componentWillMount() { + this._updateSeries(this.props, this.state); + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + const props = this.props; + + if (props.code != nextProps.code) { + this._updateSeries(nextProps, this.state); + } + } + + /** + * Render the chart + * @return {React.Component} Chart SVG + */ + render() { + return ( + + { ({ 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 { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; + const lines = seriesLines.map((line, i) => ).reverse(); + + const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; + 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} + + this._moveTip(e, width)} + onTouchMove={e => this._moveTip(e, width)} + /> + + +
+ ); + }} +
+ ); + } +} diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx index 1c9ccac8..6aa29fd4 100644 --- a/src/app/components/PieChart.jsx +++ b/src/app/components/PieChart.jsx @@ -1,92 +1,92 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ContainerDimensions from 'react-container-dimensions'; -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 : 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); - } - - - /** - * Generate a slice of the pie chart - * @param {Object} d the data for this slice - * @param {number} i the index of this slice - * @param {number} width the current width of the parent container - * @returns {Object} the SVG for the slice - */ - sliceGenerator(d, i, width) { - if (!d || d.value == 0) { - // Ignore 0 values - return null; - } - - 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() { - return ( - - { ({ width }) => { - const pie = this.pie(this.props.data), - translate = `translate(${width / 2}, ${width * 0.4})`; - - this.arc.outerRadius(width * 0.4); - return ( -
- - - {pie.map((d, i) => this.sliceGenerator(d, i, width))} - - -
- ); - }} -
- ); - } -} +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ContainerDimensions from 'react-container-dimensions'; +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 : 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); + } + + + /** + * Generate a slice of the pie chart + * @param {Object} d the data for this slice + * @param {number} i the index of this slice + * @param {number} width the current width of the parent container + * @returns {Object} the SVG for the slice + */ + sliceGenerator(d, i, width) { + if (!d || d.value == 0) { + // Ignore 0 values + return null; + } + + 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() { + return ( + + { ({ width }) => { + const pie = this.pie(this.props.data), + translate = `translate(${width / 2}, ${width * 0.4})`; + + this.arc.outerRadius(width * 0.4); + return ( +
+ + + {pie.map((d, i) => this.sliceGenerator(d, i, width))} + + +
+ ); + }} +
+ ); + } +} diff --git a/src/app/components/Slider.jsx b/src/app/components/Slider.jsx index d36ca968..0e5c5696 100644 --- a/src/app/components/Slider.jsx +++ b/src/app/components/Slider.jsx @@ -1,386 +1,386 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const MARGIN_LR = 8; // Left/ Right margin - -/** - * Horizontal Slider - */ -export default class Slider extends React.Component { - - static defaultProps = { - axis: false, - min: 0, - max: 1, - scale: 1 // SVG render scale - }; - - static propTypes = { - axis: PropTypes.bool, - axisUnit: PropTypes.string,// units (T, M, etc.) - max: PropTypes.number, - min: PropTypes.number, - onChange: PropTypes.func.isRequired,// function which determins percent value - onResize: PropTypes.func, - percent: PropTypes.number.isRequired,// value of slider - scale: PropTypes.number - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - this._down = this._down.bind(this); - this._move = this._move.bind(this); - this._up = this._up.bind(this); - this._keyup = this._keyup.bind(this); - this._keydown = this._keydown.bind(this); - this._touchstart = this._touchstart.bind(this); - this._touchend = this._touchend.bind(this); - - this._updatePercent = this._updatePercent.bind(this); - this._updateDimensions = this._updateDimensions.bind(this); - - this.state = { width: 0 }; - } - - /** - * On Mouse/Touch down handler - * @param {SyntheticEvent} event Event - */ - _down(event) { - let rect = event.currentTarget.getBoundingClientRect(); - this.left = rect.left; - this.width = rect.width; - this._move(event); - this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); - } - - /** - * Update the slider percentage on move - * @param {SyntheticEvent} event Event - */ - _move(event) { - if(this.width !== null && this.left != null) { - let clientX = event.touches ? event.touches[0].clientX : event.clientX; - event.preventDefault(); - this._updatePercent(clientX - this.left, this.width); - } - } - - /** - * On Mouse/Touch up handler - * @param {Event} event DOM Event - */ - _up(event) { - this.sliderInputBox.sliderVal.focus(); - clearTimeout(this.touchStartTimer); - event.preventDefault(); - this.left = null; - this.width = null; - } - - - /** - * Key up handler for keyboard. - * display the number field then set focus to it - * when "Enter" key is pressed - * @param {Event} event Keyboard event - */ - _keyup(event) { - switch (event.key) { - case 'Enter': - event.preventDefault(); - this.sliderInputBox._setDisplay('block'); - return; - default: - return; - } - } - /** - * Key down handler - * increment slider position by +/- 1 when right/left arrow key is pressed or held - * @param {Event} event Keyboard even - */ - _keydown(event) { - let newVal = this.props.percent * this.props.max; - switch (event.key) { - case 'ArrowRight': - newVal += 1; - if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max); - return; - case 'ArrowLeft': - newVal -= 1; - if (newVal >= 0) this.props.onChange(newVal / this.props.max); - return; - default: - return; - } - } - - /** - * Touch start handler - * @param {Event} event DOM Event - * - */ - _touchstart(event) { - this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); - } - - /** - * Touch end handler - * @param {Event} event DOM Event - * - */ - _touchend(event) { - this.sliderInputBox.sliderVal.focus(); - clearTimeout(this.touchStartTimer); - } - - /** - * Determine if the user is still dragging - * @param {SyntheticEvent} event Event - */ - _enter(event) { - if(event.buttons !== 1) { - this.left = null; - this.width = null; - } - } - - /** - * Update the slider percentage - * @param {number} pos Slider drag position - * @param {number} width Slider width - * @param {Event} event DOM Event - */ - _updatePercent(pos, width) { - this.props.onChange(Math.min(Math.max(pos / width, 0), 1)); - } - - /** - * Update dimenions from rendered DOM - */ - _updateDimensions() { - this.setState({ - outerWidth: this.node.getBoundingClientRect().width - }); - } - - /** - * Add listeners when about to mount - */ - componentWillMount() { - if (this.props.onResize) { - this.resizeListener = this.props.onResize(this._updateDimensions); - } - } - - /** - * Trigger DOM updates on mount - */ - componentDidMount() { - this._updateDimensions(); - } - - /** - * Remove listeners on unmount - */ - componentWillUnmount() { - if (this.resizeListener) { - this.resizeListener.remove(); - } - } - - /** - * Render the slider - * @return {React.Component} The slider - */ - render() { - let outerWidth = this.state.outerWidth; - let { axis, axisUnit, min, max, scale } = this.props; - let style = { - width: '100%', - height: axis ? '2.5em' : '1.5em', - boxSizing: 'border-box' - }; - if (!outerWidth) { - return this.node = node} />; - } - let margin = MARGIN_LR * scale; - let width = outerWidth - (margin * 2); - let pctPos = width * this.props.percent; - return
this.node = node} tabIndex="0"> - - - - - {axis && - {min + axisUnit} - {(min + max / 2) + axisUnit} - {max + axisUnit} - } - - this.sliderInputBox = tb} - onChange={this.props.onChange} - percent={this.props.percent} - axisUnit={this.props.axisUnit} - scale={this.props.scale} - max={this.props.max} - /> -
; - } -} -/** - * New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android) - **/ -class TextInputBox extends React.Component { - static propTypes = { - axisUnit: PropTypes.string,// units (T, M, etc.) - max: PropTypes.number, - onChange: PropTypes.func.isRequired,// function which determins percent value - percent: PropTypes.number.isRequired,// value of slider - scale: PropTypes.number - }; - /** - * Determine if the user is still dragging - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - this._handleFocus = this._handleFocus.bind(this); - this._handleBlur = this._handleBlur.bind(this); - this._handleChange = this._handleChange.bind(this); - this._keyup = this._keyup.bind(this); - this.state = this._getInitialState(); - } - /** - * Update input value if slider changes will change props/state - * @param {Object} nextProps React Component properites - * @param {Object} nextState React Component state values - */ - componentWillReceiveProps(nextProps, nextState) { - let nextValue = nextProps.percent * nextProps.max; - // See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form - if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) { - this.setState({ inputValue: nextValue }); - } - } - /** - * Update slider textbox visibility/values if changes are made to slider - * @param {Object} prevProps React Component properites - * @param {Object} prevState React Component state values - */ - componentDidUpdate(prevProps, prevState) { - if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') { - this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10); - } - if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) { - // they chose a different module - this.setState({ inputValue: this.props.max }); - } - if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) { - this.props.onChange(this.state.inputValue / this.props.max); - } - } - /** - * Set initial state for the textbox. - * We may want to rethink this to - * try and make it a stateless component - * @returns {object} React state object with initial values set - */ - _getInitialState() { - return { - divStyle: { display:'none' }, - inputStyle: { width:'4em' }, - labelStyle: { marginLeft: '.1em' }, - maxLength:5, - size:5, - min:0, - tabIndex:-1, - type:'number', - readOnly: true, - inputValue: this.props.percent * this.props.max - }; - } - /** - * - * @param {string} val block or none - */ - _setDisplay(val) { - this.setState({ - divStyle: { display:val } - }); - } - /** - * Update the input value - * when textbox gets focus - */ - _handleFocus() { - this.setState({ - inputValue:this._getValue() - }); - } - /** - * Update inputValue when textbox loses focus - */ - _handleBlur() { - this._setDisplay('none'); - if (this.state.inputValue !== '') { - this.props.onChange(this.state.inputValue / this.props.max); - } else { - this.setState({ - inputValue: this.props.percent * this.props.max - }); - } - } - /** - * Get the value in the text box - * @returns {number} inputValue Value of the input box - */ - _getValue() { - return this.state.inputValue; - } - /** - * Update and set limits on input box - * values depending on what user - * has selected - * - * @param {SyntheticEvent} event ReactJs onChange event - */ - _handleChange(event) { - if (event.target.value < 0) { - this.setState({ inputValue: 0 }); - } else if (event.target.value <= this.props.max) { - this.setState({ inputValue: event.target.value }); - } else { - this.setState({ inputValue: this.props.max }); - } - } - /** - * Key up handler for input field. - * If user hits Enter key, blur/close the input field - * @param {Event} event Keyboard event - */ - _keyup(event) { - switch (event.key) { - case 'Enter': - this.sliderVal.blur(); - return; - default: - return; - } - } - /** - * Get the value in the text box - * @return {React.Component} Text Input component for Slider - */ - render() { - let { axisUnit, onChange, percent, scale } = this.props; - return
{this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/>{this.props.axisUnit}
; - } -} - +import React from 'react'; +import PropTypes from 'prop-types'; + +const MARGIN_LR = 8; // Left/ Right margin + +/** + * Horizontal Slider + */ +export default class Slider extends React.Component { + + static defaultProps = { + axis: false, + min: 0, + max: 1, + scale: 1 // SVG render scale + }; + + static propTypes = { + axis: PropTypes.bool, + axisUnit: PropTypes.string,// units (T, M, etc.) + max: PropTypes.number, + min: PropTypes.number, + onChange: PropTypes.func.isRequired,// function which determins percent value + onResize: PropTypes.func, + percent: PropTypes.number.isRequired,// value of slider + scale: PropTypes.number + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this._down = this._down.bind(this); + this._move = this._move.bind(this); + this._up = this._up.bind(this); + this._keyup = this._keyup.bind(this); + this._keydown = this._keydown.bind(this); + this._touchstart = this._touchstart.bind(this); + this._touchend = this._touchend.bind(this); + + this._updatePercent = this._updatePercent.bind(this); + this._updateDimensions = this._updateDimensions.bind(this); + + this.state = { width: 0 }; + } + + /** + * On Mouse/Touch down handler + * @param {SyntheticEvent} event Event + */ + _down(event) { + let rect = event.currentTarget.getBoundingClientRect(); + this.left = rect.left; + this.width = rect.width; + this._move(event); + this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); + } + + /** + * Update the slider percentage on move + * @param {SyntheticEvent} event Event + */ + _move(event) { + if(this.width !== null && this.left != null) { + let clientX = event.touches ? event.touches[0].clientX : event.clientX; + event.preventDefault(); + this._updatePercent(clientX - this.left, this.width); + } + } + + /** + * On Mouse/Touch up handler + * @param {Event} event DOM Event + */ + _up(event) { + this.sliderInputBox.sliderVal.focus(); + clearTimeout(this.touchStartTimer); + event.preventDefault(); + this.left = null; + this.width = null; + } + + + /** + * Key up handler for keyboard. + * display the number field then set focus to it + * when "Enter" key is pressed + * @param {Event} event Keyboard event + */ + _keyup(event) { + switch (event.key) { + case 'Enter': + event.preventDefault(); + this.sliderInputBox._setDisplay('block'); + return; + default: + return; + } + } + /** + * Key down handler + * increment slider position by +/- 1 when right/left arrow key is pressed or held + * @param {Event} event Keyboard even + */ + _keydown(event) { + let newVal = this.props.percent * this.props.max; + switch (event.key) { + case 'ArrowRight': + newVal += 1; + if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max); + return; + case 'ArrowLeft': + newVal -= 1; + if (newVal >= 0) this.props.onChange(newVal / this.props.max); + return; + default: + return; + } + } + + /** + * Touch start handler + * @param {Event} event DOM Event + * + */ + _touchstart(event) { + this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); + } + + /** + * Touch end handler + * @param {Event} event DOM Event + * + */ + _touchend(event) { + this.sliderInputBox.sliderVal.focus(); + clearTimeout(this.touchStartTimer); + } + + /** + * Determine if the user is still dragging + * @param {SyntheticEvent} event Event + */ + _enter(event) { + if(event.buttons !== 1) { + this.left = null; + this.width = null; + } + } + + /** + * Update the slider percentage + * @param {number} pos Slider drag position + * @param {number} width Slider width + * @param {Event} event DOM Event + */ + _updatePercent(pos, width) { + this.props.onChange(Math.min(Math.max(pos / width, 0), 1)); + } + + /** + * Update dimenions from rendered DOM + */ + _updateDimensions() { + this.setState({ + outerWidth: this.node.getBoundingClientRect().width + }); + } + + /** + * Add listeners when about to mount + */ + componentWillMount() { + if (this.props.onResize) { + this.resizeListener = this.props.onResize(this._updateDimensions); + } + } + + /** + * Trigger DOM updates on mount + */ + componentDidMount() { + this._updateDimensions(); + } + + /** + * Remove listeners on unmount + */ + componentWillUnmount() { + if (this.resizeListener) { + this.resizeListener.remove(); + } + } + + /** + * Render the slider + * @return {React.Component} The slider + */ + render() { + let outerWidth = this.state.outerWidth; + let { axis, axisUnit, min, max, scale } = this.props; + let style = { + width: '100%', + height: axis ? '2.5em' : '1.5em', + boxSizing: 'border-box' + }; + if (!outerWidth) { + return this.node = node} />; + } + let margin = MARGIN_LR * scale; + let width = outerWidth - (margin * 2); + let pctPos = width * this.props.percent; + return
this.node = node} tabIndex="0"> + + + + + {axis && + {min + axisUnit} + {(min + max / 2) + axisUnit} + {max + axisUnit} + } + + this.sliderInputBox = tb} + onChange={this.props.onChange} + percent={this.props.percent} + axisUnit={this.props.axisUnit} + scale={this.props.scale} + max={this.props.max} + /> +
; + } +} +/** + * New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android) + **/ +class TextInputBox extends React.Component { + static propTypes = { + axisUnit: PropTypes.string,// units (T, M, etc.) + max: PropTypes.number, + onChange: PropTypes.func.isRequired,// function which determins percent value + percent: PropTypes.number.isRequired,// value of slider + scale: PropTypes.number + }; + /** + * Determine if the user is still dragging + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this._handleFocus = this._handleFocus.bind(this); + this._handleBlur = this._handleBlur.bind(this); + this._handleChange = this._handleChange.bind(this); + this._keyup = this._keyup.bind(this); + this.state = this._getInitialState(); + } + /** + * Update input value if slider changes will change props/state + * @param {Object} nextProps React Component properites + * @param {Object} nextState React Component state values + */ + componentWillReceiveProps(nextProps, nextState) { + let nextValue = nextProps.percent * nextProps.max; + // See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form + if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) { + this.setState({ inputValue: nextValue }); + } + } + /** + * Update slider textbox visibility/values if changes are made to slider + * @param {Object} prevProps React Component properites + * @param {Object} prevState React Component state values + */ + componentDidUpdate(prevProps, prevState) { + if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') { + this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10); + } + if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) { + // they chose a different module + this.setState({ inputValue: this.props.max }); + } + if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) { + this.props.onChange(this.state.inputValue / this.props.max); + } + } + /** + * Set initial state for the textbox. + * We may want to rethink this to + * try and make it a stateless component + * @returns {object} React state object with initial values set + */ + _getInitialState() { + return { + divStyle: { display:'none' }, + inputStyle: { width:'4em' }, + labelStyle: { marginLeft: '.1em' }, + maxLength:5, + size:5, + min:0, + tabIndex:-1, + type:'number', + readOnly: true, + inputValue: this.props.percent * this.props.max + }; + } + /** + * + * @param {string} val block or none + */ + _setDisplay(val) { + this.setState({ + divStyle: { display:val } + }); + } + /** + * Update the input value + * when textbox gets focus + */ + _handleFocus() { + this.setState({ + inputValue:this._getValue() + }); + } + /** + * Update inputValue when textbox loses focus + */ + _handleBlur() { + this._setDisplay('none'); + if (this.state.inputValue !== '') { + this.props.onChange(this.state.inputValue / this.props.max); + } else { + this.setState({ + inputValue: this.props.percent * this.props.max + }); + } + } + /** + * Get the value in the text box + * @returns {number} inputValue Value of the input box + */ + _getValue() { + return this.state.inputValue; + } + /** + * Update and set limits on input box + * values depending on what user + * has selected + * + * @param {SyntheticEvent} event ReactJs onChange event + */ + _handleChange(event) { + if (event.target.value < 0) { + this.setState({ inputValue: 0 }); + } else if (event.target.value <= this.props.max) { + this.setState({ inputValue: event.target.value }); + } else { + this.setState({ inputValue: this.props.max }); + } + } + /** + * Key up handler for input field. + * If user hits Enter key, blur/close the input field + * @param {Event} event Keyboard event + */ + _keyup(event) { + switch (event.key) { + case 'Enter': + this.sliderVal.blur(); + return; + default: + return; + } + } + /** + * Get the value in the text box + * @return {React.Component} Text Input component for Slider + */ + render() { + let { axisUnit, onChange, percent, scale } = this.props; + return
{this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/>{this.props.axisUnit}
; + } +} + diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index ad77233c..74011e0a 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -1,80 +1,80 @@ -import TranslatedComponent from './TranslatedComponent'; -import React, { PropTypes } from 'react'; -import ContainerDimensions from 'react-container-dimensions'; -import { BarChart, Bar, XAxis, YAxis, LabelList } 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); - } - - /** - * Render the bar chart - * @returns {Object} the markup - */ - render() { - 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 ( - - { ({ width }) => ( -
- - - - - - - -
- )} -
- ); - } - - /** - * 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; - } - } -} +import TranslatedComponent from './TranslatedComponent'; +import React, { PropTypes } from 'react'; +import ContainerDimensions from 'react-container-dimensions'; +import { BarChart, Bar, XAxis, YAxis, LabelList } 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); + } + + /** + * Render the bar chart + * @returns {Object} the markup + */ + render() { + 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 ( + + { ({ width }) => ( +
+ + + + + + + +
+ )} +
+ ); + } + + /** + * 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; + } + } +} diff --git a/src/app/i18n/de.js b/src/app/i18n/de.js index b1ec1c33..cb7cd702 100644 --- a/src/app/i18n/de.js +++ b/src/app/i18n/de.js @@ -1,16 +1,16 @@ -export const formats = { - decimal: ',', - thousands: '.', - grouping: [3], - currency: ['', ' €'], - dateTime: '%A, der %e. %B %Y, %X', - date: '%d.%m.%Y', - time: '%H:%M:%S', - periods: ['AM', 'PM'], // unused - days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], - shortDays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], - 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'] -}; - +export const formats = { + decimal: ',', + thousands: '.', + grouping: [3], + currency: ['', ' €'], + dateTime: '%A, der %e. %B %Y, %X', + date: '%d.%m.%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], // unused + days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + shortDays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], + 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'] +}; + export { default as terms } from './de.json'; \ No newline at end of file diff --git a/src/app/i18n/ko.js b/src/app/i18n/ko.js index c2a2887e..ded3d7d1 100644 --- a/src/app/i18n/ko.js +++ b/src/app/i18n/ko.js @@ -1,16 +1,16 @@ -export const formats = { - decimal: '.', - thousands: ',', - grouping: [3], - currency: ['₩', ''], - dateTime: '%a %b %e %X %Y', - date: '%Y/%m/%d', - time: '%H:%M:%S', - periods: ['오전', '오후'], - days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'], - shortDays: ['일', '월', '화', '수', '목', '금', '토'], - 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월'] -}; - -export { default as terms } from './ko.json'; +export const formats = { + decimal: '.', + thousands: ',', + grouping: [3], + currency: ['₩', ''], + dateTime: '%a %b %e %X %Y', + date: '%Y/%m/%d', + time: '%H:%M:%S', + periods: ['오전', '오후'], + days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'], + shortDays: ['일', '월', '화', '수', '목', '금', '토'], + 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월'] +}; + +export { default as terms } from './ko.json'; diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js index 80259bea..3365c05c 100644 --- a/src/app/utils/BlueprintFunctions.js +++ b/src/app/utils/BlueprintFunctions.js @@ -1,435 +1,435 @@ -import React from 'react'; -import { Modifications } from 'coriolis-data/dist'; -import { STATS_FORMATTING } from '../shipyard/StatsFormatting'; - -/** - * Generate a tooltip with details of a blueprint's specials - * @param {Object} translate The translate object - * @param {Object} blueprint The blueprint at the required grade - * @param {string} grp The group of the module - * @param {Object} m The module to compare with - * @param {string} specialName The name of the special - * @returns {Object} The react components - */ -export function specialToolTip(translate, blueprint, grp, m, specialName) { - const effects = []; - if (!blueprint || !blueprint.features) { - return undefined; - } - if (m) { - // We also add in any benefits from specials that aren't covered above - if (m.blueprint) { - for (const feature in Modifications.modifierActions[specialName]) { - // if (!blueprint.features[feature] && !m.mods.feature) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature) - m.getModValue(feature, true); - if (featureDef.type === 'percentage') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - } - - return ( -
- - - {effects} - -
-
- ); -} - -/** - * Generate a tooltip with details of a blueprint's effects - * @param {Object} translate The translate object - * @param {Object} blueprint The blueprint at the required grade - * @param {Array} engineers The engineers supplying this blueprint - * @param {string} grp The group of the module - * @param {Object} m The module to compare with - * @returns {Object} The react components - */ -export function blueprintTooltip(translate, blueprint, engineers, grp, m) { - const effects = []; - if (!blueprint || !blueprint.features) { - return undefined; - } - for (const feature in blueprint.features) { - const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); - const featureDef = Modifications.modifications[feature]; - if (!featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let lowerBound = blueprint.features[feature][0]; - let upperBound = blueprint.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; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} - {lowerBound}{symbol} - {current}{symbol} - {upperBound}{symbol} - - ); - } else { - // We do not have a module, no value - effects.push( - - {translate(feature, grp)} - {lowerBound}{symbol} - {upperBound}{symbol} - - ); - } - } - } - if (m) { - // Because we have a module add in any benefits that aren't part of the primary blueprint - for (const feature in m.mods) { - if (!blueprint.features[feature]) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - - // We also add in any benefits from specials that aren't covered above - if (m.blueprint && m.blueprint.special) { - for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) { - if (!blueprint.features[feature] && !m.mods.feature) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - } - } - - let components; - if (!m) { - components = []; - for (const component in blueprint.components) { - components.push( - - {translate(component)} - {blueprint.components[component]} - - ); - } - } - - let engineersList; - if (engineers) { - engineersList = []; - for (const engineer of engineers) { - engineersList.push( - - {engineer} - - ); - } - } - - return ( -
- - - - - - {m ? : null } - - - - - {effects} - -
{translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
- { components ? - - - - - - - - {components} - -
{translate('component')}{translate('amount')}
: null } - { engineersList ? - - - - - - - {engineersList} - -
{translate('engineers')}
: null } -
- ); -} - -/** - * Is this blueprint feature beneficial? - * @param {string} feature The name of the feature - * @param {array} values The value of the feature - * @returns {boolean} True if this feature is 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? - * @param {string} feature The name of the feature - * @param {number} value The value of the feature - * @returns {boolean} True if this value is beneficial - */ -export function isValueBeneficial(feature, value) { - if (Modifications.modifications[feature].higherbetter) { - return value > 0; - } else { - return value < 0; - } -} - -/** - * Is the change as shown beneficial? - * @param {string} feature The name of the feature - * @param {number} value The value of the feature as percentage change - * @returns True if the value is beneficial - */ -export function isChangeValueBeneficial(feature, value) { - let changeHigherBetter = STATS_FORMATTING[feature].higherbetter; - if (changeHigherBetter === undefined) { - return isValueBeneficial(feature, value); - } - - if (changeHigherBetter) { - 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 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)]; - if (!found || !found.fdname) { - return {}; - } - const blueprint = JSON.parse(JSON.stringify(found)); - return blueprint; -} - -/** - * Provide 'percent' primary modifications - * @param {Object} ship The ship 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. - */ -export function setPercent(ship, m, percent) { - ship.clearModifications(m); - // Pick given value as multiplier - const mult = percent / 100; - setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value)); -} - -/** - * Sets the blueprint quality and fires a callback for each property affected. - * @param {Object} blueprint The ship for which to perform the modifications - * @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) - */ -export function setQualityCB(blueprint, quality, cb) { - // Pick given value as multiplier - const features = blueprint.grades[blueprint.grade].features; - for (const featureName in features) { - let value; - if (Modifications.modifications[featureName].higherbetter) { - // Higher is better, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); - } else { - value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); - } - } else { - // Higher is worse, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); - } else { - value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); - } - } - - if (Modifications.modifications[featureName].type == 'percentage') { - value = value * 10000; - } else if (Modifications.modifications[featureName].type == 'numeric') { - value = value * 100; - } - - cb(featureName, value); - } -} - -/** - * Provide 'random' primary modifications - * @param {Object} ship The ship for which to perform the modifications - * @param {Object} m The module for which to perform the modifications - */ -export function setRandom(ship, m) { - // Pick a single value for our randomness - setPercent(ship, m, Math.random() * 100); -} - -/** - * Provide 'percent' primary query - * @param {Object} m The module for which to perform the query - * @returns {Number} percent The percentage indicator of current applied values. - */ -export function getPercent(m) { - let result = null; - const features = m.blueprint.grades[m.blueprint.grade].features; - for (const featureName in features) { - if (features[featureName][0] === features[featureName][1]) { - continue; - } - - let value = _getValue(m, featureName); - let mult; - if (Modifications.modifications[featureName].higherbetter) { - // Higher is better, but is this making it better or worse? - 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); - } else { - mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); - } - } else { - // Higher is worse, but is this making it better or worse? - 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); - } else { - mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); - } - } - - if (result && result != mult) { - return null; - } else if (result != mult) { - result = mult; - } - } - - return result; -} - -/** - * Query a feature value - * @param {Object} m The module for which to perform the query - * @param {string} featureName The feature being queried - * @returns {number} The value of the modification as a % - */ -function _getValue(m, featureName) { - if (Modifications.modifications[featureName].type == 'percentage') { - return m.getModValue(featureName, true) / 10000; - } else if (Modifications.modifications[featureName].type == 'numeric') { - return m.getModValue(featureName, true) / 100; - } else { - return m.getModValue(featureName, true); - } -} +import React from 'react'; +import { Modifications } from 'coriolis-data/dist'; +import { STATS_FORMATTING } from '../shipyard/StatsFormatting'; + +/** + * Generate a tooltip with details of a blueprint's specials + * @param {Object} translate The translate object + * @param {Object} blueprint The blueprint at the required grade + * @param {string} grp The group of the module + * @param {Object} m The module to compare with + * @param {string} specialName The name of the special + * @returns {Object} The react components + */ +export function specialToolTip(translate, blueprint, grp, m, specialName) { + const effects = []; + if (!blueprint || !blueprint.features) { + return undefined; + } + if (m) { + // We also add in any benefits from specials that aren't covered above + if (m.blueprint) { + for (const feature in Modifications.modifierActions[specialName]) { + // if (!blueprint.features[feature] && !m.mods.feature) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature) - m.getModValue(feature, true); + if (featureDef.type === 'percentage') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + } + + return ( +
+ + + {effects} + +
+
+ ); +} + +/** + * Generate a tooltip with details of a blueprint's effects + * @param {Object} translate The translate object + * @param {Object} blueprint The blueprint at the required grade + * @param {Array} engineers The engineers supplying this blueprint + * @param {string} grp The group of the module + * @param {Object} m The module to compare with + * @returns {Object} The react components + */ +export function blueprintTooltip(translate, blueprint, engineers, grp, m) { + const effects = []; + if (!blueprint || !blueprint.features) { + return undefined; + } + for (const feature in blueprint.features) { + const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); + const featureDef = Modifications.modifications[feature]; + if (!featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let lowerBound = blueprint.features[feature][0]; + let upperBound = blueprint.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; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} + {lowerBound}{symbol} + {current}{symbol} + {upperBound}{symbol} + + ); + } else { + // We do not have a module, no value + effects.push( + + {translate(feature, grp)} + {lowerBound}{symbol} + {upperBound}{symbol} + + ); + } + } + } + if (m) { + // Because we have a module add in any benefits that aren't part of the primary blueprint + for (const feature in m.mods) { + if (!blueprint.features[feature]) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + + // We also add in any benefits from specials that aren't covered above + if (m.blueprint && m.blueprint.special) { + for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) { + if (!blueprint.features[feature] && !m.mods.feature) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + } + } + + let components; + if (!m) { + components = []; + for (const component in blueprint.components) { + components.push( + + {translate(component)} + {blueprint.components[component]} + + ); + } + } + + let engineersList; + if (engineers) { + engineersList = []; + for (const engineer of engineers) { + engineersList.push( + + {engineer} + + ); + } + } + + return ( +
+ + + + + + {m ? : null } + + + + + {effects} + +
{translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
+ { components ? + + + + + + + + {components} + +
{translate('component')}{translate('amount')}
: null } + { engineersList ? + + + + + + + {engineersList} + +
{translate('engineers')}
: null } +
+ ); +} + +/** + * Is this blueprint feature beneficial? + * @param {string} feature The name of the feature + * @param {array} values The value of the feature + * @returns {boolean} True if this feature is 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? + * @param {string} feature The name of the feature + * @param {number} value The value of the feature + * @returns {boolean} True if this value is beneficial + */ +export function isValueBeneficial(feature, value) { + if (Modifications.modifications[feature].higherbetter) { + return value > 0; + } else { + return value < 0; + } +} + +/** + * Is the change as shown beneficial? + * @param {string} feature The name of the feature + * @param {number} value The value of the feature as percentage change + * @returns True if the value is beneficial + */ +export function isChangeValueBeneficial(feature, value) { + let changeHigherBetter = STATS_FORMATTING[feature].higherbetter; + if (changeHigherBetter === undefined) { + return isValueBeneficial(feature, value); + } + + if (changeHigherBetter) { + 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 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)]; + if (!found || !found.fdname) { + return {}; + } + const blueprint = JSON.parse(JSON.stringify(found)); + return blueprint; +} + +/** + * Provide 'percent' primary modifications + * @param {Object} ship The ship 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. + */ +export function setPercent(ship, m, percent) { + ship.clearModifications(m); + // Pick given value as multiplier + const mult = percent / 100; + setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value)); +} + +/** + * Sets the blueprint quality and fires a callback for each property affected. + * @param {Object} blueprint The ship for which to perform the modifications + * @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) + */ +export function setQualityCB(blueprint, quality, cb) { + // Pick given value as multiplier + const features = blueprint.grades[blueprint.grade].features; + for (const featureName in features) { + let value; + if (Modifications.modifications[featureName].higherbetter) { + // Higher is better, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); + } else { + value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); + } + } else { + // Higher is worse, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); + } else { + value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); + } + } + + if (Modifications.modifications[featureName].type == 'percentage') { + value = value * 10000; + } else if (Modifications.modifications[featureName].type == 'numeric') { + value = value * 100; + } + + cb(featureName, value); + } +} + +/** + * Provide 'random' primary modifications + * @param {Object} ship The ship for which to perform the modifications + * @param {Object} m The module for which to perform the modifications + */ +export function setRandom(ship, m) { + // Pick a single value for our randomness + setPercent(ship, m, Math.random() * 100); +} + +/** + * Provide 'percent' primary query + * @param {Object} m The module for which to perform the query + * @returns {Number} percent The percentage indicator of current applied values. + */ +export function getPercent(m) { + let result = null; + const features = m.blueprint.grades[m.blueprint.grade].features; + for (const featureName in features) { + if (features[featureName][0] === features[featureName][1]) { + continue; + } + + let value = _getValue(m, featureName); + let mult; + if (Modifications.modifications[featureName].higherbetter) { + // Higher is better, but is this making it better or worse? + 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); + } else { + mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); + } + } else { + // Higher is worse, but is this making it better or worse? + 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); + } else { + mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); + } + } + + if (result && result != mult) { + return null; + } else if (result != mult) { + result = mult; + } + } + + return result; +} + +/** + * Query a feature value + * @param {Object} m The module for which to perform the query + * @param {string} featureName The feature being queried + * @returns {number} The value of the modification as a % + */ +function _getValue(m, featureName) { + if (Modifications.modifications[featureName].type == 'percentage') { + return m.getModValue(featureName, true) / 10000; + } else if (Modifications.modifications[featureName].type == 'numeric') { + return m.getModValue(featureName, true) / 100; + } else { + return m.getModValue(featureName, true); + } +}