Type something to search...
Using MongoDB Charts to Visualize Your Data

Using MongoDB Charts to Visualize Your Data

Every team eventually wants to see its data, not just query it. Product managers want signups by week. Support wants open tickets by priority. Engineering wants to know which tenants are growing fastest. The usual path is to export data into a warehouse, flatten it into tables, and point a BI tool at it. That works, but it's a lot of machinery for "show me a chart of orders per day."

MongoDB Charts is the visualization tool built into Atlas. It connects directly to your clusters, understands documents, arrays, and nested fields without flattening, and lets you build charts and dashboards by dragging fields onto encoding channels. You can also embed those charts into your own application, with filters applied per user.

This guide covers setting up data sources, building your first chart, working with arrays and nested fields, using aggregation pipelines and calculated fields, building dashboards with filters, embedding charts securely, and keeping Charts from putting too much load on production.

Getting Started

Charts is available from the Charts tab in your Atlas project. The first time you open it, Atlas activates Charts for the project. Everything you build lives inside that project, and access is controlled by Atlas project roles plus Charts-specific permissions.

Charts works with Atlas clusters, including the free M0 tier, so you can try everything in this guide on a test cluster with sample data. If you're starting fresh, loading the Atlas sample datasets gives you realistic collections like sample_mflix.movies and sample_supplies.sales to experiment with.

Data Sources

Before you can chart a collection, it has to be added as a data source. When you activate Charts, Atlas can add your project's clusters automatically. In the Data Sources page you can:

  • Choose which clusters, databases, and collections are available.
  • Control who can use each data source (owner, manager, author, viewer).
  • Add a pre-filter pipeline that applies to every chart built on that source.
  • Set a read preference so Charts reads from secondaries or analytics nodes.

That last one matters more than it sounds. Charts runs aggregation queries against your cluster every time a chart renders or refreshes. Pointing Charts at secondaries or analytics nodes keeps dashboards from competing with your application for the primary.

Pre-filter pipelines as a security boundary

A data source pipeline runs before every chart query. It's a good place to remove sensitive fields or restrict rows:

[
  { "$match": { "deleted": { "$ne": true } } },
  { "$project": { "email": 0, "passwordHash": 0, "paymentMethod": 0 } }
]

Anyone building charts on this data source can't see those fields, because they never leave the pipeline. It's also a convenient way to apply soft-delete filtering consistently.

Building Your First Chart

Let's build a chart of revenue per month from an orders collection that looks like this:

{
  _id: ObjectId("66f1a2b3c4d5e6f708192a3b"),
  customerId: ObjectId("66a1f0c2e4b0a1b2c3d4e5f6"),
  status: "paid",
  region: "EMEA",
  channel: "web",
  total: Decimal128("129.90"),
  createdAt: ISODate("2026-09-12T14:22:05Z"),
  items: [
    { sku: "LAMP-01", category: "lighting", qty: 1, price: Decimal128("89.90") },
    { sku: "BULB-04", category: "lighting", qty: 2, price: Decimal128("20.00") }
  ]
}
  1. Open a dashboard (or create one) and click Add Chart.
  2. Select the shop.orders data source.
  3. Choose a chart type: Column, then Grouped.
  4. Drag createdAt to the X Axis. Charts detects it's a date and lets you choose binning; pick Month.
  5. Drag total to the Y Axis and set the aggregation to Sum.
  6. Drag region to Series to split each month by region.
  7. In the filter pane, add status equal to paid.

That's it. Charts generates the aggregation for you, runs it, and renders the result. You can see the generated pipeline from the chart builder, which is a nice way to learn how the query is being built.

Choosing a chart type

Charts supports a wide range of types. A quick guide to which one fits which question:

QuestionChart type
How does a value change over time?Line or area chart
How do categories compare?Bar or column chart
What share does each category have?Donut (for a few categories only)
How are two numeric values related?Scatter chart
Where are things happening?Geospatial scatter, choropleth, heatmap
What's the single most important number?Number chart
What are the actual rows?Data table

Donut charts with more than five or six slices are hard to read. Use a sorted bar chart instead.

Working with Arrays and Nested Fields

This is where Charts differs from traditional BI tools. Nested fields show up in the field panel as expandable objects, and you can drag items.category or address.city directly onto a channel.

