// Copyright 2015, 2016 Ethcore (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; import { Bar, BarChart, Rectangle, Scatter, ScatterChart, Tooltip, XAxis, YAxis, Dot, ResponsiveContainer } from 'recharts'; import Slider from 'material-ui/Slider'; import BigNumber from 'bignumber.js'; import styles from './gasPriceSelector.css'; const COLORS = { default: 'rgba(255, 99, 132, 0.2)', selected: 'rgba(255, 99, 132, 0.5)', hover: 'rgba(255, 99, 132, 0.15)', grid: 'rgba(255, 99, 132, 0.5)', line: 'rgb(255, 99, 132)', intersection: '#fff' }; const countModifier = (count) => { const val = count.toNumber ? count.toNumber() : count; return Math.log10(val + 1) + 0.1; }; class CustomCursor extends Component { static propTypes = { x: PropTypes.number, y: PropTypes.number, width: PropTypes.number, height: PropTypes.number, onClick: PropTypes.func, getIndex: PropTypes.func, counts: PropTypes.array, yDomain: PropTypes.array } render () { const { x, y, width, height, getIndex, counts, yDomain } = this.props; const index = getIndex(); if (index === -1) { return null; } const count = countModifier(counts[index]); const barHeight = (count / yDomain[1]) * (y + height); return ( ); } onClick = () => { const { onClick, getIndex } = this.props; const index = getIndex(); onClick({ index }); } } class CustomBar extends Component { static propTypes = { selected: PropTypes.number, x: PropTypes.number, y: PropTypes.number, width: PropTypes.number, height: PropTypes.number, index: PropTypes.number, onClick: PropTypes.func } render () { const { x, y, selected, index, width, height, onClick } = this.props; const fill = selected === index ? COLORS.selected : COLORS.default; const borderWidth = 0.5; const borderColor = 'rgba(255, 255, 255, 0.5)'; return ( ); } } class CustomizedShape extends Component { static propTypes = { showValue: PropTypes.number.isRequired, cx: PropTypes.number, cy: PropTypes.number, payload: PropTypes.object } render () { const { cx, cy, showValue, payload } = this.props; if (showValue !== payload.y) { return null; } return ( ); } } class CustomTooltip extends Component { static propTypes = { gasPriceHistogram: PropTypes.object.isRequired, type: PropTypes.string, payload: PropTypes.array, label: PropTypes.number, active: PropTypes.bool } render () { const { active, label, gasPriceHistogram } = this.props; if (!active) { return null; } const index = label; const count = gasPriceHistogram.counts[index]; const minGasPrice = gasPriceHistogram.bucketBounds[index]; const maxGasPrice = gasPriceHistogram.bucketBounds[index + 1]; return (

