Historgram

free

Historgram draws a value distribution as bars and highlights whichever sub-range currently matches a paired range control.

@floating-ui/react
Preview
Dark
$100 – $350
import { useState } from 'react';
import Historgram from '@/components/composites/Historgram';
import Slider from '@/components/ui/Slider';

const priceData = [
  { value: 1, label: '$0-12' },
  { value: 1, label: '$12-25' },
  { value: 2, label: '$25-37' },
  { value: 2, label: '$37-50' },
  { value: 4, label: '$50-62' },
  { value: 5, label: '$62-75' },
  { value: 6, label: '$75-87' },
  { value: 8, label: '$87-100' },
  { value: 12, label: '$100-112' },
  { value: 14, label: '$112-125' },
  { value: 16, label: '$125-137' },
  { value: 19, label: '$137-150' },
  { value: 25, label: '$150-162' },
  { value: 28, label: '$162-175' },
  { value: 31, label: '$175-187' },
  { value: 33, label: '$187-200' },
  { value: 37, label: '$200-212' },
  { value: 38, label: '$212-225' },
  { value: 39, label: '$225-237' },
  { value: 40, label: '$237-250' },
  { value: 40, label: '$250-262' },
  { value: 39, label: '$262-275' },
  { value: 38, label: '$275-287' },
  { value: 37, label: '$287-300' },
  { value: 33, label: '$300-312' },
  { value: 31, label: '$312-325' },
  { value: 28, label: '$325-337' },
  { value: 25, label: '$337-350' },
  { value: 19, label: '$350-362' },
  { value: 16, label: '$362-375' },
  { value: 14, label: '$375-387' },
  { value: 12, label: '$387-400' },
  { value: 8, label: '$400-412' },
  { value: 6, label: '$412-425' },
  { value: 5, label: '$425-437' },
  { value: 4, label: '$437-450' },
  { value: 2, label: '$450-462' },
  { value: 2, label: '$462-475' },
  { value: 1, label: '$475-487' },
  { value: 1, label: '$487-500' },
];

