Skip to content

feat(api): add chartsTable with filters, sorting and pagination - #262

Merged
anantjain341 merged 2 commits into
CivicDataLab:devfrom
anantjain341:feat/charts-table
Oct 27, 2026
Merged

anantjain341 merged 2 commits into
CivicDataLab:devfrom
anantjain341:feat/charts-table

Conversation

@anantjain341

@anantjain341 anantjain341 commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Backend for "Charts Listing API pagination and filters" (https://github.com/CivicDataLab/DataSpace/issues/329), part of https://github.com/CivicDataLab/DataSpace/issues/277.

Adds chartsTable for the charts dashboard, built the same way as datasetsTable (#215) and the use case, collaborative, AI model and publication tables (#254).
The existing getChartData query is untouched, so nothing that runs today changes.

What you get in one call

A page of rows, the total count, and the per-status numbers for the tabs:

chartsTable(filters, sortOptions, limit, offset) {
  totalItemsCount                     # total after filters, for "Showing 1–10 of 21"
  statusCounts { status count }       # for the tab labels: Draft (3), Published (18)
  data { ... }
}

Built charts and uploaded chart images come back in one list, as on the page today.
Rows keep their types, TypeResourceChart and TypeResourceChartImage (the same union getChartData returns), so the page's existing fragments, the click-through to the right editor and the delete / unpublish buttons work unchanged:

data {
  __typename
  ... on TypeResourceChart { id name chartType status modified dataset { id title } }
  ... on TypeResourceChartImage { id name status modified dataset { id title } }
}

Mapping the table UI

On screen Send / read
Search charts… filter name with icontains
Tabs Draft / Published a status filter: DRAFT / PUBLISHED. Numbers for the tab labels come from statusCounts; an All tab is their sum
Dataset filter (Any dataset) filter dataset with the dataset id
Type filter (Any type) Bar / Line / Big Number: chart_type = BAR / LINE / BIG_NUMBER. Map: chart_type in POLYGON_MAP,POINT_MAP,GEOSPATIAL_MAP,ASSAM_DISTRICT,ASSAM_RC. Uploaded image: kind = IMAGE
Chart column name
Dataset column dataset { title }
Type column chartType (image rows have none: show "Image")
Status column status, for both kinds. Today the page only asks for it on images, which is why built charts show "NA"
Last Updated column modified
Sorting a column sortOptions: [{field: "modified", direction: "desc"}]
Showing x–y of z, Previous / Next limit, offset, totalItemsCount
Edit / Delete / Unpublish need id and __typename, unchanged (existing mutations for each kind)

The design's Type filter also lists Pie; the backend has no pie chart type yet.

Filters

A list of filters, each: which field, how to compare, what value.
All filters are combined with AND. The value is always a string.

{"field": "name",       "condition": "icontains", "value": "rainfall"}
{"field": "status",     "condition": "exact",     "value": "DRAFT"}
{"field": "chart_type", "condition": "in",        "value": "BAR,LINE"}
{"field": "kind",       "condition": "exact",     "value": "IMAGE"}

Fields you can filter on:

  • name, status, chart_type, id
  • kind: CHART (built in the chart builder) or IMAGE (uploaded)
  • dataset (id), dataset__slug, dataset__title
  • modified (ISO date, e.g. 2026-01-01)

Ways to compare: the same as datasetsTable:

  • exact (default), iexact — equals
  • contains, icontains — text search (use icontains for a search box)
  • startswith, endswith and their i versions
  • gt, gte, lt, lte — for dates
  • in — any of a comma-separated list
  • isnull — "true" or "false"

Anything outside this list returns a GraphQL error that names the allowed fields.

Sorting

"sortOptions": [{"field": "modified", "direction": "desc"}]

Fields: name, modified, status, chart_type, kind.
Default when omitted: last updated first, the same order as today.

Pagination

  • limit = page size, 1 to 100 (default 50)
  • offset = rows to skip
  • pages = ceil(totalItemsCount / limit), offset = (page − 1) × limit

statusCounts is computed with every filter applied except the status filter, so the tab numbers do not change when switching tabs.

Who sees what

Same rules as getChartData. Send the usual headers: Authorization: Bearer <token> and, inside an organisation, organization: <slug>.

  • In an organisation: charts and images of that organisation's datasets (you must be a member).
  • Logged in, no organisation: charts and images of your own datasets.
  • Not logged in: refused, as getChartData does today.

Full example

query ChartsTable($filters: [FilterSpec!], $sort: [SortSpec!], $limit: Int, $offset: Int) {
  chartsTable(filters: $filters, sortOptions: $sort, limit: $limit, offset: $offset) {
    totalItemsCount
    statusCounts { status count }
    data {
      __typename
      ... on TypeResourceChart { id name chartType status modified dataset { id title } }
      ... on TypeResourceChartImage { id name status modified dataset { id title } }
    }
  }
}

{
  "filters": [
    {"field": "status", "condition": "exact", "value": "DRAFT"},
    {"field": "name", "condition": "icontains", "value": "rain"}
  ],
  "sort": [{"field": "modified", "direction": "desc"}],
  "limit": 10,
  "offset": 0
}

Errors

Ordinary GraphQL errors, for example:

  • Cannot filter on 'xyz'. Allowed fields: …
  • limit (500) must be between 1 and 100
  • offset (-1) must be 0 or more
  • A value of the wrong type (text for an id or a date) returns Django's validation message, the same as datasetsTable today.

How it works

Built charts and chart images live in two tables. They are merged in one SQL UNION, so counting, sorting and slicing happen in the database; only the rows of the page are then loaded with their dataset. A page costs the same number of queries however many charts there are. Today getChartData loads both tables in full and sorts them in Python.

No migration, no new dependency. Uses the shared helpers in api/utils/qs_utils.py: sorting and paging go through get_pagination_window like the other tables; filters are applied to each of the two tables before the UNION (Django cannot filter after it); tab counts use status_counts.

The charts dashboard loads every built chart and chart image through
getChartData and searches them in the browser. chartsTable returns one
page of both kinds in a single list, with the total count and per-status
counts, using the shared table helpers.

Both tables are merged in one SQL UNION, so counting, sorting and slicing
happen in the database; only the rows of the page are then loaded.
Visibility and permission are the same as getChartData. Rows keep their
types (TypeResourceChart, TypeResourceChartImage), so existing fragments
work unchanged. getChartData is untouched.
Sorting and paging now go through the same helper as the other table
queries. Only filtering stays per table, because Django cannot filter
after a UNION. Drops the extra id tie-breaker, matching the others.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@anantjain341
anantjain341 marked this pull request as ready for review October 27, 2026 11:48
@anantjain341
anantjain341 merged commit 113894c into CivicDataLab:dev Oct 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants