Popups, links, and charts that update themselves
A <dc-popup> inside a data element turns that element into its own
trigger. trigger="hover" opens on pointer, trigger="click" on
click — and because the popup body is markup you wrote, it can hold anything, including
a link.
<dc-chart width="500" height="350">
<dc-title>Quarterly Revenue (in thousands)</dc-title>
<dc-bar value="150" fill="#FF6B6B" label="Q1">
<dc-popup trigger="hover">
<strong>Q1 2024</strong><br>
Revenue: $150k<br>
Growth: +12%
</dc-popup>
</dc-bar>
<dc-bar value="180" fill="#4ECDC4" label="Q2">
<dc-popup trigger="hover">
<strong>Q2 2024</strong><br>
Revenue: $180k<br>
Growth: +20%
</dc-popup>
</dc-bar>
<dc-bar value="165" fill="#45B7D1" label="Q3">
<dc-popup trigger="hover">
<strong>Q3 2024</strong><br>
Revenue: $165k<br>
Growth: -8%
</dc-popup>
</dc-bar>
<dc-bar value="220" fill="#96CEB4" label="Q4">
<dc-popup trigger="hover">
<strong>Q4 2024</strong><br>
Revenue: $220k<br>
Growth: +33%
</dc-popup>
</dc-bar>
</dc-chart>
<dc-pie-chart width="500" height="350">
<dc-title>Traffic Sources</dc-title>
<dc-pie-slice value="30" fill="#FF6B6B" label="Desktop">
<dc-popup trigger="hover">
<strong>Desktop Traffic</strong><br>
Sessions: 3,240<br>
Avg. Duration: 4m 32s
</dc-popup>
</dc-pie-slice>
<dc-pie-slice value="45" fill="#4ECDC4" label="Mobile">
<dc-popup trigger="hover">
<strong>Mobile Traffic</strong><br>
Sessions: 4,860<br>
Avg. Duration: 2m 15s
</dc-popup>
</dc-pie-slice>
<dc-pie-slice value="15" fill="#45B7D1" label="Tablet">
<dc-popup trigger="hover">
<strong>Tablet Traffic</strong><br>
Sessions: 1,620<br>
Avg. Duration: 3m 45s
</dc-popup>
</dc-pie-slice>
<dc-pie-slice value="10" fill="#96CEB4" label="Other">
<dc-popup trigger="hover">
<strong>Other Devices</strong><br>
Sessions: 1,080<br>
Avg. Duration: 1m 20s
</dc-popup>
</dc-pie-slice>
</dc-pie-chart>
<dc-funnel-chart width="500" height="350" palette="blues">
<dc-title>Sales Funnel - Click a Stage!</dc-title>
<dc-funnel-stage value="1000" label="Visitors">
<dc-popup trigger="click">
<strong>Website Visitors</strong><br>
Total: 1,000 unique visitors<br>
Source: Organic + Paid
</dc-popup>
</dc-funnel-stage>
<dc-funnel-stage value="450" label="Leads">
<dc-popup trigger="click">
<strong>Qualified Leads</strong><br>
Count: 450 (45% conversion)<br>
<a href="#">View Lead Details</a>
</dc-popup>
</dc-funnel-stage>
<dc-funnel-stage value="150" label="Opportunities">
<dc-popup trigger="click">
<strong>Sales Opportunities</strong><br>
Count: 150 (33% conversion)<br>
Avg. Deal Size: $5,200
</dc-popup>
</dc-funnel-stage>
<dc-funnel-stage value="50" label="Customers">
<dc-popup trigger="click">
<strong>New Customers</strong><br>
Count: 50 (33% close rate)<br>
Total Revenue: $260,000
</dc-popup>
</dc-funnel-stage>
</dc-funnel-chart>
Any data element can carry href and target, which makes the
shape itself a link. The line chart below shows the two approaches side by side: New York
opens a popup containing a link, San Francisco navigates directly.
<dc-chart width="500" height="350">
<dc-title>City Temperature Comparison - Click a Line!</dc-title>
<dc-line stroke="#2196F3" label="New York">
<dc-popup trigger="click">
<strong>New York City</strong><br>
Population: 8.3 million<br>
<a href="https://en.wikipedia.org/wiki/New_York_City" target="_blank"
style="color: #64B5F6; text-decoration: underline;">
View on Wikipedia
</a>
</dc-popup>
<dc-point value="15" label="Mon"></dc-point>
<dc-point value="18" label="Tue"></dc-point>
<dc-point value="22" label="Wed"></dc-point>
<dc-point value="20" label="Thu"></dc-point>
<dc-point value="25" label="Fri"></dc-point>
</dc-line>
<dc-line stroke="#FF5722" label="San Francisco"
href="https://en.wikipedia.org/wiki/San_Francisco"
target="_blank">
<dc-point value="12" label="Mon"></dc-point>
<dc-point value="16" label="Tue"></dc-point>
<dc-point value="19" label="Wed"></dc-point>
<dc-point value="23" label="Thu"></dc-point>
<dc-point value="21" label="Fri"></dc-point>
</dc-line>
</dc-chart>
<dc-chart width="500" height="350">
<dc-title>Top Programming Languages - Click to Learn More</dc-title>
<dc-bar value="45" fill="#f1e05a" label="JavaScript"
href="https://en.wikipedia.org/wiki/JavaScript"
target="_blank"></dc-bar>
<dc-bar value="38" fill="#3572A5" label="Python"
href="https://en.wikipedia.org/wiki/Python_(programming_language)"
target="_blank"></dc-bar>
<dc-bar value="28" fill="#b07219" label="Java"
href="https://en.wikipedia.org/wiki/Java_(programming_language)"
target="_blank"></dc-bar>
<dc-bar value="22" fill="#e34c26" label="TypeScript"
href="https://en.wikipedia.org/wiki/TypeScript"
target="_blank"></dc-bar>
</dc-chart>
Charts re-render when you change their markup, so the update is the DOM operation. Adding or removing an element changes the chart's structure; setting an attribute changes what an existing element says.
<dc-chart id="dynamic-structure" width="500" height="350">
<dc-title>Structure</dc-title>
<dc-bar value="25" fill="#FF5722" label="A"></dc-bar>
<dc-bar value="35" fill="#FFC107" label="B"></dc-bar>
<dc-bar value="45" fill="#4CAF50" label="C"></dc-bar>
</dc-chart>
<script>
function addBar() {
const bar = document.createElement('dc-bar');
bar.setAttribute('value', Math.floor(Math.random() * 50) + 20);
bar.setAttribute('fill', colors[Math.floor(Math.random() * colors.length)]);
bar.setAttribute('label', labels[labelIndex++ % labels.length]);
document.getElementById('dynamic-structure').appendChild(bar);
}
function removeBar() {
const bars = document.querySelectorAll('#dynamic-structure dc-bar');
if (bars.length) bars[bars.length - 1].remove();
}
</script>
<dc-chart id="dynamic-values" width="500" height="350">
<dc-title>Values</dc-title>
<dc-bar value="25" fill="#FF5722" label="A"></dc-bar>
<dc-bar value="35" fill="#FFC107" label="B"></dc-bar>
<dc-bar value="45" fill="#4CAF50" label="C"></dc-bar>
</dc-chart>
<script>
function updateBar() {
document.querySelector('#dynamic-values dc-bar')
.setAttribute('value', Math.floor(Math.random() * 60) + 10);
}
// `fill`, not `color`. The `color` spelling was removed in 0.3.0, and
// setting it now does nothing at all - silently.
function changeColors() {
document.querySelectorAll('#dynamic-values dc-bar').forEach(bar =>
bar.setAttribute('fill', colors[Math.floor(Math.random() * colors.length)]));
}
</script>
Neither chart below is redrawn by hand. One appends a <dc-point> every
five seconds, the other re-sets the value attribute on bars that are already
there — and because a chart observes its own children, both re-render on their own.
There is no redraw(), no setData() and no
requestUpdate() in either script.
<dc-chart id="live-line" width="500" height="350" show-value="false">
<dc-title>Requests per second</dc-title>
<dc-line stroke="#2196F3" label="web-01">
<dc-point value="46" label="1"></dc-point>
<!-- five more seed points, then the script appends -->
</dc-line>
</dc-chart>
<script>
const line = document.querySelector('#live-line dc-line');
let sample = 6;
setInterval(() => {
const point = document.createElement('dc-point');
point.setAttribute('value', String(30 + Math.round(Math.random() * 50)));
point.setAttribute('label', String(++sample));
line.appendChild(point);
}, 5000);
</script>
The series only grows, so the points pack tighter as it runs. The axis thins its
own labels once they start to collide, and show-value="false" keeps a
number off every point — at this density they are unreadable anyway.
<dc-chart id="live-bars" width="500" height="350" show-value="false">
<dc-title>Requests by region</dc-title>
<dc-bar value="48" label="North"></dc-bar>
<dc-bar value="35" label="South"></dc-bar>
<dc-bar value="62" label="East"></dc-bar>
<dc-bar value="41" label="West"></dc-bar>
</dc-chart>
<style>
/* `part="bar"` is a public contract, and the same <rect> survives a
re-render, so an ordinary CSS transition smooths the change. */
#live-bars::part(bar) {
transition: y 900ms cubic-bezier(0.4, 0, 0.2, 1),
height 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
<script>
const bars = document.querySelectorAll('#live-bars dc-bar');
setInterval(() => {
bars.forEach(bar =>
bar.setAttribute('value', String(20 + Math.round(Math.random() * 60))));
}, 5000);
</script>
The bars glide because the library reuses the same <rect>
elements and exposes them as part="bar". Value labels are off here
for a reason: <text> has no CSS y, so a number
would snap to its new position while the bar beneath it was still moving.
The same two moves cover most live-data cases. Appending is for a series that grows;
re-setting value is for a fixed set of things whose numbers change, and it is
the cheaper of the two — no element is created or destroyed, so nothing but the
geometry moves. Only the second can be smoothed this way, for exactly that reason: a
transition needs an element that persists across the update.
Charts emit dc-click, dc-mouseenter,
dc-mouseleave and dc-render. Events are dispatched from the
element in your markup — the <dc-bar> — and bubble, so you
can listen on the element, the chart, or the document. Click and hover the bars.
<dc-chart id="events-demo" width="500" height="350">
<dc-title>Monthly Sales</dc-title>
<dc-bar value="120" fill="#4CAF50" label="Jan"></dc-bar>
<!-- three more months -->
</dc-chart>
<script>
document.querySelector('#events-demo')
.addEventListener('dc-click', (event) => {
const { label, value, percent } = event.detail;
// percent is a decimal: 0.25 means 25%
showDetail(label, value, percent);
});
</script>
Selected: nothing yet - click a bar
Event log (most recent first):
waiting for events...