Introduction: From Status Indicators to Transformed Time-Series

In the previous lesson, you learned how to use aggregate functions like AVG() to display a single summary value in a Stat Panel. That was all about creating status indicators — panels that answer "What's the overall situation right now?" at a glance.

Now we're going to explore a different visualization need: showing transformed metrics over time. Instead of aggregating many rows into one number, you'll transform each data point into something more meaningful before Grafana plots it. Think of it this way: aggregate functions help you create status panels (one big number), while calculations help you create insightful time-series graphs (lines that tell a story).

The Problem: Raw Metrics vs. Operational Insights

Here's the visualization problem we're solving. Imagine you have a time-series panel showing "CPU Usage" hovering around 75%. That's useful information, but when stakeholders look at your graphs during a planning meeting, they're asking: "Can we handle more load?" or "Do we have room to deploy this new feature?" A graph showing "CPU Headroom" at 25% answers that question directly. Same underlying data, but visualized in a way that matches how people think about operational capacity.

This concept of CPU headroom — the percentage of CPU capacity still available — is what we call a derived metric. It doesn't exist in your database tables, but you can calculate it in your query so Grafana displays it as if it were a real column. By the end of this lesson, you'll know how to create time-series panels that show derived metrics and how to name them properly so your panel legends are clear and professional.

Creating Derived Metrics for Time-Series Panels

Let's talk about what Grafana needs from your query to plot a proper time-series graph. You already know the basics: a timestamp column, a metric name for the legend, and values to plot. What you haven't done yet is transform those values before Grafana sees them. That's what derived metrics are all about — performing calculations in your query so Grafana receives exactly what you want to visualize.

When you're building a time-series panel in Grafana, you're not just pulling data — you're deciding what story you want the graph to tell. If you query usage directly from metrics_cpu, your graph shows a line representing consumption. But if you calculate 100 - usage in your query, Grafana receives headroom values instead, and your graph shows a line representing available capacity. The visualization completely changes the message.

Here's what that looks like in practice. In your query editor, you write:

When Grafana executes this query, it receives three columns: formatted timestamps, a metric name, and calculated values. It doesn't know or care that you performed arithmetic — it just sees a column named value and plots those numbers over time. This is the beauty of derived metrics: you control exactly what Grafana visualizes by transforming data in your query.

Notice how the calculation 100 - usage happens for each row individually. If your database has measurements every 10 seconds over a 6-hour period, that's about 2,160 data points. Grafana will plot all 2,160 calculated headroom values as a continuous line on your graph. The transformation happens row-by-row, preserving your time-series structure so you get a proper line graph showing how headroom changes over time.

This is fundamentally different from the aggregate functions you used in Stat Panels. Those collapsed many rows into one number for a status indicator. Derived metrics transform each row individually for time-series visualizations. Both techniques are essential in Grafana — aggregates for current status, calculations for trends and patterns.

Designing Clear Panel Legends

One of the most important aspects of visualization design is making sure anyone can understand what they're looking at without hunting through documentation or asking questions. This is where custom metric naming becomes critical. When someone opens your panel at 3 a.m. to investigate an alert, your legend needs to be instantly clear.

Here's the problem: when you write a calculated column like 100 - usage without specifying a name, PostgreSQL will name it something like ?column? or just show the expression 100 - usage. Imagine opening a time-series panel and seeing a legend that says:

  • ?column?

That's not helpful. Now imagine instead the legend shows:

  • CPU Headroom %

That's immediately understandable. The metric name tells you exactly what the graph is showing. This is what professional Grafana panels look like — every visualization has clear, descriptive labels that make it self-documenting.

In your query, you control the legend text using string literals in your SELECT statement. The pattern is 'Your Custom Name' AS metric. The single quotes indicate you're providing literal text, not referencing a database column. When you write 'CPU Headroom %' AS metric, Grafana uses that exact text in your panel's legend, tooltips, and anywhere else it needs to identify this series.

This becomes even more important when you're building multiple panels to monitor different aspects of your infrastructure. If one panel shows "CPU Headroom %" and another shows "Memory Headroom %", anyone viewing your graphs immediately understands that both panels are showing capacity metrics, just for different resources. The consistent naming pattern creates cohesive visualizations.

Think about usability from your users' perspective. Operations teams looking at your panels don't want to decipher technical column names or remember what various calculated expressions mean. They want to see "Network Throughput Mbps" or "Disk Space Available GB" — metric names that directly communicate what's being measured. That's what custom naming accomplishes.

Choosing Between Single-Series and Multi-Series Panels

