Skip to content
Brok UI

Loading…

No results

Charts

Open source

Token-themed data visualisations — the core multi-series chart plus dependency-free SVG variants for sankey, word-cloud, venn and violin plots.

Version
v1.1.1
Stability
stable
License
MIT
Related
Chart
7 of 7

Violin Chart — A dependency-free, token-themed SVG violin plot that mirrors each group's density profile left and right of a centre axis, with a median marker and baseline group labels.

Preview

A dependency-free SVG violin plot. Pass `:groups` — each `[label, density, range]` with optional `median`, `q1` and `q3` in chart units — where `density` is the half-width profile sampled low→high across `range`; the value axis, gridlines and quartile box come from the data.

previews.components.chart-violin.default.blade.php Blade
<div class="space-y-4">
    <x-ui.chart-violin :height="320" unit="ms" label="{{ __('Response latency per quarter') }}" />

    <p class="text-sm text-muted-foreground">
        {{ __('A dependency-free SVG violin plot. Pass `:groups` — each `[label, density, range]` with optional `median`, `q1` and `q3` in chart units — where `density` is the half-width profile sampled low→high across `range`; the value axis, gridlines and quartile box come from the data.') }}
    </p>
</div>

Installation

terminal
php artisan ui:add chart-violin

Registry contract

php artisan ui:add chart-violin writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/chart-violin.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

chart-violin.md
# Brok UI: Violin Chart (`chart-violin`)

A dependency-free, token-themed SVG violin plot that mirrors each group's density profile left and right of a centre axis, with a median marker and baseline group labels.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add chart-violin
```

## Usage

```blade
<div class="space-y-4">
    <x-ui.chart-violin :height="320" unit="ms" label="{{ __('Response latency per quarter') }}" />

    <p class="text-sm text-muted-foreground">
        {{ __('A dependency-free SVG violin plot. Pass `:groups` — each `[label, density, range]` with optional `median`, `q1` and `q3` in chart units — where `density` is the half-width profile sampled low→high across `range`; the value axis, gridlines and quartile box come from the data.') }}
    </p>
</div>
```

## Props

- `groups` (mixed|null, default `null`) — Groups to plot: each an object with label, a density profile (half-width samples low to high across range), range [low, high], and optional median, q1 and q3. Defaults to demo latency data.
- `domain` (mixed|null, default `null`) — Value-axis domain [low, high]; defaults to the union of every group's range.
- `ticks` (int, default `5`) — Number of axis tick marks.
- `unit` (string, default ``) — Suffix appended to tick labels and readouts, such as "ms".
- `showBox` (bool, default `true`) — Draws the inner quartile box and median dot when q1, q3 and median are given.
- `height` (int, default `320`) — SVG height in pixels; width fills the container.
- `label` (string, default `Violin chart`) — Accessible name for the figure.

## Use when

- Use when trend, distribution, flow, intensity, or relationship questions are easier to answer visually than in raw numbers.
- Comparing the full distribution shape of a value across groups, such as latency per quarter, not just their averages.
- Wanting quartile and median markers layered on top of the distribution shape in one figure.

## Avoid when

- Do not use a chart when users mainly need exact lookup or when a single metric would be clearer as text or a stat.
- Only a single summary statistic per group is needed; use chart's bar type instead.
- The distribution's density profile is not already known or computable; a box plot or bar chart needs less input data.

## Anti-patterns

- Decorative charts without a meaningful question

## Rules

- Use the `<brok:chart-violin>` tag (or `<x-ui.chart-violin>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/chart-violin
- Registry JSON (files, props, contract): https://brokui.dev/r/open/chart-violin.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

latency.blade.php Blade
{{-- The default density plot framed by chart-card; a falling latency is good
     news, so the badge tone is set by hand. --}}
<x-ui.chart-card heading="Response time distribution" subheading="Latency density by quarter (ms)" trend="−12ms" trend-variant="success">
    <x-ui.chart-violin :height="320" label="{{ __('Latency density by quarter') }}" />
</x-ui.chart-card>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
groups mixed | null null Groups to plot: each an object with label, a density profile (half-width samples low to high across range), range [low, high], and optional median, q1 and q3. Defaults to demo latency data.
domain mixed | null null Value-axis domain [low, high]; defaults to the union of every group's range.
ticks int 5 Number of axis tick marks.
unit string Suffix appended to tick labels and readouts, such as "ms".
showBox bool true Draws the inner quartile box and median dot when q1, q3 and median are given.
height int 320 SVG height in pixels; width fills the container.
label string Violin chart Accessible name for the figure.

