question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Supply & Demand : Different data sets for each line + more verbose tooltip

See original GitHub issue

Do you want to request a feature or report a bug?

feature request

What is the current behavior?

Currently there is only one data array that contains several X points, each X point has two Y values (for I am drawing two lines).

data = [
  ...
  {sell: 8},
  {buy: 2, sell: 7},
  {buy: 4, sell: 6.5},
  {buy: 4.5, sell: 6.25},
  ...
]

image

What is the expected behavior?

I have this data: Notice I can transform sellingData.priceOne to sell and buyingData.priceOne to buy and construct this XPoint data array, but then I need to display the user, quantity and priceTotal fields in a tooltip on hover. That would require an extensive mapping function as the datasets are not of a same length.

sellingData = [
  {user: 'user1', quantity: 200, priceTotal: 1600, priceOne: 8},
  {user: 'user2', quantity: 200, priceTotal: 1400, priceOne: 7},
  {user: 'user3', quantity: 100, priceTotal: 650, priceOne: 6.5},
  {user: 'user4', quantity: 100, priceTotal: 625, priceOne: 6.25}
]

buyingData = [
  {user: 'user5', quantity: 1000, priceTotal: 2000, priceOne: 2},
  {user: 'user6', quantity: 100, priceTotal: 400, priceOne: 4},
  {user: 'user7', quantity: 100, priceTotal: 450, priceOne: 4.5}
]

and I want to see two converging lines that don’t meet - This is a supply and demand chart.

Then I want to display a tooltip with the user, quantity and priceTotal fields separately for each data line, however for that I would need

  1. I don’t have different tooltip for each line
  2. The payload gives me only the current XPoint data, that is the data mangled together.

In this picture: Hover over section with both buy and sell line; and only with sell line. image

Upon closer inspection, it is apparent that the payload is same for both lines in first case. (point 2.)

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:4
  • Comments:5

github_iconTop GitHub Comments

2reactions
ackvfcommented, Oct 27, 2017

findCloserLine… When points are drawn, I hijack their coordinates and store them into an array, so I can access them later, then when hovering over the chart, I use the mouse coordinates and find closest point (and it’s line). Given this information, I then select proper tooltip content.

Here is the app (please only play with data in this one linked table)

Example data

download json here

{
  "buy" : {
    "-KwPTLmvxx4oOeSSIKz1" : {
      "price" : "2",
      "priceTotal" : 400,
      "quantity" : 200,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwPTMVWJT0uoogWsRQL" : {
      "price" : "2.15",
      "priceTotal" : 430,
      "quantity" : 200,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwPTNZywyUIIvYUADJ6" : {
      "price" : "2.36",
      "priceTotal" : 236,
      "quantity" : 100,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwPTRd6DkD1HmB7iW-F" : {
      "price" : "2.02",
      "priceTotal" : 404,
      "quantity" : 200,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    }
  },
  "sell" : {
    "-KwPSp9b_2yA8odJYTXb" : {
      "price" : "3",
      "priceTotal" : 300,
      "quantity" : 100,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwPSpbLmia-dhsqYA7k" : {
      "price" : "2.83",
      "priceTotal" : 283,
      "quantity" : 100,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwPSqn9POMd1_rK0h-U" : {
      "price" : "3.11",
      "priceTotal" : 622,
      "quantity" : 200,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwPSsQaA16IF4fCDsyI" : {
      "price" : "3.42",
      "priceTotal" : 513,
      "quantity" : 150,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwPTKBf3xgkC_REzOwU" : {
      "price" : "3.07",
      "priceTotal" : 154,
      "quantity" : 50,
      "uid" : "EtFTmz1H7cNR83LWrbFIUkDKmdq1"
    },
    "-KwchyyR4mBbf2oOnpPW" : {
      "price" : "2.5",
      "priceTotal" : 50,
      "quantity" : 20,
      "uid" : "41qw2NQen7PnY7KI2qZeFkpKPnV2"
    },
    "-Kwu228W8h_02l2D3MT5" : {
      "price" : "2.4",
      "priceTotal" : 60,
      "quantity" : 25,
      "uid" : "41qw2NQen7PnY7KI2qZeFkpKPnV2"
    }
  }
}

Code