For arrays, Charts asks how to handle them when you use an array field. Suppose you want revenue by product category, which lives inside the items array. Drag items.category onto the X axis, and Charts offers array reductions:

  • Unwind array: each array element becomes its own row (equivalent to $unwind).
  • Array length: chart the number of elements.
  • Existence, index, min, max, and similar reductions for specific use cases.

Choose Unwind array for items, then put items.price on the Y axis with a Sum aggregation. Now each line item contributes its own price to its own category. Be careful to use the item-level price rather than the order-level total, or every category in an order will be credited with the full order amount.

Using the Query Bar and Aggregation Pipelines

For anything the drag-and-drop builder can't express, the chart builder has a query bar that accepts a filter document or a full aggregation pipeline. The pipeline runs first, and the chart's encodings apply to its output.

For example, to chart revenue per line item accounting for quantity:

[
  { "$match": { "status": "paid" } },
  { "$unwind": "$items" },
  {
    "$project": {
      "createdAt": 1,
      "category": "$items.category",
      "lineTotal": { "$multiply": ["$items.qty", "$items.price"] }
    }
  }
]

Now category and lineTotal appear as fields you can encode directly. Keep these pipelines lean: filter early with $match, and let the chart's binning and aggregation handle grouping instead of writing a $group that duplicates what the encoding does.

If a pipeline gets long or is reused across many charts, move it somewhere central. You can put it in the data source pre-filter, or create a MongoDB view and chart the view. For expensive pipelines that don't need to be real-time, an on-demand materialized view updated by a scheduled job is usually the best option. Using views and on-demand materialized views in MongoDB covers both approaches.

Calculated Fields and Missed Values

Calculated fields let you define new fields with aggregation expressions directly in the chart builder, without writing a whole pipeline. For example, a field called hourOfDay:

{ "$hour": { "date": "$createdAt", "timezone": "Europe/London" } }

Or a field that buckets order sizes:

{
  "$switch": {
    "branches": [
      { "case": { "$lt": ["$total", 50] }, "then": "small" },
      { "case": { "$lt": ["$total", 200] }, "then": "medium" }
    ],
    "default": "large"
  }
}

Drag the new field onto a channel just like any other. Calculated fields are scoped to the chart, so if several charts need the same one, a view or data source pipeline is easier to maintain.

Charts also lets you decide how to handle missing values and what to do with dates in terms of time zone. By default, date binning uses UTC. If your business thinks in local days, set the time zone in the chart or dashboard settings, or daily totals near midnight will land on the wrong day.

Building Dashboards

A dashboard is a collection of charts with shared layout, filters, and refresh settings. A few features make dashboards useful rather than decorative:

Dashboard filters. Add a filter on a field such as region or createdAt, and it applies to every chart on the dashboard that uses a data source containing that field. Viewers can change the filter without editing charts.

Auto-refresh. Dashboards can refresh on an interval. Set it based on how fresh the data needs to be. A support dashboard might refresh every minute; a monthly revenue dashboard doesn't need to refresh more than a few times a day.

Caching. Charts caches query results for a period. Longer cache lifetimes mean fewer queries against your cluster. Tune this per dashboard.

Sharing. Share dashboards with specific users or teams in your Atlas organization, with viewer or author permissions. You can also publish dashboards through public links, which should be reserved for genuinely public data.

A good dashboard answers a small number of questions well. Start with a row of Number charts for the headline metrics, then trends over time, then breakdowns. Resist the urge to add every chart anyone asks for.

Embedding Charts in Your Application

Charts can be embedded in your own web application, which is often the fastest way to add analytics to an admin panel or customer-facing reports.

There are two approaches:

  • Iframe embedding is the simplest: enable embedding on a chart and paste the generated iframe. Good for internal tools.
  • The Embedding SDK gives you programmatic control: rendering, theming, filtering, refreshing, and listening for click events.

Install the SDK:

npm install @mongodb-js/charts-embed-dom

Then render a chart:

import ChartsEmbedSDK from "@mongodb-js/charts-embed-dom";

const sdk = new ChartsEmbedSDK({
  baseUrl: "https://charts.mongodb.com/charts-shop-abcde",
  getUserToken: async () => {
    const res = await fetch("/api/charts-token", { credentials: "include" });
    const { token } = await res.json();
    return token;
  },
});

