How to Drill Down from a Chart to the Matching Rows in Oracle APEX

Build a bar chart and a report page, pass the clicked value or a hidden key through the series link, and test counts, cache clearing and checksums.

A chart shows totals, but users often want to see the rows behind a bar. With a drill-down, a click on a bar opens a report that shows exactly the rows counted in that bar. The user moves from the summary to the details in one click.

Oracle APEX supports this declaratively with a series link. This guide builds a bar chart page and a report page, passes the clicked value to a hidden page item, and filters the report with it. It also passes a column that the chart does not show, and tests the counts and a changed URL.

The overview post Oracle APEX Charts: A Complete Guide mentions series links in a few lines, and Tally-Style Drill-Down Reports in Oracle APEX with Keyboard Navigation drills from one report to the next. This guide is the complete build that starts from a chart click.

The guide uses orders as the example. The same steps work for any table.

Step 1: Create the Tables

The chart counts rows of the ORDERS table. The report also shows the customer name from CUSTOMERS and the order total from ORDER_LINES. If you start from scratch, create these tables with a few sample rows:

Example:

create table customers (
    customer_id number generated by default as identity
                constraint customers_pk primary key,
    name        varchar2(100) not null,
    email       varchar2(120) not null
);

create table products (
    product_id number generated by default as identity
               constraint products_pk primary key,
    name       varchar2(60)  not null,
    category   varchar2(30)  not null,
    price      number(10, 2) not null
);

create table orders (
    order_id    number generated by default as identity
                constraint orders_pk primary key,
    customer_id number not null
                constraint orders_customer_fk references customers,
    order_date  date default sysdate not null,
    status      varchar2(12) default 'NEW' not null
                constraint orders_status_ck
                check (status in ('NEW', 'APPROVED', 'SHIPPED', 'CANCELLED')),
    approved_by varchar2(60),
    approved_on date
);

create index orders_customer_ix on orders (customer_id);

create table order_lines (
    line_id    number generated by default as identity
               constraint order_lines_pk primary key,
    order_id   number not null
               constraint order_lines_order_fk references orders on delete cascade,
    product_id number not null
               constraint order_lines_product_fk references products,
    quantity   number not null
               constraint order_lines_qty_ck check (quantity > 0),
    unit_price number(10, 2) not null,
    constraint order_lines_product_uk unique (order_id, product_id)
);

create index order_lines_product_ix on order_lines (product_id);

insert into customers (name, email) values ('Customer 1', 'customer1@example.com');
insert into customers (name, email) values ('Customer 2', 'customer2@example.com');
insert into customers (name, email) values ('Customer 3', 'customer3@example.com');

insert into products (name, category, price) values ('Laptop Pro 14', 'Computers', 1450);
insert into products (name, category, price) values ('Monitor 27', 'Displays', 320);
insert into products (name, category, price) values ('Keyboard', 'Accessories', 45);

insert into orders (customer_id, order_date, status) values (1, trunc(sysdate) - 2, 'NEW');
insert into orders (customer_id, order_date, status) values (2, trunc(sysdate) - 5, 'NEW');
insert into orders (customer_id, order_date, status) values (3, trunc(sysdate) - 8, 'APPROVED');
insert into orders (customer_id, order_date, status) values (1, trunc(sysdate) - 12, 'SHIPPED');
insert into orders (customer_id, order_date, status) values (2, trunc(sysdate) - 15, 'CANCELLED');

insert into order_lines (order_id, product_id, quantity, unit_price) values (1, 1, 1, 1450);
insert into order_lines (order_id, product_id, quantity, unit_price) values (1, 3, 2, 45);
insert into order_lines (order_id, product_id, quantity, unit_price) values (2, 2, 2, 320);
insert into order_lines (order_id, product_id, quantity, unit_price) values (3, 1, 1, 1450);
insert into order_lines (order_id, product_id, quantity, unit_price) values (4, 2, 1, 320);
insert into order_lines (order_id, product_id, quantity, unit_price) values (5, 3, 4, 45);
commit;