import React from 'react'
import firebase from 'firebase'
import { storeAuctionBid, setBidTimeout } from '../firebase-main'
import { ResponsiveContainer, LineChart, Line, YAxis, CartesianGrid, Tooltip, ReferenceLine } from 'recharts'
import { Button, Textfield, Slider, Snackbar, Spinner, RadioGroup, Radio } from 'react-mdl'
import { debounce } from '../utils'

import './itemAuction.css'

const debug = false


function insertInto(target, item) {
  const index = target.findIndex(el => el.price >= item.price)
  target.splice(index, 0, item)
}

function removeIn(target, item) {
  const index = target.findIndex(el => el.key === item.key)
  target.splice(index, 1)
}

function updateIn(target, item) {
  const index = target.findIndex(el => el.key === item.key)
  target[index] = {...target[index], ...item}
}


let buyData = [] // sorted ascending
let sellData = [] // sorted ascending

let points = [] // used for Tooltip text
let midPoints = [] // used for Tooltip text

// used for calculating Dot size
let maxSellQuantity = 0
let maxBuyQuantity = 0
let minSellQuantity = Number.MAX_SAFE_INTEGER
let minBuyQuantity = Number.MAX_SAFE_INTEGER

function resetData() {
  buyData = []
  sellData = []
}

function resetPoints() {
  points = []
  midPoints = []
  maxSellQuantity = 0
  maxBuyQuantity = 0
  minSellQuantity = Number.MAX_SAFE_INTEGER
  minBuyQuantity = Number.MAX_SAFE_INTEGER
}


const maxDotRadius = 6
const minDotRadius = 1.2
const strokeWidth = 0.3
let mouseY // really nasty, for ActiveDot

const green = '#66BB6A'
const red = '#EF5350'


export default class ItemAuction extends React.PureComponent {
  state = {
    data: [],
    selectedItem: {},
    bidPriceTotal: 0,
    bidPrice: undefined, // for chart ReferenceLine
    isSnackbarActive: false,
    sliderDefaultValue: undefined
  }

  firebaseRefBuy = null
  firebaseRefSell = null
  firebaseRefLastPush = null

  constructor(props) {
    super(props)

    const {item, tier, blueprint} = this.props.match.params
    const kindPath = blueprint ? 'blueprints' : 'items'

    this.firebaseRefBuy = firebase.database().ref(`auctions/${kindPath}/${item}/${tier}/buy`).orderByChild('price')
    this.firebaseRefSell = firebase.database().ref(`auctions/${kindPath}/${item}/${tier}/sell`).orderByChild('price')

    var subscribeToFirebaseRef = (ref, add, change, remove) => {
      // ref.on('value', snapshot => { ... //   // data not sorted here, useless
      ref.on('child_added',   snapshot => add    && (    add({...snapshot.val(), key: snapshot.key}), this.scheduleChartUpdate() ))
      ref.on('child_changed', snapshot => change && ( change({...snapshot.val(), key: snapshot.key}), this.scheduleChartUpdate() ))
      ref.on('child_removed', snapshot => remove && ( remove({...snapshot.val(), key: snapshot.key}), this.scheduleChartUpdate() ))
    }

    subscribeToFirebaseRef(this.firebaseRefBuy , this.addBuyData , this.changeBuyData , this.removeBuyData )
    subscribeToFirebaseRef(this.firebaseRefSell, this.addSellData, this.changeSellData, this.removeSellData)
  }

  componentWillUnmount() {
    this.firebaseRefBuy.off()
    this.firebaseRefSell.off()

    resetData()
    resetPoints()
  }

  addBuyData = data => {
    if (debug) console.debug('%cBUY ', 'background: #8BC34A; color: #000', data)
    if ((buyData[buyData.length-1] || {price:0}).price <= data.price) buyData.push(data)
    else insertInto(buyData, data)
  }

  addSellData = data => {
    if (debug) console.debug('%cSELL', 'background: #F44336; color: #000', data)
    if ((sellData[sellData.length-1] || {price:0}).price <= data.price) sellData.push(data)
    else insertInto(sellData, data)
  }

  changeBuyData  = data => updateIn(buyData,  data)
  changeSellData = data => updateIn(sellData, data)
  removeBuyData  = data => removeIn(buyData,  data)
  removeSellData = data => removeIn(sellData, data)

  scheduleChartUpdate = debounce(() => this.updateChart([...sellData], [...buyData]), 100)