const chart = sdk.createChart({
  chartId: "6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  height: "420px",
  theme: "light",
  autoRefresh: true,
  maxDataAge: 300,
});

await chart.render(document.getElementById("revenue-chart"));

The baseUrl and chartId come from the chart's embed settings. The SDK also supports createDashboard for embedding an entire dashboard.

Authenticated embedding and per-user filters

For anything other than public data, use authenticated embedding. Your backend issues a signed token (a JWT signed with a key you configure in Charts, or a token from a supported identity provider), and Charts verifies it before rendering.

The real power comes from injected filters. You can configure an injected filter function in the embedding settings that reads claims from the verified token and adds a $match to every query. For a multi-tenant app, that means each customer only ever sees their own data, and the browser can't override it:

// Configured in the Charts embedding settings, runs server-side
function getFilter(context) {
  return { tenantId: context.token.tenantId };
}

Your backend endpoint creates the token with the tenant ID from the logged-in user's session:

import jwt from "jsonwebtoken";

app.get("/api/charts-token", requireLogin, (req, res) => {
  const token = jwt.sign(
    { sub: req.user.id, tenantId: req.user.tenantId },
    process.env.CHARTS_EMBED_SECRET,
    { algorithm: "HS256", expiresIn: "10m" },
  );
  res.json({ token });
});

Client-side filters set with chart.setFilter() are useful for interactive UI (a region dropdown, for example), but they aren't a security boundary. Only fields you've explicitly allowed can be filtered from the client, and tenant isolation should always come from the injected filter.

document.getElementById("region").addEventListener("change", async (e) => {
  const region = e.target.value;
  await chart.setFilter(region ? { region } : {});
});

Keeping Charts Fast and Cheap

Charts queries run on your cluster, so heavy dashboards can affect production. A few habits keep them in check:

  • Read from secondaries or analytics nodes via the data source read preference.
  • Index the fields you filter on, especially dates and dashboard filter fields. A dashboard filter on an unindexed field triggers a collection scan every refresh.
  • Pre-aggregate. For big collections, chart a materialized summary collection (for example daily totals) instead of raw events. A scheduled trigger can keep it updated.
  • Increase cache lifetimes on dashboards that don't need real-time data.
  • Limit data tables. A table chart over a million-document collection is slow and not very useful. Filter or aggregate first.

Common Pitfalls

Summing parent fields after unwinding an array. When you unwind items, every element carries the parent's total. Summing total overcounts. Use item-level values.

Ignoring time zones. Date binning in UTC can shift daily and weekly totals for users in other time zones. Set the time zone explicitly.

Charting raw events at scale. Aggregating millions of documents on every dashboard load is slow and loads the cluster. Pre-aggregate into a summary collection.

Using client filters for access control. setFilter from the browser can be tampered with. Use authenticated embedding with injected filters for multi-tenant data.

Pointing dashboards at the primary. Busy dashboards compete with application traffic. Use secondaries or analytics nodes.

Exposing sensitive fields. Remove them in the data source pipeline so no chart author can accidentally display them.

Conclusion

MongoDB Charts gives you a direct path from collections to dashboards, with native support for nested fields and arrays, aggregation pipelines when you need them, and an embedding SDK that makes in-app analytics practical. Configure data sources carefully (read preference, pre-filter pipelines), pre-aggregate large datasets, and use authenticated embedding with injected filters whenever data belongs to specific users.

Pick one question your team asks you for regularly, such as "how many signups did we get each week?", and build it as a Charts dashboard this afternoon. Share the link, and you'll have one fewer ad hoc query to run next week.

Tags :
Share :

Related Posts

A Complete Guide to MongoDB Query Operators

A Complete Guide to MongoDB Query Operators

Your first MongoDB queries are usually simple equality filters: find the user with this email, find orders with this status. That covers a surprising

Continue Reading
Async MongoDB in Python with Motor and FastAPI

Async MongoDB in Python with Motor and FastAPI

FastAPI runs your endpoints on an event loop. That's what lets a single worker juggle hundreds of concurrent requests: while one request waits on the

Continue Reading
Atlas Online Archive: Tiering Cold Data to Cut Costs

Atlas Online Archive: Tiering Cold Data to Cut Costs

Look at almost any production database and you'll find the same shape. A small slice of recent data gets nearly all the reads and writes: this week's

Continue Reading