TanStack
Catalog

Expanding pinned energy tooltip

interaction

1,068 lines · 3 files · 27.0 kB

cases/84-pinned-nested-chart-tooltip/example.tsx433 lines · entry
cases/84-pinned-nested-chart-tooltip/example.tsx
import { useMemo, useRef } from 'react'
import {
  areaY,
  barX,
  barY,
  d3Curve,
  defineChart,
  dot,
  lineY,
  ruleX,
  tickY,
  whenFocused,
} from '@tanstack/charts'
import { renderChartSvgWithResources } from '@tanstack/charts/svg/resources'
import { Chart as NestedChart } from '@tanstack/charts/react'
import { Chart as TooltipChart } from '@tanstack/charts/react/tooltip'
import { tooltip } from '@tanstack/charts/tooltip'
import { scaleBand, scaleLinear } from 'd3-scale'
import { curveMonotoneX } from 'd3-shape'
import {
  consumptionBreakdown,
  energyAnnualOverview,
  energyColors,
  energyMonths,
  energyTooltipContent,
  formatEnergy,
} from './model'
import { EnergyTooltipBody, energyTooltipStyles } from './tooltip-body'
import type { ChartScene } from '@tanstack/charts'
import type { EnergyMonth, EnergyMonthId } from './model'

export function energyDefinition(
  rows: readonly EnergyMonth[],
  chartWidth: number,
) {
  const months = rows.map((row) => row.monthShort)
  const tooltipPlacement =
    chartWidth >= 500
      ? ('right' as const)
      : (['right', 'left', 'top', 'bottom'] as const)
  return defineChart(
    {
      marks: [
        areaY(rows, {
          id: 'consumption-area',
          x: 'monthShort',
          y: 'consumption',
          fill: energyColors.consumption,
          fillOpacity: 0.13,
          curve: d3Curve(curveMonotoneX),
        }),
        barY(rows, {
          id: 'used-on-site',
          x: 'monthShort',
          y: 'usedOnSite',
          fill: energyColors.generationMuted,
          inset: 0.5,
          states: [
            {
              when: { focus: 'group' },
              style: { fill: energyColors.generation },
              transition: {
                type: 'tween',
                duration: 120,
                easing: 'ease-out',
              },
            },
          ],
        }),
        barY(rows, {
          id: 'exported',
          x: 'monthShort',
          y1: 'usedOnSite',
          y2: 'generation',
          fill: 'url(#energy-exported-hatch)',
          inset: 0.5,
          radius: 3,
        }),
        lineY(rows, {
          id: 'consumption-line',
          x: 'monthShort',
          y: 'consumption',
          stroke: energyColors.consumption,
          strokeWidth: 1.6,
          curve: d3Curve(curveMonotoneX),
        }),
        whenFocused(
          ruleX(rows, {
            id: 'focused-month-guide',
            x: 'monthShort',
            stroke: 'CanvasText',
            strokeOpacity: 0.45,
            strokeWidth: 1,
            strokeDasharray: '4 4',
          }),
          { match: 'x' },
        ),
        whenFocused(
          tickY(rows, {
            id: 'focused-month-axis-marker',
            x: 'monthShort',
            y: () => 0,
            stroke: 'CanvasText',
            strokeWidth: 1.5,
          }),
          { match: 'x' },
        ),
        dot(rows, {
          id: 'consumption-points',
          x: 'monthShort',
          y: 'consumption',
          fill: 'Canvas',
          fillOpacity: 0,
          stroke: energyColors.consumption,
          strokeOpacity: 0,
          strokeWidth: 1.4,
          r: 4,
          states: [
            {
              when: { focus: 'group' },
              style: {
                r: 4,
                fillOpacity: 1,
                strokeOpacity: 1,
                strokeWidth: 1.8,
              },
              transition: {
                type: 'tween',
                duration: 150,
                easing: 'ease-out',
              },
            },
            {
              when: { focus: 'primary', pinned: true },
              style: {
                r: 4.5,
                fillOpacity: 1,
                strokeOpacity: 1,
                strokeWidth: 2,
              },
              transition: {
                type: 'tween',
                duration: 180,
                easing: 'ease-out',
              },
            },
          ],
        }),
      ],
      scales: {
        x: {
          scale: scaleBand<string>()
            .domain(months)
            .paddingInner(0.16)
            .paddingOuter(0.06),
          axis: {
            line: false,
            ticks: { size: 0, padding: 8 },
          },
        },
        y: {
          scale: scaleLinear().domain([0, 2600]),
          grid: true,
          axis: {
            line: false,
            ticks: {
              values: [0, 650, 1300, 1950, 2600],
              size: 0,
              padding: 7,
              format: (value) => `${value.toLocaleString('en-US')} kWh`,
            },
          },
        },
      },

      margin: { top: 82, right: 24, bottom: 38, left: 72 },
      gradients: [
        {
          id: 'energy-exported-hatch',
          x1: 0,
          y1: 0,
          x2: 1,
          y2: 1,
          stops: exportedHatchStops,
        },
      ],
    },
    {
      svgAnimation: false,
      keyboard: true,
      focus: 'group-x',
      focusRing: false,
      tooltip: {
        use: tooltip,
        className: 'energy-tooltip-surface',
        anchor: 'point',
        placement: tooltipPlacement,
        offset: 12,
        content: (points, { pinned }) => energyTooltipContent(points, pinned),
      },
    },
  )
}