Here's a critical distinction you need to understand about metric labels: constant string labels produce single series, while dynamic column labels produce multiple series. This choice fundamentally changes what your panel displays, and using the wrong approach will either collapse your data unexpectedly or create confusing visualizations.

When you use a constant string like 'CPU Headroom %' AS metric, you're telling Grafana that all rows in your query results belong to the same series. This works perfectly when you're querying data for a single host — you get one line on your graph with a clear label. But if you remove the host filter and query multiple hosts, Grafana will still see that constant string and collapse all hosts into a single series. Your panel will show one line that mixes data from host-a, host-b, and host-c together, which is almost never what you want.

The solution is to use dynamic labels when you need multiple series. Instead of 'CPU Headroom %' AS metric, you write host AS metric. Now Grafana creates a separate series for each distinct host value in your results. Your panel shows three distinct lines, each labeled with the appropriate hostname. If you need to include the metric type in the label, you can concatenate: host || ' CPU Headroom %' AS metric gives you labels like "host-a CPU Headroom %" and "host-b CPU Headroom %".

Here's the decision rule: use constant labels only when you're intentionally producing a single series (typically with a WHERE clause filtering to one resource). Use dynamic column labels when you want multiple series displayed on the same panel. Getting this wrong is one of the most common mistakes in Grafana query design, so always ask yourself: "How many lines should this panel show?" If the answer is "one line," use a constant label. If the answer is "one line per host" or "one line per database," use a dynamic label based on that dimension.

Filtering Data for Focused Visualizations

Grafana panels often need to show data for specific resources or segments. Maybe you want one panel showing CPU metrics for production servers and another panel showing the same metrics for development servers. Or perhaps you're creating a detailed view focused on a single high-priority host. This is where query filtering becomes essential for visualization design.

You're already familiar with Grafana's time range picker. When you select "Last 6 hours" or "Last 24 hours" in Explore, that selection determines what time range you're querying. That's what WHERE $__timeFilter(ts) does — it ensures your query respects the selected time range without you having to hardcode specific dates and times.

But now you need to add another dimension of filtering. Let's say you want a panel specifically showing CPU headroom for 'host-a'. You add this filtering condition to your query using AND host = 'host-a'. The complete filter becomes:

This compound filtering gives you precise control over what data Grafana visualizes in each panel. The time filter ensures your panel shows data from your selected time range, while the host filter ensures your panel only shows the specific resource you're monitoring. Both conditions must be satisfied for data to appear in your visualization.

Here's the complete query that creates a focused time-series panel showing CPU headroom for a specific host:

When you create a panel with this query, Grafana will draw a single line on the graph representing CPU headroom for host-a over your selected time range. If host-a had 30% headroom at 10:00 AM and 55% headroom at 11:00 AM, you'll see that exact trend in your visualization. The filtering ensures you're seeing precisely the data you need, while the calculation ensures you're seeing it in the most meaningful form.

This filtering approach is fundamental to building multiple related panels. In a dashboard, you can create panels side-by-side showing the same metric for different hosts. To add a new panel in dashboard mode, click the "Add" button in the top toolbar and select "Visualization". Each panel gets its own query, so you create the first panel with host = 'host-a', add a second panel with , and a third panel with . Grafana lets you resize and position these panels side-by-side, creating a focused view that makes it easy to compare CPU headroom across your infrastructure at a glance.

Summary and Practice Preview

You've just learned how to design Grafana time-series panels that display derived metrics — calculated values that don't exist in your database but provide more meaningful insights for monitoring. The key technique is performing arithmetic in your query (like 100 - usage) so Grafana receives the transformed data you want to visualize. You also learned how to create professional panel legends using custom metric names, and critically, when to use constant labels ('CPU Headroom %' AS metric) for single-series panels versus dynamic labels (host AS metric) for multi-series panels. Finally, you learned how to focus your visualizations using compound filtering with WHERE $__timeFilter(ts) AND host = 'host-a'. These techniques work together to create clear, focused panels that tell the right story.

In the upcoming practice exercises, you'll design panels showing various derived metrics like memory headroom, network throughput calculations, and disk capacity percentages. You'll experiment with both single-series and multi-series approaches, practice writing queries that produce clear, well-labeled visualizations, and explore different filtering combinations. Looking ahead to the next lesson, you'll learn about time bucketing using Grafana's $__interval feature. When your database has measurements every few seconds over long time ranges, time bucketing aggregates data into intervals (like 1-minute or 5-minute buckets) so Grafana displays smooth trends without overwhelming your panels — crucial for maintaining visualization performance while keeping your graphs meaningful.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal