A dynamic chart can read current PostgreSQL results whenever a page loads, or request new results after a filter change or on a schedule. A practical implementation keeps database credentials and SQL on the server: the browser requests an endpoint, PHP validates the request and runs a parameterized PostgreSQL query, PHP returns a small JSON document, and JavaScript renders or updates a chart from that response.
This guide uses PDO_PGSQL, PostgreSQL, and Chart.js. They are one workable stack, not the only way to build the data path.
What the PHP–PostgreSQL chart data path looks like
The browser should never connect directly to PostgreSQL. A request such as /api/sales.php?from=2026-01-01&to=2026-02-01 reaches PHP, which performs these steps:
- Parse and validate the dates and any filter values.
- Bind literal values to a prepared SQL statement.
- Aggregate and sort only the fields needed by the chart.
- Encode labels and numeric series as JSON with an
application/jsonresponse. - Let browser JavaScript pass that JSON to a chart configuration.
With this design, “dynamic on page load” means the query runs for each request. A filter or periodic refresh uses the same endpoint again and updates the existing chart instance.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
- Used Book in Good Condition
Install the PHP PostgreSQL driver safely
PHP’s PDO API is a common database interface, but it requires a database-specific driver. For PostgreSQL that driver is PDO_PGSQL, which depends on the PostgreSQL libpq client library. PHP 8.4 and later require libpq 10.0 or newer. Check the deployed runtime, not only a development machine, and enable the extension through the package or configuration method used by your operating system.
See the PDO overview and PDO_PGSQL documentation for version and installation details. Keep the connection string, username, and password in environment variables or a secrets manager, outside source control. Use a database role that can read only the required tables or views.
Design a query for the chart’s actual question
For a time series, PostgreSQL should do the filtering and aggregation before PHP receives the rows. The example below groups events into daily buckets. The reporting timezone is explicit so a daylight-saving transition or a server timezone cannot silently change the meaning of a day.
Rank #2
- Used Book in Good Condition
SELECT date_trunc('day', occurred_at AT TIME ZONE 'UTC') AS bucket,
sum(amount) AS total_amount
FROM sales
WHERE occurred_at >= :from_time
AND occurred_at < :to_time
GROUP BY bucket
ORDER BY bucket;
date_trunc supports other precisions such as hour or month. Choose a timezone and interval convention that matches the business question, document it, and use a half-open range (>= from and < to) to avoid double-counting adjacent requests. PostgreSQL’s date/time behavior is documented at its date/time functions reference.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Build a JSON endpoint with PDO and prepared statements
The following endpoint is intentionally small. It accepts ISO dates, rejects an invalid or reversed range, binds values, and returns labels plus numeric values. Adapt table and column names to your schema.
<?php
// api/sales.php
header('Content-Type: application/json; charset=utf-8');
$from = filter_input(INPUT_GET, 'from', FILTER_UNSAFE_RAW);
$to = filter_input(INPUT_GET, 'to', FILTER_UNSAFE_RAW);
$isoDate = static function ($value): bool {
if (!is_string($value)) {
return false;
}
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
return $date !== false && $date->format('Y-m-d') === $value;
};
if (!$isoDate($from) || !$isoDate($to) || $from >= $to) {
http_response_code(400);
echo json_encode(['error' => 'Use a valid, non-empty date range.']);
exit;
}
try {
$dsn = sprintf(
'pgsql:host=%s;port=%s;dbname=%s',
$_ENV['PGHOST'], $_ENV['PGPORT'] ?? '5432', $_ENV['PGDATABASE']
);
$pdo = new PDO($dsn, $_ENV['PGUSER'], $_ENV['PGPASSWORD'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$sql = <<<'SQL'
SELECT date_trunc('day', occurred_at AT TIME ZONE 'UTC') AS bucket,
COALESCE(sum(amount), 0) AS total_amount
FROM sales
WHERE occurred_at >= :from_time
AND occurred_at < :to_time
GROUP BY bucket
ORDER BY bucket
SQL;
$stmt = $pdo->prepare($sql);
$stmt->execute([
':from_time' => $from . ' 00:00:00+00',
':to_time' => $to . ' 00:00:00+00',
]);
$labels = [];
$values = [];
foreach ($stmt as $row) {
$labels[] = (new DateTimeImmutable($row['bucket']))->format('Y-m-d');
$values[] = (float) $row['total_amount'];
}
echo json_encode([
'labels' => $labels,
'datasets' => [[
'label' => 'Sales',
'data' => $values,
]],
], JSON_THROW_ON_ERROR);
} catch (Throwable $e) {
error_log($e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'The chart data could not be loaded.']);
}
Prepared-statement placeholders represent complete data values; they cannot stand in for a table name, column name, sort direction, or other SQL syntax. If a user can choose among dimensions, map the submitted value to a fixed server-side allowlist and insert only the selected trusted identifier. The PHP rules are described in PDO prepared statements.
Rank #3
Do not send SQL errors, stack traces, credentials, or unrestricted rows to the browser. Add authorization checks when the underlying data is not public. A production endpoint may also cap the maximum date span and apply pagination or a server-side limit.
Render the first chart in the browser
Chart.js uses a canvas element and a JavaScript configuration containing a chart type, labels, and datasets. It can be loaded as a script or integrated through a module bundler; bundler projects may need to import and register the chart components described in the Chart.js integration guide.
<label>
From
<input id="from" type="date" value="2026-01-01">
</label>
<label>
To
<input id="to" type="date" value="2026-02-01">
</label>
<button id="load" type="button">Load chart</button>
<p id="status" role="status"></p>
<canvas id="sales-chart" aria-label="Sales by day" role="img"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const canvas = document.getElementById('sales-chart');
const status = document.getElementById('status');
let chart;
async function loadChart() {
const from = document.getElementById('from').value;
const to = document.getElementById('to').value;
status.textContent = 'Loading…';
try {
const response = await fetch(
`/api/sales.php?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
{ headers: { Accept: 'application/json' } }
);
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || 'Request failed');
if (chart) {
chart.data.labels = payload.labels;
chart.data.datasets = payload.datasets;
chart.update();
} else {
chart = new Chart(canvas, {
type: 'line',
data: {
labels: payload.labels,
datasets: payload.datasets.map(dataset => ({
...dataset,
borderColor: '#1769aa',
backgroundColor: 'rgba(23, 105, 170, .15)',
tension: 0.2,
})),
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true, title: { display: true, text: 'Amount' } },
},
},
});
}
status.textContent = payload.labels.length ? '' : 'No data for this range.';
} catch (error) {
status.textContent = error.message;
}
}
document.getElementById('load').addEventListener('click', loadChart);
loadChart();
</script>
Use text nodes and chart data for values returned by the server; do not build a response by concatenating untrusted HTML. The official Chart.js usage guide covers canvas markup and configuration.
Rank #4
- Used Book in Good Condition
Refresh data without recreating the page
The example creates one chart and calls chart.update() after later requests. That is the useful distinction between an initial dynamic chart and a refreshed chart: the database query runs again, but the existing visual is reused.
setInterval(loadChart, 60_000); // optional one-minute refresh
For interactive filters, call loadChart from the filter’s change event. Disable controls or show a loading state while a request is pending, decide how to handle overlapping requests, and keep an explicit empty-data message. If the endpoint returns an error, leave the last valid chart visible or show a clear recovery action rather than replacing the canvas with server output.
Choose a chart type and make missing data explicit
Line charts for ordered trends
Use a line when the x-axis is ordered time or another continuous sequence. Decide whether a missing bucket means zero, unknown, or “no observation”; those choices produce different visuals. The SQL above emits only buckets present in the result, so a calendar table or series-generation query may be needed when every day must appear.
Bar charts for category comparisons
Use bars for discrete categories such as product or region. Sort categories deliberately and label the unit. If categories are user-selectable, map the selected column through a trusted allowlist rather than binding it as a placeholder.
Scatter charts for paired numeric values
Use scatter points when each observation is an independent x/y pair. Preserve numeric types in JSON and document units so the axes are not mistaken for dates or categories.
Keep large series usable
A chart cannot communicate thousands of nearly indistinguishable points on a small screen. Aggregate at an appropriate interval, constrain the requested range, or return the resolution the display needs. For larger line-series, prepare sorted and normalized data where appropriate and consider Chart.js decimation. Its guidance is at the Chart.js performance documentation. Do not claim a speed improvement without measuring your own schema, query, network, and browser workload.
Test the boundaries that change the meaning
- Reject missing, malformed, reversed, or excessively long date ranges.
- Check an empty result and distinguish it from a failed request.
- Decide how SQL
NULLvalues map to JSON and chart gaps. - Test timezone boundaries, including daylight-saving transitions when relevant.
- Verify that labels are sorted and that every dataset has the intended length.
- Try each allowed category and confirm that an unknown identifier is rejected.
- Test long ranges for query cost, response size, and chart readability.
- Confirm that database failures are logged privately while clients receive a safe message.
Browser charts versus server-generated images
There is no universal winner; choose according to the output and operating constraints.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →| Decision factor | Browser JavaScript chart | Server-generated image |
|---|---|---|
| Interaction and refresh | Well suited to filters, tooltips, zooming, and repeated JSON refreshes. | Usually requires generating and replacing a new image. |
| Accessibility and fallback | Needs a meaningful label, accessible surrounding text, and a data fallback where required. | Needs alt text and often a separate textual or tabular representation. |
| Dataset size | Browser memory and drawing cost matter; aggregate and decimate large series. | Rendering work stays on the server, but image generation and transfer still cost resources. |
| Deployment dependencies | Requires browser JavaScript and a chart library delivered as a script or bundle. | Requires a server-side image renderer and its fonts or graphics dependencies. |
| Exports and static documents | Can support browser export features, subject to the library and setup. | Natural for email, reports, and fixed snapshots. |
The PHP–PostgreSQL boundary remains similar either way: validate on the server, query safely, and return only the data or rendered result the client is allowed to see.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