select status, count(*) as orders
  from orders
 group by status
 order by status;

Output:

Table created.

Table created.

Table created.

Index created.

Table created.

Index created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

1 row created.

Commit complete.

STATUS           ORDERS
------------ ----------
APPROVED              1
CANCELLED             1
NEW                   2
SHIPPED               1

4 rows selected.
  • ORDERS_CUSTOMER_FK and ORDER_LINES_ORDER_FK link the three tables, and ORDERS_STATUS_CK allows only four status values.
  • ORDER_LINES_PRODUCT_UK stops the same product from appearing twice on one order.
  • The screenshots in this guide use 150 orders, so the counts you see will differ from the five sample orders.

These are the two queries the charts will use, run against the 150 orders of the screenshots:

Example:

select status, count(*) as orders
  from orders
 group by status
 order by status;

select to_char(order_date, 'YYYYMM') as month_key,
       to_char(trunc(order_date, 'MM'), 'Mon YYYY') as month_label,
       count(*) as orders
  from orders
 where order_date >= add_months(trunc(sysdate, 'MM'), -5)
 group by to_char(order_date, 'YYYYMM'), to_char(trunc(order_date, 'MM'), 'Mon YYYY')
 order by month_key;

Output:

STATUS          ORDERS
____________ _________
APPROVED            51
CANCELLED           16
NEW                 36
SHIPPED             47

MONTH_KEY    MONTH_LABEL       ORDERS
____________ ______________ _________
202605       May 2026              25
202606       Jun 2026              25
202607       Jul 2026              25
202608       Aug 2026              26
202609       Sep 2026              26
202610       Oct 2026              10

6 rows selected.

The second query returns MONTH_KEY next to the readable MONTH_LABEL. The chart will show the label, and the link will pass the key.

Step 2: Create the Chart Page

In App Builder, open the application and click Create Page. On the Component tab, click Chart:

Create a Page dialog with the Chart page type marked with a red box
The Chart page type.

The wizard asks for the chart type first. Click Bar and then Next:

Create Chart dialog with the Bar chart type marked with a red box
A bar chart.

Enter Page Number 330 and Name Orders by Status. Set Source Type to SQL Query and enter the status query:

Example:

select status, count(*) as orders
  from orders
 group by status
 order by status
Create Chart wizard with Page Number 330, Name Orders by Status and the SQL Query marked with red boxes
Page definition and SQL query.

Scroll down to Navigation. Keep Use Breadcrumb and Use Navigation on, so the chart page gets a menu entry. Click Next:

Navigation section of the Create Chart wizard with Use Breadcrumb and Use Navigation turned on
The chart page gets a menu entry.

On the last screen, set Label Column to STATUS and Value Column to ORDERS. Keep Orientation Vertical and click Create Page:

Create Chart column screen with Label Column STATUS and Value Column ORDERS marked with a red box
Label and value columns.

The wizard created page 330 with a chart region named Orders by Status and one series named Series 1.

Step 3: Create the Report Page

The report page shows the orders behind a bar. Click Create Page again and choose Classic Report:

Create a Page dialog with the Classic Report page type marked with a red box
The Classic Report page type.

Enter Page Number 331 and Name Order List. Set Source Type to SQL Query and enter this query:

Example:

select o.order_id,
       o.order_date,
       c.name as customer_name,
       o.status,
       (select nvl(sum(l.quantity * l.unit_price), 0)
          from order_lines l
         where l.order_id = o.order_id) as order_total
  from orders o
  join customers c on c.customer_id = o.customer_id
 where (:P331_STATUS is null or o.status = :P331_STATUS)
   and (:P331_MONTH is null or to_char(o.order_date, 'YYYYMM') = :P331_MONTH)
 order by o.order_id
Create Classic Report wizard with Page Number 331, Name Order List and the SQL Query marked with red boxes
Page definition and SQL query.