export default function UsageDemo() {
  const [range, setRange] = useState<[number, number]>([100, 350]);

  return (
    <div className="w-full max-w-md">
      <Historgram data={priceData} range={range} min={0} max={500} />
      <div className="mt-4 space-y-2">
        <Slider.Range min={0} max={500} value={range} onChange={setRange} />
        <div className="text-xs text-muted-foreground">
          ${range[0]} – ${range[1]}
        </div>
      </div>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Historgram

Examples

Price Filter

Preview
Dark
import { useState } from 'react';
import Form from '@/components/ui/Form';
import Slider from '@/components/ui/Slider';
import FormatInput from '@/components/composites/FormatInput';
import Historgram from '@/components/composites/Historgram';

const priceData = [
  { value: 1, label: '$0-12' },
  { value: 1, label: '$12-25' },
  { value: 2, label: '$25-37' },
  { value: 2, label: '$37-50' },
  { value: 4, label: '$50-62' },
  { value: 5, label: '$62-75' },
  { value: 6, label: '$75-87' },
  { value: 8, label: '$87-100' },
  { value: 12, label: '$100-112' },
  { value: 14, label: '$112-125' },
  { value: 16, label: '$125-137' },
  { value: 19, label: '$137-150' },
  { value: 25, label: '$150-162' },
  { value: 28, label: '$162-175' },
  { value: 31, label: '$175-187' },
  { value: 33, label: '$187-200' },
  { value: 37, label: '$200-212' },
  { value: 38, label: '$212-225' },
  { value: 39, label: '$225-237' },
  { value: 40, label: '$237-250' },
  { value: 40, label: '$250-262' },
  { value: 39, label: '$262-275' },
  { value: 38, label: '$275-287' },
  { value: 37, label: '$287-300' },
  { value: 33, label: '$300-312' },
  { value: 31, label: '$312-325' },
  { value: 28, label: '$325-337' },
  { value: 25, label: '$337-350' },
  { value: 19, label: '$350-362' },
  { value: 16, label: '$362-375' },
  { value: 14, label: '$375-387' },
  { value: 12, label: '$387-400' },
  { value: 8, label: '$400-412' },
  { value: 6, label: '$412-425' },
  { value: 5, label: '$425-437' },
  { value: 4, label: '$437-450' },
  { value: 2, label: '$450-462' },
  { value: 2, label: '$462-475' },
  { value: 1, label: '$475-487' },
  { value: 1, label: '$487-500' },
];

export default function PriceFilterDemo() {
  const [range, setRange] = useState<[number, number]>([50, 300]);

  return (
    <div className="w-full max-w-md">
      <Historgram data={priceData} range={range} min={0} max={500} />
      <div className="mt-4">
        <Slider.Range min={0} max={500} value={range} onChange={setRange} />
      </div>
      <div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
        <Form.Field label="Min" htmlFor="price-min">
          <FormatInput.Numeric
            id="price-min"
            prefix="$"
            thousandSeparator
            value={range[0]}
            onValueChange={(values) =>
              setRange([values.floatValue ?? 0, range[1]])
            }
          />
        </Form.Field>
        <Form.Field label="Max" htmlFor="price-max">
          <FormatInput.Numeric
            id="price-max"
            prefix="$"
            thousandSeparator
            value={range[1]}
            onValueChange={(values) =>
              setRange([range[0], values.floatValue ?? 0])
            }
          />
        </Form.Field>
      </div>
    </div>
  );
}

Popover

Preview
Dark
import { useState } from 'react';
import { PiFunnel } from 'react-icons/pi'
import Button from '@/components/ui/Button';
import Popover from '@/components/ui/Popover';
import Form from '@/components/ui/Form';
import Slider from '@/components/ui/Slider';
import FormatInput from '@/components/composites/FormatInput';
import Historgram from '@/components/composites/Historgram';

const priceData = [
  { value: 2, label: '$0-25' },
  { value: 4, label: '$25-50' },
  { value: 7, label: '$50-75' },
  { value: 11, label: '$75-100' },
  { value: 15, label: '$100-125' },
  { value: 20, label: '$125-150' },
  { value: 25, label: '$150-175' },
  { value: 29, label: '$175-200' },
  { value: 33, label: '$200-225' },
  { value: 35, label: '$225-250' },
  { value: 34, label: '$250-275' },
  { value: 31, label: '$275-300' },
  { value: 27, label: '$300-325' },
  { value: 22, label: '$325-350' },
  { value: 17, label: '$350-375' },
  { value: 13, label: '$375-400' },
  { value: 9, label: '$400-425' },
  { value: 6, label: '$425-450' },
  { value: 3, label: '$450-475' },
  { value: 1, label: '$475-500' },
];

export default function PopoverDemo() {
  const [range, setRange] = useState<[number, number]>([100, 350]);

  return (
    <div className="flex justify-center">
      <Popover
        renderTrigger={<Button icon={<PiFunnel />}>Filter</Button>}
        width={300}
      >
        <div className="flex flex-col gap-4">
          <Historgram data={priceData} range={range} min={0} max={500} />
          <Slider.Range min={0} max={500} value={range} onChange={setRange} />
          <div className="grid grid-cols-2 gap-4">
            <Form.Field label="Min" htmlFor="popover-price-min">
              <FormatInput.Numeric
                id="popover-price-min"
                prefix="$"
                thousandSeparator
                value={range[0]}
                onValueChange={(values) =>
                  setRange([values.floatValue ?? 0, range[1]])
                }
              />
            </Form.Field>
            <Form.Field label="Max" htmlFor="popover-price-max">
              <FormatInput.Numeric
                id="popover-price-max"
                prefix="$"
                thousandSeparator
                value={range[1]}
                onValueChange={(values) =>
                  setRange([range[0], values.floatValue ?? 0])
                }
              />
            </Form.Field>
          </div>
        </div>
      </Popover>
    </div>
  );
}

API

Historgram is presentational — its bars have pointer-events: none and cannot be clicked or dragged directly. Pair it with an interactive range control such as Slider.Range (see Usage) so range updates from user input.

PropDescriptionTypeDefault
dataBars to render, one per bin. label is part of the shape but not currently rendered by the component.{ value: number; label: string }[]
rangeCurrently highlighted sub-range; bars whose bin center falls inside it render in the active color.[number, number]
minLower bound of the domain data spans.number
maxUpper bound of the domain data spans.number
classNameClass names applied to every bar. Overriding a bg-* utility recolors all bars uniformly and loses the active/inactive distinction; shape or spacing utilities like rounded-* and mx-* override safely.string