From 2dbae581ac3f9190ddd1c3457bd51b41eef8051b Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 3 Oct 2018 10:50:05 -0230 Subject: Gas price chart improvements, redesign, bug fixes, and set up to receive external data --- .../gas-price-chart/gas-price-chart.component.js | 318 ++++++++++++++++----- .../gas-customization/gas-price-chart/index.scss | 65 +++-- 2 files changed, 296 insertions(+), 87 deletions(-) (limited to 'ui/app/components/gas-customization/gas-price-chart') 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 7dadafa95..85893f771 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 @@ -18,139 +18,319 @@ function setTickPosition (axis, n, newPosition, secondNewPosition) { .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 { static contextTypes = { t: PropTypes.func, } static propTypes = { + priceAndTimeEstimates: PropTypes.array, } - renderChart () { - c3.generate({ + renderChart (priceAndTimeEstimates) { + 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: { - 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: { - x: 'x', - 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], - ['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], - ], - types: { - data1: 'area', - }, + x: 'x', + columns: [ + ['x', ...gasPrices], + ['data1', ...estimatedTimes], + ], + types: { + data1: 'area', + }, + selection: { + enabled: false, + }, }, color: { data1: '#259de5', }, axis: { x: { - min: 0, - max: 1, + min: gasPrices[0], + max: gasPricesMax, tick: { - values: ['0', '1.00'], + values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)], 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: { text: 'Gas Price ($)', position: 'outer-center', }, }, y: { - padding: {top: 2, bottom: 0}, + padding: {top: 7, bottom: 7}, tick: { - values: ['5', '97'], + values: [Math.floor(estimatedTimesMax * 0.05), Math.ceil(estimatedTimesMax * 0.97)], outer: false, - format: val => val === '5' ? '0' : '100', }, label: { text: 'Confirmation time (sec)', position: 'outer-middle', }, + min: 0, }, }, legend: { - show: false, + show: false, }, grid: { - x: { - lines: [ - {value: 0.0833}, - {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, - }, + x: {}, + lines: { + front: false, + }, }, point: { focus: { expand: { - enabled: true, + enabled: false, r: 3.5, }, }, }, tooltip: { + format: { + title: (v) => v.toPrecision(4), + }, contents: function (d, defaultTitleFormat, defaultValueFormat, color) { - const $$ = this - const config = $$.config + const config = this.config const titleFormat = config.tooltip_format_title || defaultTitleFormat - - let text, title - d.forEach(n => { - if (n && (n.value || n.value === 0)) { - - if (!text) { - title = titleFormat ? titleFormat(n.x) : n.x - text = "" + (title || title === 0 ? "' : '') - } + let text + let title + d.forEach(el => { + if (el && (el.value || el.value === 0) && !text) { + title = titleFormat ? titleFormat(el.x) : el.x + text = "
" + title + '
" + (title || title === 0 ? "' : '') } }) - // 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 = "
" + title + '
" + (title || title === 0 ? "" : ""); - // } - // } return text + '
" + title + "
' + "
" }, position: function (data, width, height, element) { - const x = d3.event.pageX - document.getElementById('chart').getBoundingClientRect().x + 19 - const y = d3.event.pageY - document.getElementById('chart').getBoundingClientRect().y + 20 - return {top: y, left: x} + const overlayedCircle = d3.select('#overlayed-circle') + if (overlayedCircle.empty()) { + 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(() => { - setTickPosition('y', 0, -5, 2) + chart.internal.selectPoint = function (data, itemIndex = (data.index || 0)) { + 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('x', 0, 3) - setTickPosition('x', 1, 3, -5) + setTickPosition('x', 0, 3, 20) + 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-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) + + } componentDidMount () { - this.renderChart() + this.renderChart(this.props.priceAndTimeEstimates) } render () { diff --git a/ui/app/components/gas-customization/gas-price-chart/index.scss b/ui/app/components/gas-customization/gas-price-chart/index.scss index bfe9b807b..4c4640b1f 100644 --- a/ui/app/components/gas-customization/gas-price-chart/index.scss +++ b/ui/app/components/gas-customization/gas-price-chart/index.scss @@ -2,6 +2,13 @@ display: flex; position: relative; + &__root { + max-height: 154px; + max-width: 391px; + position: relative; + overflow: hidden; + } + .tick text, .c3-axis-x-label, .c3-axis-y-label { font-family: Roboto; font-style: normal; @@ -12,45 +19,61 @@ fill: #9A9CA6 !important; } + .c3-tooltip-container { + display: flex; + justify-content: center !important; + align-items: flex-end !important; + } + .custom-tooltip { background: rgba(0, 0, 0, 1); - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); - border-radius: 3px; - transform: translate(-41px, -50px); + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + border-radius: 3px; opacity: 1 !important; - width: 44px; height: 21px; z-index: 1; } .tooltip-arrow { - background: rgba(0, 0, 0, 1); - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5); - top: -35px; - left: -23px; + background: black; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5); + /* top: 15px; */ + /* left: 27px; */ + -webkit-transform: rotate(45deg); transform: rotate(45deg); opacity: 1 !important; width: 9px; height: 9px; - position: absolute; - display: inline-block; + /* position: absolute; */ + /* display: inline-block; */ + margin-top: 4px; } .custom-tooltip th { font-family: Roboto; - font-style: normal; - font-weight: 500; - line-height: normal; - font-size: 10px; - text-align: center; + font-style: normal; + font-weight: 500; + line-height: normal; + font-size: 10px; + text-align: center; + padding: 3px; + color: #FFFFFF; + } - color: #FFFFFF; + .c3-circle { + visibility: hidden; } - .c3-circle._expanded_ { + .c3-selected-circle, .c3-circle._expanded_ { fill: #FFFFFF !important; stroke-width: 2.4px !important; stroke: #2d9fd9 !important; + /* visibility: visible; */ + } + + #set-circle { + fill: #313A5E !important; + stroke: #313A5E !important; } .c3-axis-x-label, .c3-axis-y-label { @@ -84,7 +107,9 @@ stroke: #B8B8B8 !important; } - .c3-axis .tick line {display: none;} + .c3-xgrid-focus { + stroke: #aaa; + } .c3-axis-x .domain { fill: none; @@ -95,6 +120,10 @@ fill: none; stroke: #C8CCD6; } + + .c3-event-rect { + cursor: pointer; + } } @import url(//fonts.googleapis.com/css?family=Roboto:400,700,300); -- cgit