Slots

  • default — Unused; the chart renders from the groups prop only.

Data slots

Stable hooks for CSS overrides and browser tests.

chart-violin chart-violin-body chart-violin-box chart-violin-group chart-violin-label chart-violin-median

Behavior

  • Renders without arguments using built-in demo data, so it previews immediately.
  • Mirrors each group's density profile left and right of a centre axis to form the violin shape, and overlays a quartile box and median marker when showBox and quartile values are given.
  • Dependency-free SVG: no chart library, computed and laid out entirely in Blade.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Chart and visual analytics

Reveal a visual trend, distribution, flow, or relationship.

Use when

  • Use when trend, distribution, flow, intensity, or relationship questions are easier to answer visually than in raw numbers.
  • Comparing the full distribution shape of a value across groups, such as latency per quarter, not just their averages.
  • Wanting quartile and median markers layered on top of the distribution shape in one figure.

Avoid when

  • Do not use a chart when users mainly need exact lookup or when a single metric would be clearer as text or a stat.
  • Only a single summary statistic per group is needed; use chart's bar type instead.
  • The distribution's density profile is not already known or computable; a box plot or bar chart needs less input data.

Use instead

  • Table for exact lookup
  • Stat for a single value

Anti-patterns

  • Decorative charts without a meaningful question
Anatomy
root chart-violin-group chart-violin-body chart-violin-box chart-violin-median chart-violin-label
Theming hooks
chart-violin chart-violin-body chart-violin-box chart-violin-group chart-violin-label chart-violin-median

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
No component-owned keyboard interaction; native element behavior applies.
Focus
none
  • The figure carries role="img" with an aria-label from label, so a screen reader hears a named summary instead of parsing raw SVG.
  • Group labels render as visible baseline text so each violin is identifiable without relying on colour or position alone.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Safe

Livewire can update this component through forwarded wire:* attributes.

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/chart-violin.blade.php Blade
@props([
    // Groups: each ['label' => string, 'density' => [floats], 'range' => [low, high],
    //               'median' => value, 'q1' => value, 'q3' => value].
    // `density` is the half-width profile sampled low→high across `range`
    // (values in the chart's units); `median`/`q1`/`q3` are also in units.
    'groups' => null,
    // Value-axis domain [low, high]; defaults to the union of the group ranges.
    'domain' => null,
    'ticks' => 5,
    // Suffix for tick labels + readouts (e.g. "ms").
    'unit' => '',
    // Draw the inner quartile box + median dot when q1/q3/median are given.
    'showBox' => true,
    'height' => 320,
    'label' => 'Violin chart',
])