export function ConsumptionMixChart({
  month,
  idPrefix,
  catalogPreview = false,
}: {
  readonly month: EnergyMonth
  readonly idPrefix?: string
  readonly catalogPreview?: boolean
}) {
  const definition = useMemo(() => {
    const parts = consumptionBreakdown(month)
    return defineChart(
      {
        marks: [
          barX(parts, {
            id: 'consumption-breakdown',
            x1: 'start',
            x2: 'end',
            y: () => 'mix',
            fill: (part) => part.color,
            inset: 0,
          }),
        ],
        scales: {
          x: {
            scale: scaleLinear().domain([0, month.consumption]),
            axis: false,
          },
          y: {
            scale: scaleBand<string>().domain(['mix']),
            axis: false,
          },
        },

        margin: 0,
      },
      { svgAnimation: false, keyboard: false, tooltip: false },
    )
  }, [month])

  return (
    <NestedChart
      idPrefix={idPrefix}
      className={catalogPreview ? 'energy-catalog-preview-nested' : undefined}
      definition={definition}
      width={264}
      height={10}
      ariaLabel={`${month.month} consumption split: household ${month.household} kilowatt-hours, heat pump ${month.heatPump}, hot water ${month.hotWater}, and EV charging ${month.evCharging}`}
    />
  )
}

export function AnnualMetric({
  label,
  value,
}: {
  label: string
  value: string
}) {
  const [amount, unit] = value.split(' ')
  return (
    <div style={{ display: 'grid', gap: 3 }}>
      <span
        style={{ color: 'color-mix(in srgb, CanvasText 55%, transparent)' }}
      >
        {label}
      </span>
      <strong
        style={{ fontSize: 19, fontWeight: 680, letterSpacing: '-0.02em' }}
      >
        {amount}{' '}
        <span style={{ fontSize: 11, fontWeight: 620, letterSpacing: 0 }}>
          {unit}
        </span>
      </strong>
    </div>
  )
}

export const exportedHatchStops = Array.from({ length: 14 }, (_, index) => {
  const start = index / 14
  const lineStart = (index + 0.82) / 14
  const end = (index + 1) / 14
  return [
    { offset: start, color: energyColors.exported },
    { offset: lineStart, color: energyColors.exported },
    { offset: lineStart, color: '#fff7e8' },
    { offset: end, color: '#fff7e8' },
  ]
}).flat()

export interface ExampleProps {
  width?: number
  height?: number
  revision?: number
}

