phiz

Guides

Large data

phiz embeds inline data in the specification, and the browser keeps every row in memory to draw it. A few thousand rows render immediately. Hundreds of thousands of rows make the page heavy and slow, so large data should be summarized before it is sent. phiz does that summarizing in PHP.

Sizes to expect

The thresholds below are rough guidance. The limits in practice depend on the mark type, the number of layers and interactions, the browser, and the device. Only the warning threshold is fixed by phiz.

Rows embedded Effect
up to about 5,000 renders immediately, and interaction stays smooth
5,000 to 20,000 renders, but brushing and tooltips slow down
above 20,000 phiz raises a warning; the page grows by several megabytes
millions summarize in PHP or in the database

Config::$warnRows sets the warning threshold (20,000 by default, or null to turn it off). In Laravel it is config('phiz.warn_rows'). The warning is an E_USER_WARNING that names the alternatives below.

Binning in PHP

hex(), bin2d(), and histogram() count in PHP and embed only the counts. hex() always bins in PHP. bin2d() and histogram() take compute:, which is 'server', 'browser', or 'auto'. The default 'auto' bins in PHP above Config::$serverRows rows (10,000 by default, or config('phiz.server_rows')). The examples on this page use 200,000 points.

use Phiz\Plot;

$points = require __DIR__.'/_cloud.php'; // 200,000 rows as columns

$plot = Plot::of($points)
    ->aes(x: 'x', y: 'y')
    ->hex(bins: 40)
    ->scaleFill(scheme: 'viridis')
    ->labs(title: '200,000 points in hexagonal bins');

The hexagons are sized from the panel width, so neighbors tile at any plot size. A faceted plot bins each panel separately, and the specification still holds only the cells.

use Phiz\Plot;

$points = require __DIR__.'/_cloud.php';

// Above Config::$serverRows rows, bin2d() counts in PHP; the spec holds only the cells.
$plot = Plot::of($points)
    ->aes(x: 'x', y: 'y')
    ->bin2d(bins: 40)
    ->facetWrap('group')
    ->size(240, 220)
    ->scaleFill(scheme: 'blues');

Histograms computed in PHP use exactly bins equal-width bins, or binwidth and boundary, and stack or overlap by group as usual.

use Phiz\Plot;

$points = require __DIR__.'/_cloud.php';

$plot = Plot::of($points)
    ->aes(x: 'x', fill: 'group')
    ->histogram(bins: 60, position: 'identity', alpha: 0.6)
    ->labs(fill: null);

Tables stay columnar

A phibble table or an array of columns is kept as given. The binning geoms read it column by column, and rows are built only if the specification embeds them. These timings were measured on a laptop on 2026-10-16, with phiz 0.3.0 and a million-row phibble table:

Plot Time Spec size
hex(bins: 40) 0.9 s 49 KB
bin2d(bins: 50) 0.6 s 132 KB
histogram(bins: 60) 0.4 s 6 KB

Peak memory was about 100 MB, of which the table took 51 MB.

Downsampling lines

line(downsample: n) keeps about n points per group with Largest-Triangle-Three-Buckets (LTTB). LTTB picks the points that preserve the line’s visible shape, including its peaks.

use Phiz\Plot;

// A 100,000-point random walk, drawn from 800 points chosen by Largest-Triangle-Three-Buckets.
$rng = new Random\Randomizer(new Random\Engine\Mt19937(7));
$walk = [];
$level = 0.0;
for ($t = 0; $t < 100_000; $t++) {
    $level += $rng->getFloat(-1, 1);
    $walk[] = ['t' => $t, 'level' => $level];
}

$plot = Plot::of($walk)
    ->aes(x: 't', y: 'level')
    ->line(downsample: 800, color: '#1F77B4')
    ->size(560, 220);

The Bin helpers

The Bin class exposes the same computations for data you summarize yourself. Each helper returns a short list of rows. The helpers read phibble tables, column arrays, row lists, or any iterable of rows. A generator streams in one pass when the limits are given.

use Phiz\Bin;

Bin::histogram($source, 'price', bins: 50);          // price_start, price_end, count, density
Bin::grid($source, 'x', 'y', bins: [40, 30]);         // x_start, x_end, y_start, y_end, count
Bin::hex($source, 'x', 'y', bins: 30, aspect: 0.75);  // hexagon centers and count
Bin::lttb($rows, 'time', 'value', threshold: 1000);   // the rows LTTB keeps
Bin::sample($source, 5000, seed: 1);                  // a reproducible uniform sample

// One pass over a database cursor, with known limits:
$bins = Bin::grid(Car::query()->cursor(), 'weight', 'mpg', bins: 40, xlim: [1500, 5500], ylim: [5, 50]);

Plot::of($bins)
    ->rect(aes(xmin: 'weight_start', xmax: 'weight_end', ymin: 'mpg_start', ymax: 'mpg_end', fill: 'count'));

Other approaches

  • Summarize first. phplyr’s group_by() and summarize(), or a SQL GROUP BY, usually answers the question with far fewer rows. Dashboards with Livewire shows charts built from summaries.
  • Load from a URL. Data::url('/data/points.csv') lets the browser fetch the file, so the page itself stays small. The browser still holds every row, so this helps page size, not drawing speed.
  • Sample. Bin::sample() keeps a uniform random subset when a scatterplot of individual points matters more than exact counts.

From phpstats/phiz@bc7ed3e.