Each filter applies only when its item has a value. When the user comes from the status chart, only P331_STATUS is set. When the user comes from the month chart, only P331_MONTH is set.

The items P331_STATUS and P331_MONTH do not exist yet. The wizard accepted the query anyway; the next step creates them.

Scroll down to Navigation and turn off Use Navigation. Users reach this page only from the chart, so it needs no menu entry. Click Create Page:

Navigation section of the Create Classic Report wizard with Use Navigation turned off and marked with a red box
No menu entry for the report page.

The Classic Report wizard has only this one screen.

Step 4: Add the Hidden Items and a Back Button

In Page Designer on page 331, right-click the Order List region and choose Create Page Item. Set Name to P331_STATUS and Type to Hidden. Keep Value Protected on:

Page Designer with the hidden item P331_STATUS selected, Name, Type Hidden and Value Protected marked with red boxes
The hidden item that receives the clicked status.

Create a second hidden item named P331_MONTH the same way. It receives the month key from the second chart.

Then right-click the Order List region again, choose Create Button, and set:

  • Button Name: BACK_TO_CHART, Label: Back to Chart
  • Slot: Previous
  • Behavior, Action: Redirect to Page in this Application, Target: page 330

Step 5: Turn On Pagination with a Row Count

Select the Order List region and open the Attributes tab. The wizard set Pagination Type to No Pagination (Show All Rows), while Number of Rows was 15.

In a first test, the NEW bar opened a report with only 15 rows and no hint that 21 more existed. Set Pagination Type to Row Ranges X to Y of Z (with pagination):

Classic report Attributes in Page Designer with Pagination Type Row Ranges X to Y of Z marked with a red box
Pagination shows the total row count.

Do not pick Row Ranges X to Y of Z (no pagination). That option looks almost the same in the list, but with it the row count stayed hidden in this test. The report uses the template option Hide No Pagination.

Click Save.

Step 6: Add the Link to the Chart Series

Open page 330 in Page Designer and select Series 1 under the Orders by Status region. Set Name to Orders, so the tooltip shows a readable series name.

In the Link group, set Type to Redirect to Page in this Application. Then click Target to open the Link Builder:

  • Page: 331
  • Set Items: Name P331_STATUS, Value &STATUS.
  • Clear Cache: 331
Link Builder Target dialog with Page 331, item P331_STATUS set to &STATUS. and Clear Cache 331 marked with red boxes
The link passes the clicked status.

&STATUS. is replaced with the STATUS value of the bar the user clicks. Any column of the series query can be used this way.

Clear Cache 331 empties both hidden items before the new value is set. The items keep their values in session state, so this makes every drill-down start with empty filters.

Click OK. The Link group now shows Page 331 as the target:

Series Link group in Page Designer with Type Redirect to Page and Target Page 331 marked with a red box
The series link.

Select the Orders by Status region, open the Attributes tab, and turn off Show in the Legend group. With one series, the legend only repeats the series name:

Chart region Attributes in Page Designer with Legend Show turned off and marked with a red box
No legend for a single series.

Click Save.

Step 7: Add a Second Chart That Passes a Hidden Key

A link can pass a column that the chart does not display. The month chart shows labels like Aug 2026 but passes 202608, which is easier to compare in SQL.

On page 330, right-click Body and choose Create Region. Set Name to Orders by Month and Type to Chart. APEX adds a series named New, and the chart Type on the Attributes tab is Bar by default.

Select the new series and set its Name to Orders. Its Source Type starts as Table / View; change it to SQL Query and enter the month query:

Example:

select to_char(order_date, 'YYYYMM') as month_key,
       to_char(trunc(order_date, 'MM'), 'Mon YYYY') as month_label,
       count(*) as orders
  from orders
 where order_date >= add_months(trunc(sysdate, 'MM'), -5)
 group by to_char(order_date, 'YYYYMM'),
          to_char(trunc(order_date, 'MM'), 'Mon YYYY')
 order by month_key

