1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Gas price chart improvements, redesign, bug fixes, and set up to receive external data

This commit is contained in:
Dan Miller 2018-10-03 10:50:05 -02:30
parent 0ba6f7d9bb
commit 2dbae581ac
5 changed files with 301 additions and 89 deletions

View File

@ -50,7 +50,6 @@
font-size: 12px; font-size: 12px;
color: #313A5E; color: #313A5E;
margin-left: 22px; margin-left: 22px;
margin-bottom: 11px;
} }
&__speed-buttons { &__speed-buttons {

View File

@ -27,6 +27,7 @@ export default class GasModalPageContainer extends Component {
customModalGasPriceInHex: PropTypes.string, customModalGasPriceInHex: PropTypes.string,
customModalGasLimitInHex: PropTypes.string, customModalGasLimitInHex: PropTypes.string,
cancelAndClose: PropTypes.func, cancelAndClose: PropTypes.func,
transactionFee: PropTypes.string,
} }
state = {} state = {}
@ -46,6 +47,7 @@ export default class GasModalPageContainer extends Component {
customGasLimit, customGasLimit,
newTotalFiat, newTotalFiat,
}) { }) {
const { transactionFee } = this.props
return ( return (
<AdvancedTabContent <AdvancedTabContent
updateCustomGasPrice={convertThenUpdateCustomGasPrice} updateCustomGasPrice={convertThenUpdateCustomGasPrice}
@ -53,6 +55,7 @@ export default class GasModalPageContainer extends Component {
customGasPrice={customGasPrice} customGasPrice={customGasPrice}
customGasLimit={customGasLimit} customGasLimit={customGasLimit}
timeRemaining="1 min 31 sec" timeRemaining="1 min 31 sec"
transactionFee={transactionFee}
totalFee={newTotalFiat} totalFee={newTotalFiat}
/> />
) )

View File

@ -73,6 +73,7 @@ const mapStateToProps = state => {
customGasPrice: calcCustomGasPrice(customModalGasPriceInHex), customGasPrice: calcCustomGasPrice(customModalGasPriceInHex),
customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), customGasLimit: calcCustomGasLimit(customModalGasLimitInHex),
newTotalFiat, newTotalFiat,
transactionFee: addHexWEIsToRenderableFiat('0x0', customGasTotal, currentCurrency, conversionRate),
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
buttonDataLoading, buttonDataLoading,
defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex),

View File

@ -18,139 +18,319 @@ function setTickPosition (axis, n, newPosition, secondNewPosition) {
.style('visibility', 'visible') .style('visibility', 'visible')
} }
function appendOrUpdateCircle ({ circle, data, itemIndex, cx, cy, cssId, appendOnly }) {
if (appendOnly || circle.empty()) {
circle.data([data])
.enter().append('circle')
.attr('class', () => this.generateClass('c3-selected-circle', itemIndex))
.attr('id', cssId)
.attr('cx', cx)
.attr('cy', cy)
.attr('stroke', () => this.color(data))
.attr('r', 6)
} else {
circle.data([data])
.attr('cx', cx)
.attr('cy', cy)
}
}
export default class GasPriceChart extends Component { export default class GasPriceChart extends Component {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
static propTypes = { static propTypes = {
priceAndTimeEstimates: PropTypes.array,
} }
renderChart () { renderChart (priceAndTimeEstimates) {
c3.generate({ 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: { size: {
height: 154, height: 165,
}, },
padding: {left: 36, right: 25, top: -5, bottom: 5}, transition: {
duration: 0,
},
padding: {left: 20, right: 15, top: 6, bottom: 10},
data: { data: {
x: 'x', x: 'x',
columns: [ columns: [
['x', 0, 0.01, 0.02, 0.03, 0.05, 0.07, 0.11, 0.15, 0.29, 0.35, 0.5, 0.55, 0.60, 0.63, 0.77, 0.88, 0.92, 0.93, 0.98, 0.99], ['x', ...gasPrices],
['data1', 100, 66, 55, 50, 45, 25, 22, 20.1, 20, 19.9, 15, 12, 10, 9.9, 8.0, 4.0, 3, 1, 0.5, 0.2], ['data1', ...estimatedTimes],
], ],
types: { types: {
data1: 'area', data1: 'area',
}, },
selection: {
enabled: false,
},
}, },
color: { color: {
data1: '#259de5', data1: '#259de5',
}, },
axis: { axis: {
x: { x: {
min: 0, min: gasPrices[0],
max: 1, max: gasPricesMax,
tick: { tick: {
values: ['0', '1.00'], values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)],
outer: false, outer: false,
format: val => val === '0' ? val : '$' + val, format: function (val) { return val + ' GWEI' },
}, },
padding: {left: 0.005, right: 0}, padding: {left: gasPricesMax / 50, right: gasPricesMax / 50},
label: { label: {
text: 'Gas Price ($)', text: 'Gas Price ($)',
position: 'outer-center', position: 'outer-center',
}, },
}, },
y: { y: {
padding: {top: 2, bottom: 0}, padding: {top: 7, bottom: 7},
tick: { tick: {
values: ['5', '97'], values: [Math.floor(estimatedTimesMax * 0.05), Math.ceil(estimatedTimesMax * 0.97)],
outer: false, outer: false,
format: val => val === '5' ? '0' : '100',
}, },
label: { label: {
text: 'Confirmation time (sec)', text: 'Confirmation time (sec)',
position: 'outer-middle', position: 'outer-middle',
}, },
min: 0,
}, },
}, },
legend: { legend: {
show: false, show: false,
}, },
grid: { grid: {
x: { x: {},
lines: [ lines: {
{value: 0.0833}, front: false,
{value: 0.1667}, },
{value: 0.2500},
{value: 0.3333},
{value: 0.4167},
{value: 0.5000},
{value: 0.5833},
{value: 0.6667},
{value: 0.7500},
{value: 0.8333},
{value: 0.9167},
{value: 1.0000},
],
},
lines: {
front: false,
},
}, },
point: { point: {
focus: { focus: {
expand: { expand: {
enabled: true, enabled: false,
r: 3.5, r: 3.5,
}, },
}, },
}, },
tooltip: { tooltip: {
format: {
title: (v) => v.toPrecision(4),
},
contents: function (d, defaultTitleFormat, defaultValueFormat, color) { contents: function (d, defaultTitleFormat, defaultValueFormat, color) {
const $$ = this const config = this.config
const config = $$.config
const titleFormat = config.tooltip_format_title || defaultTitleFormat const titleFormat = config.tooltip_format_title || defaultTitleFormat
let text
let text, title let title
d.forEach(n => { d.forEach(el => {
if (n && (n.value || n.value === 0)) { if (el && (el.value || el.value === 0) && !text) {
title = titleFormat ? titleFormat(el.x) : el.x
if (!text) { text = "<table class='" + 'custom-tooltip' + "'>" + (title || title === 0 ? "<tr><th colspan='2'>" + title + '</th></tr>' : '')
title = titleFormat ? titleFormat(n.x) : n.x
text = "<table class='" + 'custom-tooltip' + "'>" + (title || title === 0 ? "<tr><th colspan='2'>" + title + '</th></tr>' : '')
}
} }
}) })
// for (i = 0; i < d.length; i++) {
// if (! (d[i] && (d[i].value || d[i].value === 0))) { continue; }
// if (! text) {
// title = titleFormat ? titleFormat(d[i].x) : d[i].x;
// text = "<table class='" + 'custom-tooltip' + "'>" + (title || title === 0 ? "<tr><th colspan='2'>" + title + "</th></tr>" : "");
// }
// }
return text + '</table>' + "<div class='tooltip-arrow'></div>" return text + '</table>' + "<div class='tooltip-arrow'></div>"
}, },
position: function (data, width, height, element) { position: function (data, width, height, element) {
const x = d3.event.pageX - document.getElementById('chart').getBoundingClientRect().x + 19 const overlayedCircle = d3.select('#overlayed-circle')
const y = d3.event.pageY - document.getElementById('chart').getBoundingClientRect().y + 20 if (overlayedCircle.empty()) {
return {top: y, left: x} return { top: -100, left: -100 }
}
const { x: circleX, y: circleY, width: circleWidth } = overlayedCircle.node().getBoundingClientRect()
const { x: chartXStart, y: chartYStart } = d3.select('.c3-chart').node().getBoundingClientRect()
// TODO: Confirm the below constants work with all data sets and screen sizes
// TODO: simplify l149-l159
let y = circleY - chartYStart - 19
if (circleY - circleWidth < chartYStart + 5) {
y = y + circleWidth + 38
d3.select('.tooltip-arrow').style('margin-top', '-16px')
} else {
d3.select('.tooltip-arrow').style('margin-top', '4px')
}
return {
top: y,
left: circleX - chartXStart + circleWidth - (gasPricesMax / 50),
}
}, },
show: true,
}, },
}) })
setTimeout(() => { chart.internal.selectPoint = function (data, itemIndex = (data.index || 0)) {
setTickPosition('y', 0, -5, 2) const { x: circleX, y: circleY, width: circleWidth } = d3.select('#overlayed-circle')
.node()
.getBoundingClientRect()
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: () => circleX - chartXStart + circleWidth + 2,
cy: () => circleY - chartYStart + circleWidth + 1,
cssId: 'set-circle',
appendOnly: true,
})
}
chart.internal.overlayPoint = function (data, itemIndex) {
const circle = this.main
.select('.' + 'c3-selected-circles' + this.getTargetSelectorSuffix(data.id))
.selectAll('.' + 'c3-selected-circle' + '-' + itemIndex)
appendOrUpdateCircle.bind(this)({
circle,
data,
itemIndex,
cx: this.circleX.bind(this),
cy: this.circleY.bind(this),
cssId: 'overlayed-circle',
})
}
chart.internal.setCurrentCircle = function (data, itemIndex) {
const circle = this.main
.select('.' + 'c3-selected-circles' + this.getTargetSelectorSuffix(data.id))
.selectAll('#current-circle')
appendOrUpdateCircle.bind(this)({
circle,
data,
itemIndex,
cx: this.circleX.bind(this),
cy: this.circleY.bind(this),
cssId: 'current-circle',
})
}
chart.internal.showTooltip = function (selectedData, element) {
const $$ = this
const config = $$.config
const forArc = $$.hasArcType()
const dataToShow = selectedData.filter((d) => d && (d.value || d.value === 0))
const positionFunction = config.tooltip_position || chart.internal.prototype.tooltipPosition
if (dataToShow.length === 0 || !config.tooltip_show) {
return
}
$$.tooltip.html(config.tooltip_contents.call($$, selectedData, $$.axis.getXAxisTickFormat(), $$.getYFormat(forArc), $$.color)).style('display', 'flex')
// Get tooltip dimensions
const tWidth = $$.tooltip.property('offsetWidth')
const tHeight = $$.tooltip.property('offsetHeight')
const position = positionFunction.call(this, dataToShow, tWidth, tHeight, element)
// Set tooltip
$$.tooltip.style('top', position.top + 'px').style('left', position.left + 'px')
}
setTimeout(function () {
setTickPosition('y', 0, -5, 8)
setTickPosition('y', 1, -3) setTickPosition('y', 1, -3)
setTickPosition('x', 0, 3) setTickPosition('x', 0, 3, 20)
setTickPosition('x', 1, 3, -5) setTickPosition('x', 1, 3, -10)
// 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(42, 2) rotate(-90)') d3.select('.c3-axis-y-label').attr('transform', 'translate(32, 2) rotate(-90)')
d3.select('.c3-xgrid-focus line').attr('y2', 98)
d3.select('.c3-chart').on('mouseout', () => {
const overLayedCircle = d3.select('#overlayed-circle')
if (!overLayedCircle.empty()) {
overLayedCircle.remove()
}
d3.select('.c3-tooltip-container').style('display', 'none !important')
})
const chartRect = d3.select('.c3-areas-data1')
const { x: chartXStart, width: chartWidth } = chartRect.node().getBoundingClientRect()
d3.select('.c3-chart').on('click', () => {
const overlayedCircle = d3.select('#overlayed-circle')
const numberOfValues = chart.internal.data.xs.data1.length
const { x: circleX, y: circleY } = overlayedCircle.node().getBoundingClientRect()
chart.internal.selectPoint({
x: circleX - chartXStart,
value: circleY - 1.5,
id: 'data1',
index: numberOfValues,
name: 'data1',
}, numberOfValues)
})
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
})
const closestLowerValue = gasPrices[closestLowerValueIndex]
const estimatedClosestLowerTimeEstimate = estimatedTimes[closestLowerValueIndex]
const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => {
return e > currentPosValue
})
const closestHigherValue = gasPrices[closestHigherValueIndex]
if (!closestHigherValue || !closestLowerValue) {
const overLayedCircle = d3.select('#overlayed-circle')
if (!overLayedCircle.empty()) {
overLayedCircle.remove()
}
d3.select('.c3-tooltip-container').style('display', 'none !important')
chart.internal.hideXGridFocus()
return
}
const estimatedClosestHigherTimeEstimate = estimatedTimes[closestHigherValueIndex]
const slope = (estimatedClosestHigherTimeEstimate - estimatedClosestLowerTimeEstimate) / (closestHigherValue - closestLowerValue)
const newTimeEstimate = -1 * (slope * (closestHigherValue - currentPosValue) - estimatedClosestHigherTimeEstimate)
const newEstimatedTimes = [...estimatedTimes, newTimeEstimate]
chart.internal.overlayPoint({
x: currentPosValue,
value: newTimeEstimate,
id: 'data1',
index: newEstimatedTimes.length,
name: 'data1',
}, newEstimatedTimes.length)
chart.internal.showTooltip([{
x: currentPosValue,
value: newTimeEstimate,
id: 'data1',
index: newEstimatedTimes.length,
name: 'data1',
}], chartRect._groups[0])
chart.internal.showXGridFocus([{
x: currentPosValue,
value: newTimeEstimate,
id: 'data1',
index: newEstimatedTimes.length,
name: 'data1',
}])
})
}, 0) }, 0)
} }
componentDidMount () { componentDidMount () {
this.renderChart() this.renderChart(this.props.priceAndTimeEstimates)
} }
render () { render () {

View File

@ -2,6 +2,13 @@
display: flex; display: flex;
position: relative; position: relative;
&__root {
max-height: 154px;
max-width: 391px;
position: relative;
overflow: hidden;
}
.tick text, .c3-axis-x-label, .c3-axis-y-label { .tick text, .c3-axis-x-label, .c3-axis-y-label {
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
@ -12,45 +19,61 @@
fill: #9A9CA6 !important; fill: #9A9CA6 !important;
} }
.c3-tooltip-container {
display: flex;
justify-content: center !important;
align-items: flex-end !important;
}
.custom-tooltip { .custom-tooltip {
background: rgba(0, 0, 0, 1); background: rgba(0, 0, 0, 1);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 3px; border-radius: 3px;
transform: translate(-41px, -50px);
opacity: 1 !important; opacity: 1 !important;
width: 44px;
height: 21px; height: 21px;
z-index: 1; z-index: 1;
} }
.tooltip-arrow { .tooltip-arrow {
background: rgba(0, 0, 0, 1); background: black;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5); box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5);
top: -35px; /* top: 15px; */
left: -23px; /* left: 27px; */
-webkit-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
opacity: 1 !important; opacity: 1 !important;
width: 9px; width: 9px;
height: 9px; height: 9px;
position: absolute; /* position: absolute; */
display: inline-block; /* display: inline-block; */
margin-top: 4px;
} }
.custom-tooltip th { .custom-tooltip th {
font-family: Roboto; font-family: Roboto;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
line-height: normal; line-height: normal;
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
padding: 3px;
color: #FFFFFF; color: #FFFFFF;
} }
.c3-circle._expanded_ { .c3-circle {
visibility: hidden;
}
.c3-selected-circle, .c3-circle._expanded_ {
fill: #FFFFFF !important; fill: #FFFFFF !important;
stroke-width: 2.4px !important; stroke-width: 2.4px !important;
stroke: #2d9fd9 !important; stroke: #2d9fd9 !important;
/* visibility: visible; */
}
#set-circle {
fill: #313A5E !important;
stroke: #313A5E !important;
} }
.c3-axis-x-label, .c3-axis-y-label { .c3-axis-x-label, .c3-axis-y-label {
@ -84,7 +107,9 @@
stroke: #B8B8B8 !important; stroke: #B8B8B8 !important;
} }
.c3-axis .tick line {display: none;} .c3-xgrid-focus {
stroke: #aaa;
}
.c3-axis-x .domain { .c3-axis-x .domain {
fill: none; fill: none;
@ -95,6 +120,10 @@
fill: none; fill: none;
stroke: #C8CCD6; stroke: #C8CCD6;
} }
.c3-event-rect {
cursor: pointer;
}
} }
@import url(//fonts.googleapis.com/css?family=Roboto:400,700,300); @import url(//fonts.googleapis.com/css?family=Roboto:400,700,300);