Every chart in a workbook draws from data, and Excel does not redraw it from scratch each time the file opens. Instead it freezes a complete copy of the plotted numbers and category labels into the chart part itself — a <c:numCache> for each numeric series and a <c:strCache> for the labels — stored in xl/charts/chart1.xml. That cache is what makes a chart render instantly and keep displaying even when its source is unavailable. It is also a verbatim record of the data behind the picture, and it survives deleting the source rows, hiding the source sheet, or breaking the link to an external workbook. This post walks through where the layer lives, what the caches leak, why the cached values are often more precise than anything visible on the axis, why series formulas map the workbook’s internal structure, why Document Inspector ignores it entirely, and how to actually strip cached chart data before a workbook leaves the perimeter.
When a user selects a range and inserts a chart, Excel records two completely different descriptions of the same data. The first is a reference: a series formula such as Sheet1!$B$2:$B$13 that tells Excel where to look when it recalculates. The second is a copy: the actual values that lived in that range at the moment the chart was last saved, frozen into the chart part as a cache. Both travel inside the workbook, and they are stored in xl/charts/chart1.xml (with one numbered part per chart), wired to the worksheet through a drawing part and a chain of relationship files.
The cache exists for a sensible engineering reason. A chart that had to recompute every series from its source formulas on every open would be slow, and a chart whose source had moved, been deleted, or lived in a now-unreachable external file would simply fail to render. The cache makes charts fast and robust: Excel paints from the frozen copy first and only refreshes from the source when the data is present and a recalculation is triggered. The side effect is that every chart carries a self-contained snapshot of its underlying numbers — and that snapshot does not vanish when the source does. This is the same fundamental pattern as the pivot cache embedding a full copy of source data, applied to a layer almost nobody thinks of as a data store.
A chart is not a live window onto a range — it is a frozen copy plus a reference back to that range. Delete the range and the reference dangles, but the frozen copy in <c:numCache> keeps the numbers. A workbook stripped of its source data can still hand a recipient every value that was ever plotted, point for point.
Each plotted series in a chart is a <c:ser> element, and inside it sit the two halves of the picture. The category axis values — the labels along the bottom — live under <c:cat> in a <c:strCache> (for text) or <c:numCache> (for numbers and dates). The plotted values — the bar heights or line points — live under <c:val> in a <c:numCache>. Each cache pairs a <c:f> formula reference with a list of <c:pt> points carrying the literal cached value at each index:
// xl/charts/chart1.xml — one series: reference plus frozen copy
<c:ser>
<c:tx><c:strRef>
<c:f>Margins!$D$1</c:f>
<c:strCache><c:pt idx="0"><c:v>True Gross Margin %</c:v></c:pt></c:strCache>
</c:strRef></c:tx>
<c:cat><c:strRef>
<c:f>Margins!$A$2:$A$5</c:f>
<c:strCache>
<c:pt idx="0"><c:v>Northeast Hospital Group</c:v></c:pt>
<c:pt idx="1"><c:v>Pinnacle Retail (at-risk)</c:v></c:pt>
</c:strCache></c:strRef></c:cat>
<c:val><c:numRef>
<c:f>Margins!$D$2:$D$5</c:f>
<c:numCache>
<c:pt idx="0"><c:v>0.4131</c:v></c:pt>
<c:pt idx="1"><c:v>0.0892</c:v></c:pt>
</c:numCache></c:numRef></c:val>
</c:ser>
Three things leak from this one series. The series-title cache names the metric exactly as the author labelled it internally — True Gross Margin %, not whatever softened heading appears in the deck. The category cache carries verbatim labels, including the parenthetical Pinnacle Retail (at-risk) annotation that the author never intended for an outside reader. And the value cache holds 0.4131 and 0.0892 — the precise figures behind two bars whose axis might only show “41%” and “9%.” All of it is plain text, and all of it ships inside the chart whether or not the Margins sheet still exists.
Every <c:numCache> and <c:strCache> is a small spreadsheet in its own right — one value per index, in source order. Read them across every series and you reconstruct the chart’s entire data table without ever opening the worksheet it came from. The picture is lossy; the cache behind it is not.
The dangerous property of the cache is that it is decoupled from the source. Three common workflows all leave the cached values fully intact while creating the impression that the data is gone:
In each case the author performs an action that feels like removing data and reasonably concludes the workbook is clean. The chart still looks right, which reinforces the belief. But “the chart still looks right” is precisely the symptom of the problem: it looks right because the data is still there, frozen one layer down.
A chart that displays correctly after you delete its source is not proof that the data was redundant — it is proof that the cache retained it. The only way a chart can survive losing its source is by carrying a copy. Treat every surviving chart as a live data table on its way to the recipient.
A chart is a deliberately lossy presentation. An axis labelled in increments of ten thousand rounds away the last four digits; a percentage shown to the nearest whole number hides the decimals; a bar drawn to the edge of the plot area communicates “big” but not “47,318,994.” The cache, by contrast, stores the value at full precision exactly as it was computed. A reader who only sees the rendered chart sees the rounded story; a reader who reads <c:numCache> sees the exact figure.
This gap matters most for the kinds of numbers people deliberately blur in a chart. Revenue figures shown as a tidy bar carry their exact value in the cache. Headcount plotted as a trend line caches the precise count at each point. A margin chart that visually communicates “healthy” caches the figure to four decimal places, including the quarters where it was thin. The chart was designed to round; the cache refuses to. Anyone extracting the cache recovers the unrounded numbers the visual was meant to soften.
// the axis shows tidy rounded labels; the cache does not
// rendered axis tick: "$45M"
<c:pt idx="0"><c:v>45318994.27</c:v></c:pt>
// rendered axis tick: "12%"
<c:pt idx="1"><c:v>0.1163842</c:v></c:pt>
Alongside the cached values, each series keeps its <c:f> formula reference — the address the data came from. These references are a map of the workbook’s internal structure, and they survive even when what they point at does not. A formula such as 'Q3 Internal Working'!$F$2:$F$60 names a sheet that may have been deleted, telling a reader it once existed, that it had at least sixty rows of data, and what it was called. References to defined names expose the workbook’s named-range vocabulary; references to other files expose the network paths of external sources.
The combination is what makes the chart layer rich for an investigator. The cache says what the numbers were; the formula says where they lived and what the author called that place. A chart whose source sheet has been deleted still names the sheet, still records its row count, and still holds its values — a near-complete reconstruction of a tab the author believed was gone. The same self-documenting reference that makes the chart maintainable makes it a structural disclosure.
Excel does not rewrite a chart’s <c:f> when the sheet it points at is deleted — it leaves the formula as a dangling reference. The sheet name, its column letters, and its row span all persist in plain text, documenting a tab that no longer appears anywhere in the workbook.
A chart is not anchored directly to a worksheet; it sits behind a short chain of parts, and any cleanup has to respect the whole chain or it produces a package Excel offers to repair. The worksheet references a drawing part (xl/drawings/drawing1.xml) through its rels file. The drawing part anchors the chart frame to a cell range and references the chart part (xl/charts/chart1.xml) through its own rels. The chart part holds the series, the caches, and the formulas — and may carry its own xl/charts/_rels/chart1.xml.rels pointing at a colour-style part, a chart-style part, or, for charts built from their own embedded mini-table, a xl/charts/chart1.xml companion data part.
Modern chart types add another wrinkle. Waterfall, treemap, sunburst, histogram, box-and-whisker, and funnel charts — the “extended” family — are stored in a parallel xl/charts/chartEx1.xml part using a different schema, and they keep their cached data inside a <cx:data> block rather than the classic numCache. A cleanup pass that only knows the classic chart schema walks straight past every extended chart and leaves its data fully intact, exactly the way a comment cleanup that only knows threaded comments leaves the legacy note layer untouched.
Extracting a chart’s data needs only a ZIP reader and an XML parser. Walking every xl/charts/chart*.xml part and reading the cached points out of each series reconstructs the underlying table directly from the picture:
# dump every chart series: its source formula and cached points
import zipfile, re
from lxml import etree
C = "http://schemas.openxmlformats.org/drawingml/2006/chart"
with zipfile.ZipFile("workbook.xlsx") as z:
for name in z.namelist():
if not re.match(r"xl/charts/chart\d+\.xml", name):
continue
tree = etree.fromstring(z.read(name))
for ser in tree.iter(f"{{C}}ser"):
for ref in ser.iter():
if ref.tag.endswith("}f"):
print("ref:", ref.text)
pts = [p.text for p in ser.iter(f"{{C}}v")]
print("cached:", pts)
The script prints, for every series, its source references and the full list of cached values — titles, categories, and plotted numbers together. For ad-hoc inspection, renaming the file to .zip, expanding it, and grepping xl/charts/ for <c:v> surfaces every cached point in seconds. That is precisely what an opportunistic recipient does the moment a workbook with charts lands — the same trivial unzip-and-read move that exposes the shared-strings table.
From a single sweep through the chart parts of a workbook, a reader typically extracts:
<c:f> formula references.The first three categories are the data and how it was framed; the last four are structure and provenance. Together they routinely reconstruct an entire analytical worksheet that the author believed they had removed from the file, leaving only the harmless-looking picture behind.
Microsoft’s Document Inspector has checks for document properties, hidden sheets, comments, and several other layers — but it has no concept of chart-cache data. From the inspector’s point of view a chart is legitimate visible content the user deliberately created and wants to keep; stripping the data that makes it render would break the chart, so the inspector leaves it entirely alone. There is no “remove cached chart data” option because, by design, the cache is not treated as removable metadata at all.
That places chart caches in the same blind spot as table schemas and conditional-formatting rules: layers that carry real disclosure but that the inspector classifies as ordinary content. A workbook that has been “inspected and cleaned” ships every chart cache untouched. Hiding the source sheet does not help, deleting it does not help, and the one tool most users trust to find hidden data does not look here.
Document Inspector treats a chart as content to keep, not metadata to remove, so it never reads the cache. A workbook can pass every inspector check and still carry the complete data table behind every chart, at full precision, including data from sheets and files the recipient was never meant to see.
There is no clean Excel UI action that removes a chart’s cache while keeping the chart, because the cache is what keeps it rendering. The realistic options trade off how much of the chart you need to preserve:
numCache, no strCache, and no series formulas — only pixels. Confirm the image itself is clean, since pasted pictures travel through the media layer that can retain its own metadata.xl/charts/chart*.xml and chartEx*.xml, and either overwrite the cached points with rounded or redacted values or rebuild the chart against a sanitised summary range. This keeps a live, branded chart while removing the unrounded figures and the original references.Whichever path you take, the extended chart family is the easy thing to miss: a sweep that only rewrites classic numCache elements leaves every waterfall, treemap, and histogram fully populated in its chartEx part. And remember the formulas: redacting the cached values while leaving the <c:f> references in place still discloses the structure and names of the source sheets. A complete pass handles both schemas and clears or rewrites both the caches and the references.
xl/charts/chart*.xml and chartEx*.xml part; do not assume the classic schema covers every chart.<c:numCache>, <c:strCache>, and <cx:data> block; confirm every cached value is safe to disclose at full precision.<c:f> reference for the names of deleted or hidden source sheets and external-file paths.The chart cache rarely travels alone. The same source data it freezes is usually also frozen in the pivot cache if a pivot drove the chart, preserved in the shared-strings table if the labels were ever typed into cells, and reachable through the external-links layer if the series pointed at another workbook. Stripping the chart cache while leaving those siblings populated removes one copy of the data and ships three others. A workbook is only chart-clean when the cache, the references, and every parallel store of the same numbers are handled in one pass.
The broader lesson is the one this series keeps returning to: a workbook is a package of cooperating parts, and the same data is frequently stored in several of them at once. A chart looks like the most innocent object in the file — a summary, a picture, the opposite of raw data. In the format it is one of the most faithful copies of that raw data the workbook carries, sitting in plain XML behind a layer of paint.
A chart is meant to be the safe thing to share — the distilled, rounded, presentation-ready summary you hand over precisely because it is not the underlying data. The cache quietly inverts that intention. Behind every bar is its exact value to full precision; behind every label is the verbatim text the author typed; behind the whole picture is a reference naming the sheet it came from, intact even after that sheet is deleted. None of it shows in the rendered chart, and none of it is removed by hiding the source, deleting the source, or running Document Inspector. All of it is plain UTF-8 in xl/charts/, recoverable with a ZIP reader and a ten-line script.
For workbooks that stay inside an organisation, the cache is exactly what it was built to be: the thing that makes charts fast and resilient. For workbooks crossing the perimeter, it is a full copy of the data the author thought they had left behind. The only durable defences are to flatten finished charts to pictures or to rewrite the caches and references in code — and to remember that in this format, the picture was never the point. The data behind it is.
Use MetaData Analyzer to enumerate every chart across every worksheet, read the cached values and labels behind each one, recover the data tables of deleted and hidden source sheets, surface external-file references frozen into series formulas, and confirm no chart is shipping data you never meant to disclose before your workbooks leave the organisation.
The same frozen-copy pattern in the pivot layer — a full snapshot of source data that survives deleting the source sheet.
Where chart series point at other files — broken links go inert but leave cached values and network paths behind.
The same invisible-in-the-grid, fully-present-in-the-XML pattern — chart labels and deleted cell values both linger in plain XML.