Set Column Mapping Label to MONTH_LABEL and Value to ORDERS:

Month series in Page Designer with the SQL Query and the Column Mapping Label MONTH_LABEL and Value ORDERS marked with red boxes
The month series shows MONTH_LABEL.

Set the series Link Type to Redirect to Page in this Application. In the Link Builder, set Page 331, item P331_MONTH to &MONTH_KEY. and Clear Cache 331:

Link Builder Target dialog with Page 331, item P331_MONTH set to &MONTH_KEY. and Clear Cache 331 marked with red boxes
The link passes the hidden month key.

Turn off the legend for this region too, and click Save.

Step 8: Test the Drill-Down

Run page 330. The screenshots use SALES_USER, a user with read-only access. Both charts appear on the page, and hovering over the NEW bar shows 36 orders:

Orders by Status chart with the tooltip Series Orders Group NEW Value 36 and the NEW bar marked with a red box
The NEW bar counts 36 orders.

Click the NEW bar. Page 331 opens and shows only NEW orders, with row(s) 1 - 15 of 36 at the bottom:

Order List report with only NEW orders and the pagination text row(s) 1 - 15 of 36 marked with a red box
The report shows the 36 NEW orders.

SQL gives the same count:

Example:

select count(*) as new_orders
  from orders
 where status = 'NEW';

select count(*) as aug_2026_orders
  from orders
 where to_char(order_date, 'YYYYMM') = '202608';

Output:

   NEW_ORDERS
_____________
           36

   AUG_2026_ORDERS
__________________
                26

The URL that the bar opened was:

Output:

.../order-list?p331_status=NEW&clear=331&session=...&cs=1Ty-mO_qskXM73J5...

The cs parameter is a checksum. Both new pages have Page Access Protection set to Arguments Must Have Checksum, and APEX added a checksum to the link of each bar on its own.

Click Back to Chart, then click the Aug 2026 bar in the month chart:

Orders by Month chart with the tooltip Group Aug 2026 Value 26 and the Aug 2026 bar marked with a red box
The Aug 2026 bar counts 26 orders.

The report shows row(s) 1 - 15 of 26, and the URL contains p331_month=202608. All four statuses appear, so the NEW filter from the first click was cleared:

Order List report with August 2026 orders of all statuses and the pagination text row(s) 1 - 15 of 26 marked with a red box
The report shows the 26 August orders.

As a negative test, NEW in the first URL was changed to SHIPPED, keeping the checksum. APEX refused the page with this error. Removing the cs parameter gave the same error:

APEX error page Session state protection violation after changing the status in the URL
A changed URL is rejected.

So users cannot open the report with a value that no bar offered. They can still see all orders the report query allows, so put real access rules into the query or an authorization scheme.

Things to Know

  • A series link can use any column of the series query as a substitution, for example &STATUS. or &MONTH_KEY. The column does not have to be mapped to the label or value.
  • Pass a key, not a display text, when the label is formatted. The month chart passes 202608 instead of Aug 2026.
  • Clear the cache of the report page in the link, so a value from an earlier drill-down does not stay in session state. In the test, the month drill-down after a status drill-down showed all statuses.
  • The Classic Report wizard created the report with No Pagination and 15 rows, which silently cut the list. Choose a pagination type with a row count.
  • With Page Access Protection Arguments Must Have Checksum, chart links still work because APEX adds the checksum to each bar's URL. A changed or missing checksum gives a session state protection violation.

Related Guides

Conclusion

To drill down from a chart in Oracle APEX, set the series Link to redirect to a report page and pass the clicked column into a hidden page item. Filter the report query with that item, and clear the report page cache in the link.

The tests showed that the report counts match SQL, that a hidden key column can be passed, and that a changed URL is rejected by the checksum. Turn on pagination with a row count, so users see how many rows are behind each bar.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted