Complete integration of gas chart with redux.

feature/default_network_editable
Dan Miller 6 years ago
parent a2bbf504b8
commit cd32c58fb4
  1. 2
      ui/app/components/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  2. 3
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
  3. 31
      ui/app/components/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  4. 2
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js
  5. 16
      ui/app/components/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
  6. 94
      ui/app/components/gas-customization/gas-price-chart/gas-price-chart.component.js
  7. 11
      ui/app/components/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js
  8. 2
      ui/app/ducks/gas.duck.js
  9. 1
      ui/app/selectors/custom-gas.js

@ -97,7 +97,7 @@ export default class AdvancedTabContent extends Component {
updateCustomGasLimit updateCustomGasLimit
) } ) }
<div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div> <div className="advanced-tab__fee-chart__title">Live Gas Price Predictions</div>
<GasPriceChart {...gasChartProps} /> <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} />
<div className="advanced-tab__fee-chart__speed-buttons"> <div className="advanced-tab__fee-chart__speed-buttons">
<span>Slower</span> <span>Slower</span>
<span>Faster</span> <span>Faster</span>

@ -47,6 +47,7 @@ export default class GasModalPageContainer extends Component {
customGasLimit, customGasLimit,
newTotalFiat, newTotalFiat,
gasChartProps, gasChartProps,
currentTimeEstimate,
}) { }) {
const { transactionFee } = this.props const { transactionFee } = this.props
return ( return (
@ -55,7 +56,7 @@ export default class GasModalPageContainer extends Component {
updateCustomGasLimit={convertThenUpdateCustomGasLimit} updateCustomGasLimit={convertThenUpdateCustomGasLimit}
customGasPrice={customGasPrice} customGasPrice={customGasPrice}
customGasLimit={customGasLimit} customGasLimit={customGasLimit}
timeRemaining="1 min 31 sec" timeRemaining={currentTimeEstimate}
transactionFee={transactionFee} transactionFee={transactionFee}
totalFee={newTotalFiat} totalFee={newTotalFiat}
gasChartProps={gasChartProps} gasChartProps={gasChartProps}

@ -10,6 +10,7 @@ import {
setCustomGasPrice, setCustomGasPrice,
setCustomGasLimit, setCustomGasLimit,
resetCustomData, resetCustomData,
setCustomTimeEstimate,
} from '../../../ducks/gas.duck' } from '../../../ducks/gas.duck'
import { import {
hideGasButtonGroup, hideGasButtonGroup,
@ -29,6 +30,7 @@ import {
getBasicGasEstimateLoadingStatus, getBasicGasEstimateLoadingStatus,
getAveragePriceEstimateInHexWEI, getAveragePriceEstimateInHexWEI,
getDefaultActiveButtonIndex, getDefaultActiveButtonIndex,
formatTimeEstimate,
} from '../../../selectors/custom-gas' } from '../../../selectors/custom-gas'
import { import {
formatCurrency, formatCurrency,
@ -65,20 +67,24 @@ const mapStateToProps = state => {
const hideBasic = state.appState.modal.modalState.props.hideBasic const hideBasic = state.appState.modal.modalState.props.hideBasic
const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex)
return { return {
hideBasic, hideBasic,
isConfirm: isConfirm(state), isConfirm: isConfirm(state),
customModalGasPriceInHex, customModalGasPriceInHex,
customModalGasLimitInHex, customModalGasLimitInHex,
customGasPrice: calcCustomGasPrice(customModalGasPriceInHex), customGasPrice,
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
newTotalFiat, newTotalFiat,
currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, state.gas.priceAndTimeEstimates),
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
buttonDataLoading, buttonDataLoading,
defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),
gasButtonInfo, gasButtonInfo,
}, },
gasChartProps: { gasChartProps: {
currentPrice: customGasPrice,
priceAndTimeEstimates: state.gas.priceAndTimeEstimates, priceAndTimeEstimates: state.gas.priceAndTimeEstimates,
}, },
infoRowProps: { infoRowProps: {
@ -111,6 +117,7 @@ const mapDispatchToProps = dispatch => {
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
}, },
hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)),
} }
} }
@ -181,3 +188,25 @@ function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conver
partialRight(formatCurrency, [convertedCurrency]), partialRight(formatCurrency, [convertedCurrency]),
)(aHexWEI, bHexWEI) )(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)
}

@ -65,6 +65,7 @@ describe('GasModalPageContainer Component', function () {
customGasLimit={54321} customGasLimit={54321}
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
infoRowProps={mockInfoRowProps} infoRowProps={mockInfoRowProps}
currentTimeEstimate={'1 min 31 sec'}
customGasPriceInHex={'mockCustomGasPriceInHex'} customGasPriceInHex={'mockCustomGasPriceInHex'}
customGasLimitInHex={'mockCustomGasLimitInHex'} customGasLimitInHex={'mockCustomGasLimitInHex'}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
@ -199,6 +200,7 @@ describe('GasModalPageContainer Component', function () {
customGasPrice: 123, customGasPrice: 123,
customGasLimit: 456, customGasLimit: 456,
newTotalFiat: '$0.30', newTotalFiat: '$0.30',
currentTimeEstimate: '1 min 31 sec',
}) })
const advancedTabContentProps = renderAdvancedTabContentResult.props const advancedTabContentProps = renderAdvancedTabContentResult.props
assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice') assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice')

@ -75,7 +75,12 @@ describe('gas-modal-page-container container', () => {
limit: 'aaaaaaaa', limit: 'aaaaaaaa',
price: 'ffffffff', price: 'ffffffff',
}, },
priceAndTimeEstimates: 'mockPriceAndTimeEstimates', priceAndTimeEstimates: [
{ gasprice: 3, expectedTime: '31' },
{ gasprice: 4, expectedTime: '62' },
{ gasprice: 5, expectedTime: '93' },
{ gasprice: 6, expectedTime: '124' },
],
}, },
confirmTransaction: { confirmTransaction: {
txData: { txData: {
@ -93,11 +98,18 @@ describe('gas-modal-page-container container', () => {
isConfirm: true, isConfirm: true,
customGasPrice: 4.294967295, customGasPrice: 4.294967295,
customGasLimit: 2863311530, customGasLimit: 2863311530,
currentTimeEstimate: '~1 min 11 sec',
newTotalFiat: '637.41', newTotalFiat: '637.41',
customModalGasLimitInHex: 'aaaaaaaa', customModalGasLimitInHex: 'aaaaaaaa',
customModalGasPriceInHex: 'ffffffff', customModalGasPriceInHex: 'ffffffff',
gasChartProps: { gasChartProps: {
priceAndTimeEstimates: 'mockPriceAndTimeEstimates', 'currentPrice': 4.294967295,
priceAndTimeEstimates: [
{ gasprice: 3, expectedTime: '31' },
{ gasprice: 4, expectedTime: '62' },
{ gasprice: 5, expectedTime: '93' },
{ gasprice: 6, expectedTime: '124' },
],
}, },
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4', buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4',

@ -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 { export default class GasPriceChart extends Component {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
@ -42,12 +79,15 @@ export default class GasPriceChart extends Component {
static propTypes = { static propTypes = {
priceAndTimeEstimates: PropTypes.array, priceAndTimeEstimates: PropTypes.array,
currentPrice: PropTypes.number,
updateCustomGasPrice: PropTypes.func,
} }
renderChart (priceAndTimeEstimates) { renderChart (currentPrice, priceAndTimeEstimates, updateCustomGasPrice) {
const gasPrices = priceAndTimeEstimates.map(({ gasprice }) => gasprice) const gasPrices = priceAndTimeEstimates.map(({ gasprice }) => gasprice)
const gasPricesMax = gasPrices[gasPrices.length - 1] + 1 const gasPricesMax = gasPrices[gasPrices.length - 1] + 1
const estimatedTimes = priceAndTimeEstimates.map(({ expectedTime }) => expectedTime) const estimatedTimes = priceAndTimeEstimates.map(({ expectedTime }) => expectedTime)
const estimatedTimesMax = estimatedTimes[0] const estimatedTimesMax = estimatedTimes[0]
const chart = c3.generate({ const chart = c3.generate({
size: { 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) { chart.internal.overlayPoint = function (data, itemIndex) {
const circle = this.main const circle = this.main
.select('.' + 'c3-selected-circles' + this.getTargetSelectorSuffix(data.id)) .select('.' + 'c3-selected-circles' + this.getTargetSelectorSuffix(data.id))
@ -238,13 +300,13 @@ export default class GasPriceChart extends Component {
setTimeout(function () { setTimeout(function () {
setTickPosition('y', 0, -5, 8) setTickPosition('y', 0, -5, 8)
setTickPosition('y', 1, -3) setTickPosition('y', 1, -3, -5)
setTickPosition('x', 0, 3, 20) setTickPosition('x', 0, 3, 15)
setTickPosition('x', 1, 3, -10) setTickPosition('x', 1, 3, -8)
// TODO: Confirm the below constants work with all data sets and screen sizes // 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-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-xgrid-focus line').attr('y2', 98)
d3.select('.c3-chart').on('mouseout', () => { d3.select('.c3-chart').on('mouseout', () => {
@ -262,6 +324,7 @@ export default class GasPriceChart extends Component {
const overlayedCircle = d3.select('#overlayed-circle') const overlayedCircle = d3.select('#overlayed-circle')
const numberOfValues = chart.internal.data.xs.data1.length const numberOfValues = chart.internal.data.xs.data1.length
const { x: circleX, y: circleY } = overlayedCircle.node().getBoundingClientRect() const { x: circleX, y: circleY } = overlayedCircle.node().getBoundingClientRect()
const { x: xData } = overlayedCircle.datum()
chart.internal.selectPoint({ chart.internal.selectPoint({
x: circleX - chartXStart, x: circleX - chartXStart,
value: circleY - 1.5, value: circleY - 1.5,
@ -269,13 +332,15 @@ export default class GasPriceChart extends Component {
index: numberOfValues, index: numberOfValues,
name: 'data1', name: 'data1',
}, numberOfValues) }, numberOfValues)
updateCustomGasPrice(xData)
}) })
setSelectedCircle({ chart, gasPrices, currentPrice, chartXStart, chartWidth })
d3.select('.c3-chart').on('mousemove', function () { d3.select('.c3-chart').on('mousemove', function () {
const chartMouseXPos = d3.event.clientX - chartXStart const chartMouseXPos = d3.event.clientX - chartXStart
const posPercentile = chartMouseXPos / chartWidth const posPercentile = chartMouseXPos / chartWidth
const currentPosValue = (gasPrices[gasPrices.length - 1] - gasPrices[0]) * posPercentile + gasPrices[0] const currentPosValue = (gasPrices[gasPrices.length - 1] - gasPrices[0]) * posPercentile + gasPrices[0]
const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => { const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => {
return e <= currentPosValue && a[i + 1] >= currentPosValue return e <= currentPosValue && a[i + 1] >= currentPosValue
@ -326,11 +391,26 @@ export default class GasPriceChart extends Component {
}) })
}, 0) }, 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 () { componentDidMount () {
this.renderChart(this.props.priceAndTimeEstimates) const { currentPrice, priceAndTimeEstimates, updateCustomGasPrice } = this.props
this.renderChart(currentPrice, priceAndTimeEstimates, updateCustomGasPrice)
} }
render () { render () {

@ -10,6 +10,9 @@ const mockSelectReturn = {
node: () => ({ node: () => ({
getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }), getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }),
}), }),
empty: sinon.spy(),
remove: sinon.spy(),
style: sinon.spy(),
select: d3.select, select: d3.select,
attr: sinon.spy(), attr: sinon.spy(),
on: sinon.spy(), on: sinon.spy(),
@ -17,11 +20,17 @@ const mockSelectReturn = {
const GasPriceChart = proxyquire('../gas-price-chart.component.js', { const GasPriceChart = proxyquire('../gas-price-chart.component.js', {
'c3': { 'c3': {
generate: function () { generate: function ({ data: { columns } }) {
return { return {
internal: { internal: {
showTooltip: () => {}, showTooltip: () => {},
showXGridFocus: () => {}, showXGridFocus: () => {},
hideXGridFocus: () => {},
data: {
xs: {
[columns[1][0]]: columns[1].slice(1),
},
},
}, },
} }
}, },

@ -212,7 +212,7 @@ export function fetchGasEstimates (blockTime) {
const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice })) const estimatedPricesAndTimes = r.map(({ expectedTime, expectedWait, gasprice }) => ({ expectedTime, expectedWait, gasprice }))
const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes) const estimatedTimeWithUniquePrices = uniqBy(({ expectedTime }) => expectedTime, estimatedPricesAndTimes)
const timeMappedToSeconds = estimatedTimeWithUniquePrices.map(({ expectedWait, gasprice }) => { 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 { return {
expectedTime, expectedTime,
expectedWait, expectedWait,

@ -31,6 +31,7 @@ const selectors = {
getAveragePriceEstimateInHexWEI, getAveragePriceEstimateInHexWEI,
getDefaultActiveButtonIndex, getDefaultActiveButtonIndex,
priceEstimateToWei, priceEstimateToWei,
formatTimeEstimate,
} }
module.exports = selectors module.exports = selectors

Loading…
Cancel
Save