export default function EnergyTooltipExample({
  width = 640,
  height = 480,
  revision = 0,
}: ExampleProps = {}) {
  const input = { width, height, revision, preview: false, interactive: true }
  const idPrefix = '84-pinned-nested-chart-tooltip'
  const viewRef = useRef<HTMLDivElement>(null)

  const focusedIdRef = useRef<EnergyMonthId | null>(null)

  const renderedRef = useRef<{
    scene: ChartScene<EnergyMonth, string, number>
    svg: SVGSVGElement
  } | null>(null)

  const rows = useMemo(() => energyMonths(input.revision), [input.revision])

  const chartWidth = Math.max(1, input.width - 24)

  const chartHeight = Math.max(1, input.height - 48)

  const annualConsumption = rows.reduce(
    (total, month) => total + month.consumption,
    0,
  )

  const mainDefinition = useMemo(
    () => energyDefinition(rows, chartWidth),
    [chartWidth, rows],
  )

  return (
    <div
      ref={viewRef}
      data-conformance-view="main"
      role="region"
      aria-label="Monthly household energy with an expanding pinned tooltip"
      style={{
        position: 'relative',
        width: input.width,
        height: input.height,
        paddingTop: 4,
        background: 'Canvas',
        color: 'CanvasText',
        boxSizing: 'border-box',
      }}
    >
      <style>{energyTooltipStyles}</style>
      <header
        style={{
          display: 'flex',
          height: 36,
          alignItems: 'center',
          padding: '0 24px',
          font: '500 12px/1.3 system-ui, sans-serif',
        }}
      >
        <strong style={{ fontSize: 13, fontWeight: 680 }}>
          Annual overview
        </strong>
      </header>
      <div
        className="energy-overview-card"
        style={{
          position: 'relative',
          width: chartWidth,
          height: chartHeight,
          margin: '0 12px',
          border: '1px solid color-mix(in srgb, CanvasText 8%, transparent)',
          borderRadius: 7,
          boxSizing: 'border-box',
        }}
      >
        <div
          aria-hidden="true"
          style={{
            position: 'absolute',
            zIndex: 1,
            top: 14,
            left: 16,
            display: 'flex',
            gap: 26,
            pointerEvents: 'none',
            font: '500 11px/1.2 system-ui, sans-serif',
          }}
        >
          <AnnualMetric
            label="Energy generated"
            value={formatEnergy(energyAnnualOverview.generation)}
          />
          <AnnualMetric
            label="Total consumption"
            value={formatEnergy(annualConsumption)}
          />
        </div>
        <TooltipChart
          idPrefix={idPrefix ? `${idPrefix}-main` : undefined}
          definition={mainDefinition}
          initialWidth={chartWidth}
          height={chartHeight}
          renderSvg={renderChartSvgWithResources}
          ariaLabel="Annual household energy overview"
          ariaDescription="A gray area tracks monthly electricity consumption. Stacked gold bars show solar energy used on site and exported. Hover or focus a month for totals, then click or press Enter to expand the breakdown."
          onFocusGroupChange={(points) => {
            focusedIdRef.current = points[0]?.datum.id ?? null
          }}
          onRender={({ scene, svg }) => {
            renderedRef.current = { scene, svg }
          }}
          renderTooltipBody={({ points, defaultBody, pinned, dismiss }) => {
            const month = points[0]?.datum
            if (!month) return defaultBody
            return (
              <EnergyTooltipBody
                month={month}
                pinned={pinned}
                dismiss={dismiss}
                consumptionChart={
                  <ConsumptionMixChart
                    month={month}
                    idPrefix={idPrefix ? `${idPrefix}-nested` : undefined}
                  />
                }
              />
            )
          }}
        />
      </div>
    </div>
  )
}
cases/84-pinned-nested-chart-tooltip/model.ts220 lines · dependency
cases/84-pinned-nested-chart-tooltip/model.ts
import type { ChartTooltipContent } from '@tanstack/charts'