{ count.toNumber() } transactions with gas price set from { minGasPrice.toFormat(0) } to { maxGasPrice.toFormat(0) }

); } } const TOOL_STYLE = { color: 'rgba(255,255,255,0.5)', backgroundColor: 'rgba(0, 0, 0, 0.75)', padding: '0 0.5em', fontSize: '0.75em' }; export default class GasPriceSelector extends Component { static propTypes = { gasPriceHistogram: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, gasPrice: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]) } state = { gasPrice: null, sliderValue: 0.5, selectedIndex: 0, chartData: { values: [], xDomain: [], yDomain: [], N: 0 } } componentWillMount () { this.computeCharts(); this.setGasPrice(); } componentWillReceiveProps (nextProps) { if (nextProps.gasPrice !== this.props.gasPrice) { this.setGasPrice(nextProps); } } componentWillUpdate (nextProps, nextState) { if (Math.floor(nextState.sliderValue) !== Math.floor(this.state.sliderValue)) { this.updateSelectedBarChart(nextState); } } render () { return (
{ this.renderChart() } { this.renderSlider() }
); } renderSlider () { const { sliderValue } = this.state; return (
); } renderChart () { const { gasPriceHistogram } = this.props; const { chartData, sliderValue, selectedIndex } = this.state; if (chartData.values.length === 0) { return null; } const height = 300; const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1)); const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]); return (
} line isAnimationActive={ false } />
} /> } />
); } renderCustomCursor = () => { const { gasPriceHistogram } = this.props; const { chartData } = this.state; return ( ); } getBarHoverIndex = () => { const { barChart } = this.refs; if (!barChart || !barChart.state) { return -1; } return barChart.state.activeTooltipIndex; } computeChartsData () { const { gasPriceChartData } = this.state; const { gasPriceHistogram } = this.props; const values = gasPriceChartData .map((value, index) => ({ value, index })); const N = values.length - 1; const maxGasCounts = countModifier( gasPriceHistogram .counts .reduce((max, count) => count.greaterThan(max) ? count : max, 0) ); const xDomain = [0, N]; const yDomain = [0, maxGasCounts * 1.1]; const chartData = { values, N, xDomain, yDomain }; this.setState({ chartData }, () => { this.updateSelectedBarChart(); }); } computeCharts (props = this.props) { const { gasPriceHistogram } = props; const gasPriceChartData = gasPriceHistogram .counts .map(count => countModifier(count)); this.setState( { gasPriceChartData }, () => this.computeChartsData() ); } updateSelectedBarChart (state = this.state) { } setGasPrice (props = this.props) { const { gasPrice, gasPriceHistogram } = props; // If no gas price yet... if (!gasPrice) { return this.setSliderValue(0.5); } const bnGasPrice = (typeof gasPrice === 'string') ? new BigNumber(gasPrice) : gasPrice; // If gas price hasn't changed if (this.state.gasPrice && bnGasPrice.equals(this.state.gasPrice)) { return; } const gasPrices = gasPriceHistogram.bucketBounds; const startIndex = gasPrices .findIndex(price => price.greaterThan(bnGasPrice)) - 1; // gasPrice Lower than the max in histogram if (startIndex === -1) { return this.setSliderValue(0, bnGasPrice); } // gasPrice Greater than the max in histogram if (startIndex === -2) { return this.setSliderValue(1, bnGasPrice); } const priceA = gasPrices[startIndex]; const priceB = gasPrices[startIndex + 1]; const sliderValueDec = bnGasPrice .minus(priceA) .dividedBy(priceB.minus(priceA)) .toNumber(); const sliderValue = (startIndex + sliderValueDec) / (gasPrices.length - 1); this.setSliderValue(sliderValue, bnGasPrice); } setSliderValue (value, gasPrice = this.state.gasPrice) { const { gasPriceHistogram } = this.props; const N = gasPriceHistogram.bucketBounds.length - 1; const sliderValue = Math.max(0, Math.min(value, 1)); const selectedIndex = Math.floor(sliderValue * N); this.setState({ sliderValue, gasPrice, selectedIndex }); } onBarChartMouseUp = (event) => { console.log(event); } onClickGasPrice = (bar) => { const { index } = bar; const ratio = (index + 0.5) / (this.state.chartData.N + 1); this.onEditGasPriceSlider(null, ratio); } onEditGasPriceSlider = (event, sliderValue) => { const { gasPriceHistogram } = this.props; const gasPrices = gasPriceHistogram.bucketBounds; const N = gasPrices.length - 1; const gasPriceAIdx = Math.floor(sliderValue * N); const gasPriceBIdx = gasPriceAIdx + 1; if (gasPriceBIdx === N + 1) { const gasPrice = gasPrices[gasPriceAIdx].round(); this.props.onChange(event, gasPrice); return; } const gasPriceA = gasPrices[gasPriceAIdx]; const gasPriceB = gasPrices[gasPriceBIdx]; const mult = Math.round((sliderValue % 1) * 100) / 100; const gasPrice = gasPriceA .plus(gasPriceB.minus(gasPriceA).times(mult)) .round(); this.setSliderValue(sliderValue, gasPrice); this.props.onChange(event, gasPrice); } }