  updateChart = (sellingData, buyingData) => {
    sellingData.reverse()
    resetPoints()
    const data = []

    while (sellingData.length || buyingData.length) {
      let sell = sellingData.pop()
      let buy = buyingData.pop()

      let XPoint = {}

      if (sell) {
        sell.price = Number(sell.price)
        XPoint.sellData = sell
        XPoint.sell = sell.price

        if (sell.quantity > maxSellQuantity) maxSellQuantity = sell.quantity
        if (sell.quantity < minSellQuantity) minSellQuantity = sell.quantity
        // if (sell.price > maxPrice) maxPrice = sell.price
      }

      if (buy) {
        buy.price = Number(buy.price)
        XPoint.buyData = buy
        XPoint.buy = buy.price

        if (buy.quantity > maxBuyQuantity) maxBuyQuantity = buy.quantity
        if (buy.quantity < minBuyQuantity) minBuyQuantity = buy.quantity
        // if (buy.price > maxPrice) maxPrice = buy.price
      }

      if (debug) console.debug('XPoint', XPoint)

      data.unshift(XPoint)
    }

    this.setState({data}, () => this.lineChart.forceUpdate())
  }

  handleOnClick = params => {
    if (!params) return

    const {activeLabel, chartY, activePayload} = params

    const closerLine = findCloserLine(activeLabel, chartY)
    const selectedPayload = activePayload[0].payload[closerLine+'Data']

    // query database for other user contact details
    firebase.database().ref().child('users').child(selectedPayload.uid).once('value', data => {
      const { telegram, hangout, custom } = data.val()
      this.setState(state => {
        return {
          selectedItem: {
            ...state.selectedItem,
            user: { telegram, hangout, custom }
          }
        }
      })
    })

    this.setState({
      selectedItem: {
        ...selectedPayload,
        action: closerLine === 'sell' ?  'buy' : 'sell' // user action is reverse to line data
      }
    })
  }

  handleDismiss = e => {
    if (e.target.className.includes('offerPopup') || e.target.className.includes('dismiss')) {
      this.setState({selectedItem: {}})
    }
  }

  handleBidChange = () => {
    if (!this.bidAmount || !this.bidPrice) return

    const bidPriceTotal = this.bidAmount * this.bidPrice | 0
    this.setState(
      {bidPriceTotal, bidPrice: this.bidPrice, sliderDefaultValue: bidPriceTotal},
      () => this.lineChart.forceUpdate()
    )
  }

  onSliderChange = e => {
    if (!this.bidAmount || !this.bidPrice) return

    const bidPriceTotal = e.target.value
    const bidPrice = Math.round(100 * bidPriceTotal / this.bidAmount) / 100
    this.bidPrice = bidPrice
    this.setState(
      {bidPrice, bidPriceTotal},
      () => this.lineChart.forceUpdate()
    )
  }

  get bidAmount() { return this.inputAmount.inputRef.value | 0 }

  get bidPrice() { return this.inputPrice.inputRef.value }

  set bidPrice(value) { this.inputPrice.inputRef.value = value }

  // bidDuration = Infinity

  handleBidSell = () => {
    if (!this.bidAmount || !this.bidPrice) return

    this.firebaseRefLastPush = storeAuctionBid({
      firebaseRef: this.firebaseRefSell.ref,
      payload: {
        quantity: this.bidAmount,
        price: this.bidPrice,
        priceTotal: Math.round(this.bidAmount * this.bidPrice)
      }
    })
    this.setState({isSnackbarActive: true})
  }

  handleBidBuy = () => {
    if (!this.bidAmount || !this.bidPrice) return

    this.firebaseRefLastPush = storeAuctionBid({
      firebaseRef: this.firebaseRefBuy.ref,
      payload: {
        quantity: this.bidAmount,
        price: this.bidPrice,
        priceTotal: Math.round(this.bidAmount * this.bidPrice)
      }
    })
    this.setState({isSnackbarActive: true})
  }

  // handleDurationChange = e => this.bidDuration = e.target.value

  handleTimeoutSnackbar = () => this.setState({isSnackbarActive: false})

  handleClickActionSnackbar = () => {
    this.firebaseRefLastPush[0].remove()
    this.firebaseRefLastPush[1].remove()
    this.firebaseRefLastPush = null
    this.setState({isSnackbarActive: false})
  }

