From cd32c58fb4bcd731d8a83d354c9b01a38c8df219 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 10 Oct 2018 13:36:38 -0230 Subject: [PATCH] Complete integration of gas chart with redux. --- .../advanced-tab-content.component.js | 2 +- .../gas-modal-page-container.component.js | 3 +- .../gas-modal-page-container.container.js | 31 +++++- ...gas-modal-page-container-component.test.js | 2 + ...gas-modal-page-container-container.test.js | 16 +++- .../gas-price-chart.component.js | 94 +++++++++++++++++-- .../tests/gas-price-chart.component.test.js | 11 ++- ui/app/ducks/gas.duck.js | 2 +- ui/app/selectors/custom-gas.js | 1 + 9 files changed, 148 insertions(+), 14 deletions(-) diff --git a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index 44aba358c..4dd18ce2b 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -97,7 +97,7 @@ export default class AdvancedTabContent extends Component { updateCustomGasLimit ) }
Live Gas Price Predictions
- +
Slower Faster diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index de14e1b38..8f23b22e0 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -47,6 +47,7 @@ export default class GasModalPageContainer extends Component { customGasLimit, newTotalFiat, gasChartProps, + currentTimeEstimate, }) { const { transactionFee } = this.props return ( @@ -55,7 +56,7 @@ export default class GasModalPageContainer extends Component { updateCustomGasLimit={convertThenUpdateCustomGasLimit} customGasPrice={customGasPrice} customGasLimit={customGasLimit} - timeRemaining="1 min 31 sec" + timeRemaining={currentTimeEstimate} transactionFee={transactionFee} totalFee={newTotalFiat} gasChartProps={gasChartProps} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 84eae1880..3a62d21cc 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -10,6 +10,7 @@ import { setCustomGasPrice, setCustomGasLimit, resetCustomData, + setCustomTimeEstimate, } from '../../../ducks/gas.duck' import { hideGasButtonGroup, @@ -29,6 +30,7 @@ import { getBasicGasEstimateLoadingStatus, getAveragePriceEstimateInHexWEI, getDefaultActiveButtonIndex, + formatTimeEstimate, } from '../../../selectors/custom-gas' import { formatCurrency, @@ -65,20 +67,24 @@ const mapStateToProps = state => { const hideBasic = state.appState.modal.modalState.props.hideBasic + const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex) + return { hideBasic, isConfirm: isConfirm(state), customModalGasPriceInHex, customModalGasLimitInHex, - customGasPrice: calcCustomGasPrice(customModalGasPriceInHex), + customGasPrice, customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), newTotalFiat, + currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, state.gas.priceAndTimeEstimates), gasPriceButtonGroupProps: { buttonDataLoading, defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), gasButtonInfo, }, gasChartProps: { + currentPrice: customGasPrice, priceAndTimeEstimates: state.gas.priceAndTimeEstimates, }, infoRowProps: { @@ -111,6 +117,7 @@ const mapDispatchToProps = dispatch => { return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) }, hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), + setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)), } } @@ -181,3 +188,25 @@ function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conver partialRight(formatCurrency, [convertedCurrency]), )(aHexWEI, bHexWEI) } + +function getRenderableTimeEstimate (currentGasPrice, priceAndTimeEstimates) { + const gasPrices = priceAndTimeEstimates.map(({ gasprice }) => gasprice) + const estimatedTimes = priceAndTimeEstimates.map(({ expectedTime }) => expectedTime) + + const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => { + return e <= currentGasPrice && a[i + 1] >= currentGasPrice + }) + const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => { + return e > currentGasPrice + }) + + const closestLowerValue = gasPrices[closestLowerValueIndex] + const closestHigherValue = gasPrices[closestHigherValueIndex] + const estimatedClosestLowerTimeEstimate = estimatedTimes[closestLowerValueIndex] + const estimatedClosestHigherTimeEstimate = estimatedTimes[closestHigherValueIndex] + + const slope = (estimatedClosestHigherTimeEstimate - estimatedClosestLowerTimeEstimate) / (closestHigherValue - closestLowerValue) + const newTimeEstimate = -1 * (slope * (closestHigherValue - currentGasPrice) - estimatedClosestHigherTimeEstimate) + + return formatTimeEstimate(newTimeEstimate) +} diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index bb1a28136..61871f5f3 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -65,6 +65,7 @@ describe('GasModalPageContainer Component', function () { customGasLimit={54321} gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} infoRowProps={mockInfoRowProps} + currentTimeEstimate={'1 min 31 sec'} customGasPriceInHex={'mockCustomGasPriceInHex'} customGasLimitInHex={'mockCustomGasLimitInHex'} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) @@ -199,6 +200,7 @@ describe('GasModalPageContainer Component', function () { customGasPrice: 123, customGasLimit: 456, newTotalFiat: '$0.30', + currentTimeEstimate: '1 min 31 sec', }) const advancedTabContentProps = renderAdvancedTabContentResult.props assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice') diff --git a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index c16a07b76..3f3be8d0c 100644 --- a/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -75,7 +75,12 @@ describe('gas-modal-page-container container', () => { limit: 'aaaaaaaa', price: 'ffffffff', }, - priceAndTimeEstimates: 'mockPriceAndTimeEstimates', + priceAndTimeEstimates: [ + { gasprice: 3, expectedTime: '31' }, + { gasprice: 4, expectedTime: '62' }, + { gasprice: 5, expectedTime: '93' }, + { gasprice: 6, expectedTime: '124' }, + ], }, confirmTransaction: { txData: { @@ -93,11 +98,18 @@ describe('gas-modal-page-container container', () => { isConfirm: true, customGasPrice: 4.294967295, customGasLimit: 2863311530, + currentTimeEstimate: '~1 min 11 sec', newTotalFiat: '637.41', customModalGasLimitInHex: 'aaaaaaaa', customModalGasPriceInHex: 'ffffffff', gasChartProps: { - priceAndTimeEstimates: 'mockPriceAndTimeEstimates', + 'currentPrice': 4.294967295, + priceAndTimeEstimates: [ + { gasprice: 3, expectedTime: '31' }, + { gasprice: 4, expectedTime: '62' }, + { gasprice: 5, expectedTime: '93' }, + { gasprice: 6, expectedTime: '124' }, + ], }, gasPriceButtonGroupProps: { buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4', diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js index 85893f771..69bbd12f6 100644 --- a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js +++ b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js @@ -35,6 +35,43 @@ function appendOrUpdateCircle ({ circle, data, itemIndex, cx, cy, cssId, appendO } } +function setSelectedCircle ({ chart, gasPrices, currentPrice, chartXStart, chartWidth }) { + const numberOfValues = chart.internal.data.xs.data1.length + const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => { + return e <= currentPrice && a[i + 1] >= currentPrice + }) + const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => { + return e > currentPrice + }) + const closestHigherValue = gasPrices[closestHigherValueIndex] + const closestLowerValue = gasPrices[closestLowerValueIndex] + + if (closestHigherValue && closestLowerValue) { + const closestLowerCircle = d3.select(`.c3-circle-${closestLowerValueIndex}`) + const closestHigherCircle = d3.select(`.c3-circle-${closestHigherValueIndex}`) + const { x: lowerX, y: lowerY } = closestLowerCircle.node().getBoundingClientRect() + const { x: higherX, y: higherY } = closestHigherCircle.node().getBoundingClientRect() + const currentX = lowerX + (higherX - lowerX) * (currentPrice - closestLowerValue) / (closestHigherValue - closestLowerValue) + const slope = (higherY - lowerY) / (higherX - lowerX) + const newTimeEstimate = -1 * (slope * (higherX - currentX) - higherY) + chart.internal.selectPointB({ + x: currentX, + value: newTimeEstimate, + id: 'data1', + index: numberOfValues, + name: 'data1', + }, numberOfValues) + } else { + const setCircle = d3.select('#set-circle') + if (!setCircle.empty()) { + setCircle.remove() + } + d3.select('.c3-tooltip-container').style('display', 'none !important') + chart.internal.hideXGridFocus() + return + } +} + export default class GasPriceChart extends Component { static contextTypes = { t: PropTypes.func, @@ -42,12 +79,15 @@ export default class GasPriceChart extends Component { static propTypes = { priceAndTimeEstimates: PropTypes.array, + currentPrice: PropTypes.number, + updateCustomGasPrice: PropTypes.func, } - renderChart (priceAndTimeEstimates) { + renderChart (currentPrice, priceAndTimeEstimates, updateCustomGasPrice) { const gasPrices = priceAndTimeEstimates.map(({ gasprice }) => gasprice) const gasPricesMax = gasPrices[gasPrices.length - 1] + 1 const estimatedTimes = priceAndTimeEstimates.map(({ expectedTime }) => expectedTime) + const estimatedTimesMax = estimatedTimes[0] const chart = c3.generate({ size: { @@ -187,6 +227,28 @@ export default class GasPriceChart extends Component { }) } + chart.internal.selectPointB = function (data, itemIndex = (data.index || 0)) { + const { x: chartXStart, y: chartYStart } = d3.select('.c3-areas-data1') + .node() + .getBoundingClientRect() + + d3.select('#set-circle').remove() + + const circle = this.main + .select('.' + 'c3-selected-circles' + this.getTargetSelectorSuffix(data.id)) + .selectAll('.' + 'c3-selected-circle' + '-' + itemIndex) + + appendOrUpdateCircle.bind(this)({ + circle, + data, + itemIndex, + cx: () => data.x - chartXStart + 11, + cy: () => data.value - chartYStart + 10, + cssId: 'set-circle', + appendOnly: true, + }) + } + chart.internal.overlayPoint = function (data, itemIndex) { const circle = this.main .select('.' + 'c3-selected-circles' + this.getTargetSelectorSuffix(data.id)) @@ -238,13 +300,13 @@ export default class GasPriceChart extends Component { setTimeout(function () { setTickPosition('y', 0, -5, 8) - setTickPosition('y', 1, -3) - setTickPosition('x', 0, 3, 20) - setTickPosition('x', 1, 3, -10) + setTickPosition('y', 1, -3, -5) + setTickPosition('x', 0, 3, 15) + setTickPosition('x', 1, 3, -8) // TODO: Confirm the below constants work with all data sets and screen sizes d3.select('.c3-axis-x-label').attr('transform', 'translate(0,-15)') - d3.select('.c3-axis-y-label').attr('transform', 'translate(32, 2) rotate(-90)') + d3.select('.c3-axis-y-label').attr('transform', 'translate(52, 2) rotate(-90)') d3.select('.c3-xgrid-focus line').attr('y2', 98) d3.select('.c3-chart').on('mouseout', () => { @@ -262,6 +324,7 @@ export default class GasPriceChart extends Component { const overlayedCircle = d3.select('#overlayed-circle') const numberOfValues = chart.internal.data.xs.data1.length const { x: circleX, y: circleY } = overlayedCircle.node().getBoundingClientRect() + const { x: xData } = overlayedCircle.datum() chart.internal.selectPoint({ x: circleX - chartXStart, value: circleY - 1.5, @@ -269,13 +332,15 @@ export default class GasPriceChart extends Component { index: numberOfValues, name: 'data1', }, numberOfValues) + updateCustomGasPrice(xData) }) + setSelectedCircle({ chart, gasPrices, currentPrice, chartXStart, chartWidth }) + d3.select('.c3-chart').on('mousemove', function () { const chartMouseXPos = d3.event.clientX - chartXStart const posPercentile = chartMouseXPos / chartWidth - const currentPosValue = (gasPrices[gasPrices.length - 1] - gasPrices[0]) * posPercentile + gasPrices[0] const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => { return e <= currentPosValue && a[i + 1] >= currentPosValue @@ -326,11 +391,26 @@ export default class GasPriceChart extends Component { }) }, 0) + this.chart = chart + } + componentDidUpdate (prevProps) { + if (prevProps.currentPrice !== this.props.currentPrice) { + const chartRect = d3.select('.c3-areas-data1') + const { x: chartXStart, width: chartWidth } = chartRect.node().getBoundingClientRect() + setSelectedCircle({ + chart: this.chart, + currentPrice: this.props.currentPrice, + gasPrices: this.props.priceAndTimeEstimates.map(({ gasprice }) => gasprice), + chartXStart, + chartWidth, + }) + } } componentDidMount () { - this.renderChart(this.props.priceAndTimeEstimates) + const { currentPrice, priceAndTimeEstimates, updateCustomGasPrice } = this.props + this.renderChart(currentPrice, priceAndTimeEstimates, updateCustomGasPrice) } render () { diff --git a/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js index ae98659cc..48b8a6525 100644 --- a/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js +++ b/ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js @@ -10,6 +10,9 @@ const mockSelectReturn = { node: () => ({ getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }), }), + empty: sinon.spy(), + remove: sinon.spy(), + style: sinon.spy(), select: d3.select, attr: sinon.spy(), on: sinon.spy(), @@ -17,11 +20,17 @@ const mockSelectReturn = { const GasPriceChart = proxyquire('../gas-price-chart.component.js', { 'c3': { - generate: function () { + generate: function ({ data: { columns } }) { return { internal: { showTooltip: () => {}, showXGridFocus: () => {}, + hideXGridFocus: () => {}, + data: { + xs: { + [columns[1][0]]: columns[1].slice(1), + }, + }, }, } }, diff --git a/ui/app/ducks/gas.duck.js b/ui/app/ducks/gas.duck.js index 35e26544a..2dcec91de 100644 --- a/ui/app/ducks/gas.duck.js +++ b/ui/app/ducks/gas.duck.js @@ -212,7 +212,7 @@ export function fetchGasEstimates (blockTime) { const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice })) const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes) const timeMappedToSeconds = estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }) => { - const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).div(60, 10).toString(10) + const expectedTime = (new BigNumber(expectedWait)).times(Number(blockTime), 10).toString(10) return { expectedTime, expectedWait, diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index d5a3972df..abc8ba191 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -31,6 +31,7 @@ const selectors = { getAveragePriceEstimateInHexWEI, getDefaultActiveButtonIndex, priceEstimateToWei, + formatTimeEstimate, } module.exports = selectors