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 3a62d21cc..67c1ff2e3 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 @@ -69,6 +69,10 @@ const mapStateToProps = state => { const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex) + const priceAndTimeEstimates = state.gas.priceAndTimeEstimates + const gasPrices = priceAndTimeEstimates.map(({ gasprice }) => gasprice) + const estimatedTimes = priceAndTimeEstimates.map(({ expectedTime }) => expectedTime) + return { hideBasic, isConfirm: isConfirm(state), @@ -77,7 +81,7 @@ const mapStateToProps = state => { customGasPrice, customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), newTotalFiat, - currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, state.gas.priceAndTimeEstimates), + currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, priceAndTimeEstimates), gasPriceButtonGroupProps: { buttonDataLoading, defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), @@ -85,7 +89,10 @@ const mapStateToProps = state => { }, gasChartProps: { currentPrice: customGasPrice, - priceAndTimeEstimates: state.gas.priceAndTimeEstimates, + gasPrices, + estimatedTimes, + gasPricesMax: gasPrices[gasPrices.length - 1] + 1, + estimatedTimesMax: estimatedTimes[0], }, infoRowProps: { originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate), 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 69bbd12f6..69f8b3a91 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 @@ -2,75 +2,18 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import * as d3 from 'd3' import c3 from 'c3' - -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') -} - -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) - } -} - -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 - } -} +import { + appendOrUpdateCircle, + generateChart, + generateDataUIObj, + getAdjacentGasPrices, + getCoordinateData, + getNewXandTimeEstimate, + handleChartUpdate, + hideDataUI, + setSelectedCircle, + setTickPosition, +} from './gas-price-chart.utils.js' export default class GasPriceChart extends Component { static contextTypes = { @@ -78,225 +21,23 @@ export default class GasPriceChart extends Component { } static propTypes = { - priceAndTimeEstimates: PropTypes.array, + gasPrices: PropTypes.array, + estimatedTimes: PropTypes.array, + gasPricesMax: PropTypes.number, + estimatedTimesMax: PropTypes.number, currentPrice: PropTypes.number, updateCustomGasPrice: PropTypes.func, } - 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: { - 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, defaultTitleFormat, defaultValueFormat, color) { - const config = this.config - const titleFormat = config.tooltip_format_title || defaultTitleFormat - 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 === 0 ? "' : '') - } - }) - return text + '
" + title + '
' + "
" - }, - position: function (data, width, height, element) { - 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, - }, - }) - - 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.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)) - .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') - } + renderChart ({ + currentPrice, + gasPrices, + estimatedTimes, + gasPricesMax, + estimatedTimesMax, + updateCustomGasPrice, + }) { + const chart = generateChart(gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) setTimeout(function () { setTickPosition('y', 0, -5, 8) @@ -310,84 +51,42 @@ export default class GasPriceChart extends Component { 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') + hideDataUI(chart, '#overlayed-circle') }) - 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() - const { x: xData } = overlayedCircle.datum() - chart.internal.selectPoint({ - x: circleX - chartXStart, - value: circleY - 1.5, - id: 'data1', - index: numberOfValues, - name: 'data1', - }, numberOfValues) - updateCustomGasPrice(xData) + const { x: newGasPrice } = d3.select('#overlayed-circle').datum() + updateCustomGasPrice(newGasPrice) }) - setSelectedCircle({ chart, gasPrices, currentPrice, chartXStart, chartWidth }) + const { x: chartXStart, width: chartWidth } = getCoordinateData('.c3-areas-data1') - d3.select('.c3-chart').on('mousemove', function () { - const chartMouseXPos = d3.event.clientX - chartXStart - const posPercentile = chartMouseXPos / chartWidth + handleChartUpdate ({ + chart, + gasPrices, + newPrice: currentPrice, + cssId: '#set-circle', + }) - 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 + d3.select('.c3-chart').on('mousemove', function () { + const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({ + xMousePos: d3.event.clientX, + chartXStart, + chartWidth, + gasPrices, + estimatedTimes, }) - 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 + if (currentPosValue === null && newTimeEstimate === null) { + hideDataUI(chart, '#overlayed-circle') } - const estimatedClosestHigherTimeEstimate = estimatedTimes[closestHigherValueIndex] - const slope = (estimatedClosestHigherTimeEstimate - estimatedClosestLowerTimeEstimate) / (closestHigherValue - closestLowerValue) - const newTimeEstimate = -1 * (slope * (closestHigherValue - currentPosValue) - estimatedClosestHigherTimeEstimate) + const indexOfNewCircle = estimatedTimes.length + 1 + const dataUIObj = generateDataUIObj(currentPosValue, indexOfNewCircle, newTimeEstimate) - 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', - }]) + chart.internal.overlayPoint(dataUIObj, indexOfNewCircle) + chart.internal.showTooltip([dataUIObj], d3.select('.c3-areas-data1')._groups[0]) + chart.internal.showXGridFocus([dataUIObj]) }) }, 0) @@ -395,22 +94,20 @@ export default class GasPriceChart extends Component { } componentDidUpdate (prevProps) { - if (prevProps.currentPrice !== this.props.currentPrice) { - const chartRect = d3.select('.c3-areas-data1') - const { x: chartXStart, width: chartWidth } = chartRect.node().getBoundingClientRect() - setSelectedCircle({ + const { gasPrices, currentPrice: newPrice } = this.props + + if (prevProps.currentPrice !== newPrice) { + handleChartUpdate ({ chart: this.chart, - currentPrice: this.props.currentPrice, - gasPrices: this.props.priceAndTimeEstimates.map(({ gasprice }) => gasprice), - chartXStart, - chartWidth, + gasPrices, + newPrice, + cssId: '#set-circle', }) } } componentDidMount () { - const { currentPrice, priceAndTimeEstimates, updateCustomGasPrice } = this.props - this.renderChart(currentPrice, priceAndTimeEstimates, updateCustomGasPrice) + this.renderChart(this.props) } render () { 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 +}