@php
    // ── Demo data (response latency per quarter, in ms) so it renders without args ──
    $groups ??= [
        ['label' => __('Q1'), 'range' => [40, 360], 'density' => [0.02, 0.10, 0.30, 0.62, 0.92, 1.00, 0.84, 0.58, 0.34, 0.18, 0.09, 0.04, 0.01], 'q1' => 118, 'median' => 152, 'q3' => 196],
        ['label' => __('Q2'), 'range' => [50, 300], 'density' => [0.02, 0.14, 0.40, 0.78, 1.00, 0.90, 0.60, 0.32, 0.14, 0.05, 0.01], 'q1' => 112, 'median' => 138, 'q3' => 168],
        ['label' => __('Q3'), 'range' => [30, 250], 'density' => [0.03, 0.18, 0.52, 0.90, 1.00, 0.74, 0.44, 0.22, 0.10, 0.04, 0.01], 'q1' => 84, 'median' => 108, 'q3' => 136],
        ['label' => __('Q4'), 'range' => [20, 200], 'density' => [0.02, 0.22, 0.66, 1.00, 0.86, 0.52, 0.26, 0.12, 0.05, 0.01], 'q1' => 58, 'median' => 76, 'q3' => 98],
    ];

    $showBox = filter_var($showBox, FILTER_VALIDATE_BOOLEAN);
    $ticks = max(2, (int) $ticks);

    // ── Domain ─────────────────────────────────────────────────────────────
    if (! is_array($domain) || count($domain) < 2) {
        $lows = $highs = [];
        foreach ($groups as $g) {
            $r = $g['range'] ?? [0, 1];
            $lows[] = (float) $r[0];
            $highs[] = (float) $r[1];
        }
        $domain = [min($lows ?: [0]), max($highs ?: [1])];
    }
    [$dLo, $dHi] = [(float) $domain[0], (float) $domain[1]];
    if ($dHi <= $dLo) {
        $dHi = $dLo + 1;
    }

    // ── Geometry ───────────────────────────────────────────────────────────
    $W = 720;
    $H = max(160, (int) $height);
    $axisW = 44;                // room for tick labels on the start side
    $top = 12;
    $axisY = $H - 28;
    $plotH = $axisY - $top;
    $plotX0 = $axisW;
    $plotW = $W - $axisW - 8;

    $count = max(1, count($groups));
    $colW = $plotW / $count;
    $halfMax = min($colW, 120) / 2 - 10;

    $yFor = fn (float $v) => $axisY - (($v - $dLo) / ($dHi - $dLo)) * $plotH;
    $palette = ['text-chart-1', 'text-chart-2', 'text-chart-3', 'text-chart-4', 'text-chart-5'];
    $fmt = fn (float $v) => (floor($v) == $v ? number_format($v) : number_format($v, 1)).$unit;

    // Catmull-Rom → cubic Bézier, so the density outline is a smooth curve
    // rather than a polyline through the samples.
    $smooth = function (array $pts): string {
        $n = count($pts);
        if ($n < 2) {
            return '';
        }
        $d = '';
        for ($i = 0; $i < $n - 1; $i++) {
            $p0 = $pts[max(0, $i - 1)];
            $p1 = $pts[$i];
            $p2 = $pts[$i + 1];
            $p3 = $pts[min($n - 1, $i + 2)];
            $c1x = $p1[0] + ($p2[0] - $p0[0]) / 6;
            $c1y = $p1[1] + ($p2[1] - $p0[1]) / 6;
            $c2x = $p2[0] - ($p3[0] - $p1[0]) / 6;
            $c2y = $p2[1] - ($p3[1] - $p1[1]) / 6;
            $d .= sprintf('C%.2f %.2f %.2f %.2f %.2f %.2f ', $c1x, $c1y, $c2x, $c2y, $p2[0], $p2[1]);
        }

        return $d;
    };

    $violins = [];
    foreach (array_values($groups) as $i => $g) {
        $density = array_values(array_map('floatval', $g['density'] ?? []));
        $n = count($density);
        if ($n < 2) {
            continue;
        }
        [$lo, $hi] = array_map('floatval', $g['range'] ?? [$dLo, $dHi]);
        $cx = $plotX0 + $colW * ($i + 0.5);
        $peak = max($density) ?: 1.0;

        $right = $left = [];
        foreach ($density as $k => $dv) {
            $v = $lo + ($k / ($n - 1)) * ($hi - $lo);
            $y = $yFor($v);
            $hw = ($dv / $peak) * $halfMax;
            $right[] = [$cx + $hw, $y];
            $left[] = [$cx - $hw, $y];
        }

        // Closed outline: up the right edge, down the left, pinched to the
        // centre line at both ends so the tails taper.
        $path = sprintf('M%.2f %.2f ', $cx, $yFor($lo))
            .$smooth(array_merge([[$cx, $yFor($lo)]], $right, [[$cx, $yFor($hi)]]))
            .$smooth(array_merge([[$cx, $yFor($hi)]], array_reverse($left), [[$cx, $yFor($lo)]]))
            .'Z';

        $violins[] = [
            'label' => $g['label'] ?? '',
            'color' => $palette[$i % count($palette)],
            'd' => $path,
            'cx' => $cx,
            'lo' => $yFor($lo),
            'hi' => $yFor($hi),
            'median' => isset($g['median']) ? $yFor((float) $g['median']) : null,
            'q1' => isset($g['q1']) ? $yFor((float) $g['q1']) : null,
            'q3' => isset($g['q3']) ? $yFor((float) $g['q3']) : null,
            'summary' => isset($g['median']) ? __('median :value', ['value' => $fmt((float) $g['median'])]) : '',
        ];
    }

    // "Nice" tick steps (1/2/5 × 10ⁿ) inside the domain, so the axis reads
    // 50/100/150 rather than 20/105/190.
    $rawStep = ($dHi - $dLo) / ($ticks - 1);
    $mag = 10 ** floor(log10($rawStep));
    $step = collect([1, 2, 2.5, 5, 10])->map(fn ($m) => $m * $mag)->first(fn ($st) => $st >= $rawStep);
    $tickValues = [];
    for ($tv = ceil($dLo / $step) * $step; $tv <= $dHi + 1e-9; $tv += $step) {
        $tickValues[] = $tv;
    }
@endphp

<div
    data-slot="chart-violin"
    role="img"
    aria-label="{{ $label }}"
    {{ $attributes->merge(['class' => 'relative w-full text-foreground']) }}
>
    <svg
        viewBox="0 0 {{ $W }} {{ $H }}"
        width="100%"
        height="{{ $H }}"
        preserveAspectRatio="xMidYMid meet"
        class="h-auto w-full"
        fill="none"
        aria-hidden="true"
    >
        {{-- Value axis: hairline gridlines + muted tick labels. --}}
        @foreach ($tickValues as $tv)
            @php($ty = $yFor($tv))
            <line
                class="stroke-border"
                x1="{{ $plotX0 }}"
                y1="{{ round($ty, 2) }}"
                x2="{{ $W - 8 }}"
                y2="{{ round($ty, 2) }}"
                stroke="currentColor"
                stroke-width="1"
            />
            <text
                class="fill-muted-foreground text-2xs tabular-nums"
                x="{{ $plotX0 - 8 }}"
                y="{{ round($ty, 2) }}"
                text-anchor="end"
                dominant-baseline="middle"
            >{{ $fmt($tv) }}</text>
        @endforeach

        @foreach ($violins as $v)
            <g data-slot="chart-violin-group" class="{{ $v['color'] }}">
                {{-- Density body, mirrored left/right of the centre axis. --}}
                <path
                    data-slot="chart-violin-body"
                    class="transition-opacity duration-200 hover:opacity-90"
                    d="{{ $v['d'] }}"
                    fill="currentColor"
                    fill-opacity="0.35"
                    stroke="currentColor"
                    stroke-width="1.5"
                    stroke-linejoin="round"
                />

                @if ($showBox && $v['q1'] !== null && $v['q3'] !== null)
                    {{-- Interquartile box + whisker to the range ends. --}}
                    <line
                        class="stroke-foreground/50"
                        x1="{{ round($v['cx'], 2) }}"
                        y1="{{ round($v['lo'], 2) }}"
                        x2="{{ round($v['cx'], 2) }}"
                        y2="{{ round($v['hi'], 2) }}"
                        stroke="currentColor"
                        stroke-width="1"
                    />
                    <rect
                        data-slot="chart-violin-box"
                        class="fill-foreground"
                        x="{{ round($v['cx'] - 4, 2) }}"
                        y="{{ round($v['q3'], 2) }}"
                        width="8"
                        height="{{ round(max(1, $v['q1'] - $v['q3']), 2) }}"
                        rx="2"
                        fill="currentColor"
                    />
                @endif

                @if ($v['median'] !== null)
                    {{-- Median marker: a page-coloured dot so it reads on the box. --}}
                    <circle
                        data-slot="chart-violin-median"
                        class="fill-background stroke-foreground"
                        cx="{{ round($v['cx'], 2) }}"
                        cy="{{ round($v['median'], 2) }}"
                        r="3"
                        fill="currentColor"
                        stroke="currentColor"
                        stroke-width="1.5"
                    >
                        <title>{{ $v['summary'] }}</title>
                    </circle>
                @endif
            </g>

            {{-- Group label under the axis. --}}
            <text
                data-slot="chart-violin-label"
                class="fill-foreground text-2xs font-medium"
                x="{{ round($v['cx'], 2) }}"
                y="{{ $axisY + 18 }}"
                text-anchor="middle"
                dominant-baseline="middle"
            >{{ $v['label'] }}</text>
        @endforeach

        {{-- Baseline axis. --}}
        <line
            class="stroke-border"
            x1="{{ $plotX0 }}"
            y1="{{ $axisY }}"
            x2="{{ $W - 8 }}"
            y2="{{ $axisY }}"
            stroke="currentColor"
            stroke-width="1"
        />
    </svg>
</div>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.1
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0