  render() {

    const {data, bidPriceTotal, sliderDefaultValue, bidPrice, isSnackbarActive} = this.state
    const {action, quantity, price, priceTotal, user} = this.state.selectedItem

    if (debug) console.debug('%crender container', 'background: #c34ff7; color: #bada55')

    return (
      <div className="graph-container mdl-cell mdl-cell--12-col mdl-grid">
        <ChartPresentational
          data={data}
          ref={r => this.lineChart = r}
          handleOnClick={this.handleOnClick}
          referenceLineY={bidPrice}
        />
        <div className="bid mdl-color--grey-200 mdl-color-text--grey-600">
          <div className='title'>Place a bid</div>
          <div className="amount">
              <Textfield
                type='number'
                pattern='[0-9]*'
                onChange={this.handleBidChange}
                label="Amount..."
                style={{width: '100px'}}
                floatingLabel
                ref={r => this.inputAmount = r}
                error='invalid input'
              />
              <Textfield
                type='number'
                step={0.01}
                onChange={this.handleBidChange}
                label="Price..."
                style={{width: '70px'}}
                floatingLabel
                ref={r => this.inputPrice = r}
              /><span>TC</span>
          </div>
          <div className="total">
            <span>Total:</span> <span>{bidPriceTotal || 0}</span> <span>TC</span>
            <p className='slider'>
                <Slider
                  step={1}
                  min={sliderDefaultValue * 0.9 | 0 || 1}
                  max={sliderDefaultValue * 1.1 | 0 || 3}
                  defaultValue={sliderDefaultValue || 2}
                  onChange={this.onSliderChange}
                />
              </p>
          </div>
          {/* <div className="duration">
            <span style={{color: 'silver'}}>Days</span>
            <RadioGroup
              name="duration"
              value={this.bidDuration}
              onChange={this.handleDurationChange}
            >
              <Radio value={1} ripple>1</Radio>
              <Radio value={2} ripple>2</Radio>
              <Radio value={3} ripple>3</Radio>
              <Radio value={5} ripple>5</Radio>
              <Radio value={7} ripple>7</Radio>
              <Radio value={Infinity} ripple>∞</Radio>
            </RadioGroup>
          </div> */}
          <div className="control">
            <span style={{color: 'silver'}}>I want to...</span>
            <Button onClick={this.handleBidSell} className='mdl-color--red-400'   raised accent ripple>SELL</Button>
            <Button onClick={this.handleBidBuy}  className='mdl-color--green-400' raised accent ripple>BUY</Button>
          </div>
        </div>
        { action && <div className={`offerPopup ${action}`} onClick={this.handleDismiss}>
            <div className='dismiss'>dismiss</div>
            <div className='inner mdl-color--grey-200'>
              <span className='label'>{action}</span>
              <ul>
                <li>quantity: {quantity}</li>
                <li>price: {price} TC</li>
                <li>total: {priceTotal} TC</li>
                <li><hr/></li>
                <li>
                  { user
                    ?
                      <ul>
                        {user.telegram && <li>telegram: <a href={`http://t.me/${user.telegram}`} target='_blank'>{`http://t.me/${user.telegram}`}</a></li>}
                        {user.hangout && <li>hangout: {user.hangout}</li>}
                        {user.custom && <li>custom: {user.custom}</li>}
                      </ul>
                    : ['user:', <Spinner key='userSpinner' singleColor />]
                  }
                </li>
              </ul>
            </div>
          </div>
        }

        <Snackbar
          active={isSnackbarActive}
          onClick={this.handleClickActionSnackbar}
          onTimeout={this.handleTimeoutSnackbar}
          action="Undo"
          timeout={7000}
        >Bid created</Snackbar>
      </div>
    )
  }
}


class ChartPresentational extends React.PureComponent {
  shouldComponentUpdate(nextProps) {
    // rerenders cause unfocus - hover elements are lost (ActiveDot, Tooltip)
    // force rerenders by parent
    return false
  }

  render () {
    const {data, handleOnClick, referenceLineY} = this.props


    if (debug) console.debug('%crender chart', 'background: #4fc3f7; color: #222', this.props)

    return (
      <ResponsiveContainer width='100%' height='100%'>
        <LineChart data={data}
          margin={{top: 15, right: 30, left: -30, bottom: 5}} // the only way to position the Chart is to set negative margin... okay
          onClick={handleOnClick}
          onMouseMove={({chartY}) => mouseY = chartY}
        >
          <YAxis/>
          <CartesianGrid strokeDasharray={`${minDotRadius} 5`}/>
          <ReferenceLine y={referenceLineY} label="your bid" stroke="#4fc3f7" strokeDasharray="3 3" />
          <Tooltip content={<MyTooltip/>} />
          <Line isAnimationActive={false} type="monotone" dataKey="sell" strokeWidth={strokeWidth} stroke={red} dot={<Dot dataKey='sell'/>} activeDot={<ActiveDot/>}/>
          <Line isAnimationActive={false} type="monotone" dataKey="buy" strokeWidth={strokeWidth} stroke={green} dot={<Dot dataKey='buy'/>} activeDot={<ActiveDot/>}/>
        </LineChart>
      </ResponsiveContainer>
    )
  }

}


const Dot = ({cx, cy, stroke, dataKey, payload, index, ...rest}) => {
  /* When dot gets rendered, hijack it's props and store them for later reference */
  
  let size

  // calculate dot size relative to largest quantity
  if (dataKey === 'sell') {
    if (!payload.sell) return null
    size = (minDotRadius + (payload.sellData.quantity-minSellQuantity)*(maxDotRadius-minDotRadius)/(maxSellQuantity-minSellQuantity)) || (maxDotRadius-minDotRadius)/2
  } else {
    if (!payload.buy) return null
    size = (minDotRadius + (payload.buyData.quantity-minBuyQuantity)*(maxDotRadius-minDotRadius)/(maxBuyQuantity-minBuyQuantity)) || (maxDotRadius-minDotRadius)/2
  }

  // store points' Y coordinates to an array to be accessible later, also calculate and store mid points for tooltip
  if (!points[index]) {
    points[index] = {[dataKey]: cy}
  } else {
    points[index][dataKey] = cy
    let midPoint = (points[index].sell + points[index].buy) / 2
    if (midPoint) {
      points[index].mid = midPoint
      midPoints[index] = midPoint
    }
  }

  return <circle cx={cx} cy={cy} r={size} fill={stroke} />
}


const ActiveDot = ({dataKey, index, payload, ...rest}) => {
  // If you hover over a label (vertical line, Y axis) all points on that line get highlighted, but we want only one (top or bottom)
  if (dataKey === findCloserLine(index, mouseY)) return <circle {...rest} />
  return null
}


const MyTooltip = ({active, payload, label, coordinate, ...props}) => {
  if (!active || !payload) return null

  const closerLine = findCloserLine(label, coordinate.y)
  const {quantity, price, total} = payload[0].payload[closerLine+'Data']

  // user action is actually reverse to a line. A line displays sell offers and I want to BUY this sell offer.
  const userAction = closerLine === 'sell' ?  'click to buy this' : 'sell to the buyer'
  const borderColor = closerLine === 'sell' ? red : green

  return (
    <div className='tooltip'
      style={{borderColor}}
    >
      <span className='label'>{userAction}</span>
      <ul>
        <li>quantity: {quantity}</li>
        <li>price: {price} TC</li>
        <li>total: {total} TC</li>
      </ul>
    </div>
  )
}


function findCloserLine(/* coordinates: x */ index, y) {
  if (!midPoints[index]) {
    return Object.keys(points[index] || {})[0] // sell or buy ... only one key is present and that is the line
  } else {
    if (y < midPoints[index]) return 'sell'
    else return 'buy'
  }
}

0reactions
zzolocommented, Sep 4, 2019

I want to do something very similar where I want to have the tooltip react to what line/dot is closest to the mouse position. This was closed, was there a reason for that? Some sort of way to do this without @ackvf solution?

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to Show Different Data in Tooltip Based on ... - YouTube
How to Show Different Data in Tooltip Based on Dataset in Combo Bar Line Chart in Chart JSIn this video we will explore...
Read more >
JavaScript - Bootstrap
Data attributes. You can use all Bootstrap plugins purely through the markup API without writing a single line of JavaScript. This is Bootstrap's...
Read more >
Amazon EC2 Spot Instance Bid Status | AWS News Blog
You can click on the Bid Status message in the AWS Management Console to see a more verbose message in the tooltip:.
Read more >
The RGraph API documentation - Canvas
For some, the name is different so that it makes a little more sense. ... The Line chart obj.original_data is an aggregation of...
Read more >
Available CRAN Packages By Date of Publication
2022-12-18, christmas, Generation of Different Animated Christmas Cards ... 2022-12-17, survivoR, Data from all Seasons of Survivor (US) TV Series in Tidy ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found