Skip to content

feedback on tooltip API design #9

Description

@gillkyle

Loving the library!

I rebuilt 21 Tremor tooltip examples with TanStack Charts + React. The tooltip API itself worked well for content: renderTooltipBody, defaultBody, custom anchors, and portals were all solid.

There were a couple things that I ran into that maybe belong in userland, maybe could make for some nice features:

Background highlights

One thing I wanted to replicate was using a background highlight to show an active bar mark, instead of a circular mark that appears in the bar

Tremor Ant Design TanStack Charts
Image Image Image

It took some hacking, but was possible:

  const previous = svg.querySelector('[data-tremor-focus-band]')

  if (!focusedDate) {
    previous?.remove()
    return
  }

  const center = scene.scales.x.map(focusedDate)
  const width = scene.scales.x.bandwidth
  const band =
    previous ??
    svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'rect')

  band.setAttribute('x', String(center - width / 2 - 6))
  band.setAttribute('y', String(scene.chart.y))
  band.setAttribute('width', String(width + 12))
  band.setAttribute('height', String(scene.chart.height))
  band.setAttribute('fill', '#94a3b8')
  band.setAttribute('fill-opacity', '0.16')

  svg.insertBefore(band, svg.querySelector('.ts-chart__axes'))

Maybe there's a more elegant way to do this with some d3 primitive?...

Removing axis ticks

To get gridlines without tick stubs, I had to inspect every rendered line and hide the short ones:

for (const tick of svg.querySelectorAll('.ts-chart__axes line')) {
  const dx = Number(tick.getAttribute('x2')) - Number(tick.getAttribute('x1'))
  const dy = Number(tick.getAttribute('y2')) - Number(tick.getAttribute('y1'))

  if (Math.hypot(dx, dy) <= 4.1) {
    tick.style.visibility = 'hidden'
  }
}

I don't think axis/grid stuff is super exposed yet?..

Responsive axis labels

I really like a feature of Tremor's where they adapt smaller UI's to hide labels when they start to get squished together:

Wide container view Narrow container view
Image Image

Container queries make a great case for this, so that's what I used. Feels like a nice default for a lot of timescale charts, but maybe that's not something to be too opinionated about.

@container tremor-chart (max-width: 639px) {
  .chart .ts-chart__axes
    text[data-ts-key^='x-tick-label']
    :not(:nth-child(1 of [data-ts-key^='x-tick-label']))
    :not(:nth-last-child(1 of [data-ts-key^='x-tick-label'])) {
    display: none;
  }
}

Fixed tooltip placement

Another thing I was trying to emulate that Tremor does design-wise that feels tasteful is anchoring tooltips along an axis.

CleanShot.2026-07-30.at.22.48.36.mp4

This was pretty doable, but had to be calculated:

const tooltipAnchor = (points, { chart }) => ({
  x: points.reduce((sum, point) => sum + point.x, 0) / points.length,
  y: chart.y,
})

const definition = defineChart({
  // ...
  tooltip: {
    anchor: tooltipAnchor,
    placement: 'bottom',
    offset: 12,
  },
})

Stacked/dodged bars are a little boilerplate-y

This makes sense from a design standpoint but just feels annoying to have to do this magic feeling y1: stackStart thing combined with some data transformation and get it just right, felt like there were lots of easy ways to screw this up where types check but charts look wonky.

const runningTotals = new Map<string, number>()

const stackedRows = rows.map((row) => {
  const stackStart = runningTotals.get(row.date) ?? 0
  const stackEnd = stackStart + row.value
  runningTotals.set(row.date, stackEnd)

  return { ...row, stackStart, stackEnd }
})

barY(stackedRows, {
  x: 'date',
  y1: 'stackStart',
  y2: 'stackEnd',
  z: 'category',
})

Grouped bars aren't too bad:

barY(rows, {
  x: 'date',
  y: 'value',
  z: 'category',
  groupScale: () => scaleBand().padding(0.1),
})

The really common charts (stacked + grouped bars) require the users to own both the data transform and the geometry. Not sure what the solution is, just pointing out a place that took a little more care.


Trying to not to prescribe solutions and just call out some overall papercuts I noticed. Overall was very pleased with how much I could do and how quickly agents could pick up on and turn this stuff into all sorts of weird/cool visualizations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions