From 6f0406125d9d656a4a9826b616f28ffda894ec8e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 23 Oct 2018 14:40:27 -0230 Subject: Clean up gas chart code. --- .../gas-price-chart/gas-price-chart.utils.js | 307 +++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js (limited to 'ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js') diff --git a/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js new file mode 100644 index 000000000..43bbfd9ab --- /dev/null +++ b/ui/app/components/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -0,0 +1,307 @@ +import * as d3 from 'd3' +import c3 from 'c3' + +export function getCoordinateData (selector) { + return d3.select(selector).node().getBoundingClientRect() +} + +export function generateDataUIObj (x, index, value) { + return { + x, + value, + index, + id: 'data1', + name: 'data1', + } +} + +export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) { + const { + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: newPrice }) + + if (closestLowerValue && closestHigherValue) { + setSelectedCircle({ + chart, + newPrice, + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, + }) + } else { + hideDataUI(chart, cssId) + } +} + +export function getAdjacentGasPrices({ gasPrices, priceToPosition }) { + const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) + const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => e > priceToPosition) + return { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue: gasPrices[closestHigherValueIndex], + closestLowerValue: gasPrices[closestLowerValueIndex], + } +} + +export function extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation }) { + const slope = (higherY - lowerY) / (higherX - lowerX) + const newTimeEstimate = -1 * (slope * (higherX - xForExtrapolation) - higherY) + + return newTimeEstimate +} + + +export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) { + const chartMouseXPos = xMousePos - chartXStart + const posPercentile = chartMouseXPos / chartWidth + + const currentPosValue = (gasPrices[gasPrices.length - 1] - gasPrices[0]) * posPercentile + gasPrices[0] + + const { + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: currentPosValue }) + + return !closestHigherValue || !closestLowerValue + ? { + currentPosValue: null, + newTimeEstimate: null, + } + : { + currentPosValue, + newTimeEstimate: extrapolateY ({ + higherY: estimatedTimes[closestHigherValueIndex], + lowerY: estimatedTimes[closestLowerValueIndex], + higherX: closestHigherValue, + lowerX: closestLowerValue, + xForExtrapolation: currentPosValue, + }), + } +} + +export function hideDataUI (chart, dataNodeId) { + const overLayedCircle = d3.select(dataNodeId) + if (!overLayedCircle.empty()) { + overLayedCircle.remove() + } + d3.select('.c3-tooltip-container').style('display', 'none !important') + chart.internal.hideXGridFocus() +} + +export function setTickPosition (axis, n, newPosition, secondNewPosition) { + const positionToShift = axis === 'y' ? 'x' : 'y' + const secondPositionToShift = axis === 'y' ? 'y' : 'x' + d3.select('#chart') + .select(`.c3-axis-${axis}`) + .selectAll('.tick') + .filter((d, i) => i === n) + .select('text') + .attr(positionToShift, 0) + .select('tspan') + .attr(positionToShift, newPosition) + .attr(secondPositionToShift, secondNewPosition || 0) + .style('visibility', 'visible') +} + +export function appendOrUpdateCircle ({ data, itemIndex, cx, cy, cssId, appendOnly }) { + const circle = this.main + .select('.' + 'c3-selected-circles' + this.getTargetSelectorSuffix(data.id)) + .selectAll('.' + 'c3-selected-circle' + '-' + itemIndex) + + 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 function setSelectedCircle ({ + chart, + newPrice, + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, +}) { + const numberOfValues = chart.internal.data.xs.data1.length + const { x: lowerX, y: lowerY } = getCoordinateData(`.c3-circle-${closestLowerValueIndex}`) + const { x: higherX, y: higherY } = getCoordinateData(`.c3-circle-${closestHigherValueIndex}`) + + const currentX = lowerX + (higherX - lowerX) * (newPrice - closestLowerValue) / (closestHigherValue - closestLowerValue) + const newTimeEstimate = extrapolateY ({ higherY, lowerY, higherX, lowerX, xForExtrapolation: currentX }) + + chart.internal.selectPoint( + generateDataUIObj(currentX, numberOfValues, newTimeEstimate), + numberOfValues + ) +} + + +export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) { + const chart = c3.generate({ + size: { + height: 165, + }, + transition: { + duration: 0, + }, + padding: {left: 20, right: 15, top: 6, bottom: 10}, + data: { + x: 'x', + columns: [ + ['x', ...gasPrices], + ['data1', ...estimatedTimes], + ], + types: { + data1: 'area', + }, + selection: { + enabled: false, + }, + }, + color: { + data1: '#259de5', + }, + axis: { + x: { + min: gasPrices[0], + max: gasPricesMax, + tick: { + values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)], + outer: false, + format: function (val) { return val + ' GWEI' }, + }, + padding: {left: gasPricesMax / 50, right: gasPricesMax / 50}, + label: { + text: 'Gas Price ($)', + position: 'outer-center', + }, + }, + y: { + padding: {top: 7, bottom: 7}, + tick: { + values: [Math.floor(estimatedTimesMax * 0.05), Math.ceil(estimatedTimesMax * 0.97)], + outer: false, + }, + label: { + text: 'Confirmation time (sec)', + position: 'outer-middle', + }, + min: 0, + }, + }, + legend: { + show: false, + }, + grid: { + x: {}, + lines: { + front: false, + }, + }, + point: { + focus: { + expand: { + enabled: false, + r: 3.5, + }, + }, + }, + tooltip: { + format: { + title: (v) => v.toPrecision(4), + }, + contents: function (d) { + const titleFormat = this.config.tooltip_format_title + let text + d.forEach(el => { + if (el && (el.value || el.value === 0) && !text) { + text = "" + "' + } + }) + return text + '
" + titleFormat(el.x) + '
' + "
" + }, + position: function (data) { + if (d3.select('#overlayed-circle').empty()) { + return { top: -100, left: -100 } + } + + const { x: circleX, y: circleY, width: circleWidth } = getCoordinateData('#overlayed-circle') + const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-chart') + + // TODO: Confirm the below constants work with all data sets and screen sizes + const flipTooltip = circleY - circleWidth < chartYStart + 5 + + d3 + .select('.tooltip-arrow') + .style('margin-top', flipTooltip ? '-16px' : '4px') + + return { + top: circleY - chartYStart - 19 + (flipTooltip ? circleWidth + 38 : 0), + left: circleX - chartXStart + circleWidth - (gasPricesMax / 50), + } + }, + show: true, + }, + }) + + chart.internal.selectPoint = function (data, itemIndex = (data.index || 0)) { + const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-areas-data1') + + d3.select('#set-circle').remove() + + appendOrUpdateCircle.bind(this)({ + data, + itemIndex, + cx: () => data.x - chartXStart + 11, + cy: () => data.value - chartYStart + 10, + cssId: 'set-circle', + appendOnly: true, + }) + } + + chart.internal.overlayPoint = function (data, itemIndex) { + appendOrUpdateCircle.bind(this)({ + data, + itemIndex, + cx: this.circleX.bind(this), + cy: this.circleY.bind(this), + cssId: 'overlayed-circle', + }) + } + + chart.internal.showTooltip = function (selectedData, element) { + const dataToShow = selectedData.filter((d) => d && (d.value || d.value === 0)) + + if (dataToShow.length) { + this.tooltip.html( + this.config.tooltip_contents.call(this, selectedData, this.axis.getXAxisTickFormat(), this.getYFormat(), this.color) + ).style('display', 'flex') + + // Get tooltip dimensions + const tWidth = this.tooltip.property('offsetWidth') + const tHeight = this.tooltip.property('offsetHeight') + const position = this.config.tooltip_position.call(this, dataToShow, tWidth, tHeight, element) + // Set tooltip + this.tooltip.style('top', position.top + 'px').style('left', position.left + 'px') + } + } + + return chart +} -- cgit