export const energyColors = {
  consumption: '#8b8d90',
  household: '#1685ff',
  heatPump: '#e82285',
  hotWater: '#ee4c91',
  evCharging: '#5cbd68',
  generationMuted: '#f4c675',
  generation: '#f2a900',
  exported: '#f8d99a',
} as const

export const energyAnnualOverview = {
  generation: 3_509,
  consumption: 17_847,
} as const

export const energyMonthIds = [
  'jan',
  'feb',
  'mar',
  'apr',
  'may',
  'jun',
  'jul',
  'aug',
  'sep',
  'oct',
  'nov',
  'dec',
] as const

export type EnergyMonthId = (typeof energyMonthIds)[number]

export interface EnergyMonth {
  readonly id: EnergyMonthId
  readonly month: string
  readonly monthShort: string
  readonly household: number
  readonly heatPump: number
  readonly hotWater: number
  readonly evCharging: number
  readonly consumption: number
  readonly generation: number
  readonly usedOnSite: number
  readonly exported: number
  readonly householdStart: number
  readonly householdEnd: number
  readonly heatPumpStart: number
  readonly heatPumpEnd: number
  readonly hotWaterStart: number
  readonly hotWaterEnd: number
  readonly evChargingStart: number
  readonly evChargingEnd: number
}

export interface EnergyBreakdownPart {
  readonly id: string
  readonly label: string
  readonly value: number
  readonly start: number
  readonly end: number
  readonly color: string
}

const baseMonths = [
  ['jan', 'January', 'Jan', 783, 637, 468, 688, 188, 180, 8],
  ['feb', 'February', 'Feb', 672, 531, 400, 563, 219, 217, 2],
  ['mar', 'March', 'Mar', 668, 466, 397, 614, 262, 257, 5],
  ['apr', 'April', 'Apr', 570, 342, 352, 524, 375, 322, 53],
  ['may', 'May', 'May', 376, 191, 232, 352, 427, 219, 208],
  ['jun', 'June', 'Jun', 241, 122, 150, 225, 482, 169, 313],
  ['jul', 'July', 'Jul', 246, 91, 155, 233, 367, 150, 217],
  ['aug', 'August', 'Aug', 241, 96, 155, 233, 354, 138, 216],
  ['sep', 'September', 'Sep', 242, 135, 147, 223, 304, 142, 162],
  ['oct', 'October', 'Oct', 362, 239, 218, 335, 258, 185, 73],
  ['nov', 'November', 'Nov', 577, 449, 345, 524, 174, 171, 3],
  ['dec', 'December', 'Dec', 607, 495, 365, 570, 100, 99, 1],
] as const satisfies readonly (readonly [
  EnergyMonthId,
  string,
  string,
  number,
  number,
  number,
  number,
  number,
  number,
  number,
])[]

export function energyMonths(revision = 0): readonly EnergyMonth[] {
  return baseMonths.map(
    ([
      id,
      month,
      monthShort,
      household,
      heatPump,
      hotWater,
      baseEvCharging,
      generation,
      usedOnSite,
      exported,
    ]) => {
      const evCharging =
        id === 'dec' && revision % 2 === 1
          ? baseEvCharging + 18
          : baseEvCharging
      const householdStart = 0
      const householdEnd = household
      const heatPumpStart = householdEnd
      const heatPumpEnd = heatPumpStart + heatPump
      const hotWaterStart = heatPumpEnd
      const hotWaterEnd = hotWaterStart + hotWater
      const evChargingStart = hotWaterEnd
      const evChargingEnd = evChargingStart + evCharging
      return {
        id,
        month,
        monthShort,
        household,
        heatPump,
        hotWater,
        evCharging,
        consumption: evChargingEnd,
        generation,
        usedOnSite,
        exported,
        householdStart,
        householdEnd,
        heatPumpStart,
        heatPumpEnd,
        hotWaterStart,
        hotWaterEnd,
        evChargingStart,
        evChargingEnd,
      }
    },
  )
}

export function isEnergyMonthId(value: unknown): value is EnergyMonthId {
  return energyMonthIds.some((id) => id === value)
}

export function monthFromTarget(target: { view?: string; anchor: string }) {
  if (target.view !== undefined && target.view !== 'main') return null
  const [kind, id] = target.anchor.split(':')
  return kind === 'month' && isEnergyMonthId(id) ? id : null
}

export function consumptionBreakdown(
  month: EnergyMonth,
): readonly EnergyBreakdownPart[] {
  return [
    {
      id: 'household',
      label: 'Household',
      value: month.household,
      start: month.householdStart,
      end: month.householdEnd,
      color: energyColors.household,
    },
    {
      id: 'heat-pump',
      label: 'Heat pump',
      value: month.heatPump,
      start: month.heatPumpStart,
      end: month.heatPumpEnd,
      color: energyColors.heatPump,
    },
    {
      id: 'hot-water',
      label: 'Hot water',
      value: month.hotWater,
      start: month.hotWaterStart,
      end: month.hotWaterEnd,
      color: energyColors.hotWater,
    },
    {
      id: 'ev-charging',
      label: 'EV charging',
      value: month.evCharging,
      start: month.evChargingStart,
      end: month.evChargingEnd,
      color: energyColors.evCharging,
    },
  ]
}

export function energyTooltipContent(
  points: readonly { readonly datum: EnergyMonth }[],
  _pinned: boolean,
): ChartTooltipContent {
  const month = points[0]?.datum
  if (!month) return { rows: [] }
  return {
    title: month.month,
    rows: [
      {
        label: 'Consumption',
        value: formatEnergy(month.consumption),
      },
      {
        label: 'Generation',
        value: formatEnergy(month.generation),
      },
    ],
  }
}

export function formatEnergy(value: number) {
  return `${value.toLocaleString('en-US')} kWh`
}

export function formatPercent(value: number) {
  return `${Math.round(value * 100)}%`
}
cases/84-pinned-nested-chart-tooltip/tooltip-body.tsx415 lines · dependency
cases/84-pinned-nested-chart-tooltip/tooltip-body.tsx
import type { ReactNode } from 'react'
import {
  consumptionBreakdown,
  energyColors,
  formatEnergy,
  formatPercent,
} from './model'
import type { EnergyMonth } from './model'

interface EnergyTooltipBodyProps {
  readonly month: EnergyMonth
  readonly pinned: boolean
  readonly dismiss: () => void
  readonly consumptionChart: ReactNode
}

export function EnergyTooltipBody({
  month,
  pinned,
  dismiss,
  consumptionChart,
}: EnergyTooltipBodyProps) {
  const coverageShare = month.usedOnSite / month.consumption
  const usedShare = month.usedOnSite / month.generation
  const exportedShare = month.exported / month.generation

  return (
    <div className="energy-tooltip" data-expanded={String(pinned)}>
      <div className="energy-tooltip__summary">
        <div className="ts-chart-tooltip__title">{month.month}</div>
        {pinned ? (
          <button
            className="energy-tooltip__close"
            type="button"
            data-energy-tooltip-close
            aria-label="Close energy details"
            onPointerDown={(event) => event.stopPropagation()}
            onClick={dismiss}
          >
            <Chevron expanded />
          </button>
        ) : (
          <span className="energy-tooltip__toggle" aria-hidden="true">
            <Chevron expanded={false} />
          </span>
        )}
      </div>
      <MetricRow label="Consumption" value={formatEnergy(month.consumption)} />
      <div className="energy-tooltip__compact-generation">
        <div className="energy-tooltip__compact-generation-inner">
          <MetricRow
            label="Generation"
            value={formatEnergy(month.generation)}
          />
        </div>
      </div>
      <div className="energy-tooltip__reveal" aria-hidden={!pinned}>
        <div className="energy-tooltip__reveal-inner">
          <div className="energy-tooltip__details">
            <section aria-label="Consumption mix">
              <div className="energy-tooltip__mini-chart">
                {consumptionChart}
              </div>
              {consumptionBreakdown(month).map((part) => (
                <DetailRow
                  key={part.id}
                  color={part.color}
                  label={part.label}
                  value={formatEnergy(part.value)}
                />
              ))}
            </section>

            <section aria-label="Generation use">
              <MetricRow
                className="energy-tooltip__generation-heading"
                label="Generation"
                summary={false}
                value={formatEnergy(month.generation)}
              />
              <div
                className="energy-tooltip__generation-bar"
                aria-hidden="true"
              >
                <span
                  style={{
                    flex: month.usedOnSite,
                    background: energyColors.generation,
                  }}
                />
                <span
                  style={{
                    flex: month.exported,
                    background: energyColors.exported,
                  }}
                />
              </div>
              <DetailRow
                color={energyColors.generation}
                label="Used on site"
                value={formatPercent(usedShare)}
              />
              <DetailRow
                color={energyColors.exported}
                label="Exported"
                value={formatPercent(exportedShare)}
              />
            </section>
          </div>
        </div>
      </div>
      <p className="energy-tooltip__footer">
        Solar covered {formatPercent(coverageShare)} of this month&apos;s
        consumption, with the rest coming from the grid.
      </p>
    </div>
  )
}

function Chevron({ expanded }: { readonly expanded: boolean }) {
  return (
    <svg
      className="energy-tooltip__chevron"
      viewBox="0 0 12 12"
      aria-hidden="true"
    >
      <path
        d={
          expanded
            ? 'M3 2.5 6 5 9 2.5M3 9.5 6 7 9 9.5'
            : 'm3 4 3-3 3 3M3 8l3 3 3-3'
        }
      />
    </svg>
  )
}

function MetricRow({
  className,
  label,
  summary = true,
  value,
}: {
  readonly className?: string
  readonly label: string
  readonly summary?: boolean
  readonly value: string
}) {
  return (
    <div
      className={[
        'energy-tooltip__metric-row',
        summary ? 'ts-chart-tooltip__row' : null,
        className,
      ]
        .filter(Boolean)
        .join(' ')}
    >
      <span>{label}</span>
      <span>{value}</span>
    </div>
  )
}

function DetailRow({
  color,
  label,
  value,
}: {
  readonly color: string
  readonly label: string
  readonly value: string
}) {
  return (
    <div className="energy-tooltip__detail-row" data-energy-detail-row>
      <span
        className="energy-tooltip__swatch"
        style={{ background: color }}
        aria-hidden="true"
      />
      <span>{label}</span>
      <span>{value}</span>
    </div>
  )
}

export const energyTooltipStyles = `
  .energy-overview-card .ts-chart:focus,
  .energy-overview-card [role='listbox']:focus {
    outline: none;
  }

  .ts-chart-tooltip.energy-tooltip-surface,
  .energy-reference-tooltip {
    box-sizing: border-box;
    width: 292px;
    max-width: calc(100vw - 24px) !important;
    padding: 0 !important;
    overflow: hidden;
    border: 1px solid rgb(255 255 255 / 0.1) !important;
    border-radius: 10px !important;
    background: #2b2b2e !important;
    color: #f4f4f5 !important;
    box-shadow: 0 14px 34px rgb(0 0 0 / 0.3) !important;
    font: 500 12px/1.35 system-ui, sans-serif !important;
  }

  .energy-reference-tooltip {
    position: absolute;
    z-index: 2;
  }

  .energy-tooltip {
    padding: 12px;
  }

  .energy-tooltip__summary {
    position: relative;
    min-width: 0;
    padding-right: 28px;
  }

  .energy-tooltip .ts-chart-tooltip__title {
    display: flex;
    align-items: center;
    min-height: 18px;
    margin: 0 0 6px;
    color: #f4f4f5;
    font-size: 12px;
    font-weight: 650;
  }

  .energy-tooltip__metric-row {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) auto !important;
    align-items: center !important;
    column-gap: 12px !important;
    font-variant-numeric: tabular-nums;
  }

  .energy-tooltip__metric-row > :last-child {
    color: #fafafa;
    font-weight: 620;
    text-align: right;
    white-space: nowrap;
  }

  .energy-tooltip__detail-row {
    display: grid;
    grid-template-columns: 3px minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 8px;
    min-height: 17px;
    color: #d4d4d8;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
  }

  .energy-tooltip__detail-row > :last-child {
    color: #f4f4f5;
    font-weight: 600;
    text-align: right;
    white-space: nowrap;
  }

  .energy-tooltip__swatch {
    display: block;
    width: 3px;
    height: 10px;
    border-radius: 999px;
  }

  .energy-tooltip__close,
  .energy-tooltip__toggle {
    position: absolute;
    top: -9px;
    right: -9px;
    display: grid;
    width: 36px;
    height: 36px;
    place-items: center;
    color: #a1a1aa;
  }

  .energy-tooltip__close {
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: #a1a1aa;
    cursor: pointer;
    pointer-events: auto;
  }

  .energy-tooltip__chevron {
    width: 12px;
    height: 12px;
    overflow: visible;
    fill: none;
    stroke: currentColor;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-width: 1.5;
  }

  .energy-tooltip__close:hover,
  .energy-tooltip__close:focus-visible {
    background: rgb(255 255 255 / 0.08);
    color: #fafafa;
    outline: none;
  }

  .energy-tooltip__close:focus-visible {
    box-shadow: inset 0 0 0 2px #f5b942;
  }

  .energy-tooltip__reveal {
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    transition:
      grid-template-rows 260ms cubic-bezier(0.22, 1, 0.36, 1),
      opacity 160ms ease;
  }

  .energy-tooltip[data-expanded='true'] .energy-tooltip__reveal {
    grid-template-rows: 1fr;
    opacity: 1;
  }

  .energy-tooltip__reveal-inner {
    min-height: 0;
    overflow: hidden;
  }

  .energy-tooltip__details {
    display: grid;
    gap: 12px;
    margin-top: 5px;
    transform: translateY(-4px);
    transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
  }

  .energy-tooltip[data-expanded='true'] .energy-tooltip__details {
    transform: translateY(0);
  }

  .energy-tooltip__details section {
    display: grid;
    gap: 5px;
  }

  .energy-tooltip__compact-generation {
    display: grid;
    grid-template-rows: 1fr;
    opacity: 1;
    transition:
      grid-template-rows 260ms cubic-bezier(0.22, 1, 0.36, 1),
      opacity 120ms ease;
  }

  .energy-tooltip[data-expanded='true'] .energy-tooltip__compact-generation {
    grid-template-rows: 0fr;
    opacity: 0;
  }

  .energy-tooltip__compact-generation-inner {
    min-height: 0;
    overflow: hidden;
  }

  .energy-tooltip__generation-heading {
    margin-top: 2px;
    padding-top: 8px;
    border-top: 1px solid rgb(255 255 255 / 0.09);
  }

  .energy-tooltip__mini-chart,
  .energy-tooltip__generation-bar {
    width: 100%;
    height: 8px;
    overflow: hidden;
    border-radius: 3px;
    background: rgb(255 255 255 / 0.08);
  }

  .energy-tooltip__mini-chart svg {
    display: block;
    width: 100%;
    height: 8px;
  }

  .energy-tooltip__generation-bar {
    display: flex;
  }

  .energy-tooltip__footer {
    margin: 10px -12px -12px;
    padding: 10px 12px 11px;
    border-top: 1px solid rgb(255 255 255 / 0.07);
    background: #222225;
    color: #a8a8af;
    font-size: 12px;
    font-weight: 500;
    line-height: 1.4;
  }

  @media (prefers-reduced-motion: reduce) {
    .energy-tooltip__compact-generation,
    .energy-tooltip__reveal,
    .energy-tooltip__details {
      transition: none;
    }
  }
`