WordPress.org provides useful public information about every plugin in its directory, but it does not offer a ready-made chart I can drop into my own website. I wanted a small, reusable component that could display the download history for each of my plugins without manually copying data.
The finished solution has four layers:
- WordPress.org supplies authoritative daily download counts.
- HuxxConnect retrieves and caches the API response.
- A custom WordPress shortcode turns the JSON into accessible chart markup.
- An Etch component exposes the chart settings as reusable props.
The result can be used as a seven-day bar chart, a longer line chart, or a cumulative view calculated from the daily figures.

What the numbers mean
Before building the chart, it helps to distinguish downloads from active installations.
The WordPress.org download-statistics endpoint records downloads of plugin ZIP archives. Those downloads can include new installations, updates, reinstalls, automated testing, and other activity. They should therefore be labeled downloads, not users or active sites.
WordPress.org publishes plugin metadata through its Plugins API, including a coarse active_installs bucket. The official plugins_api() reference documents plugin lookup by slug, but the time series used in this chart comes from the separate public download-statistics endpoint.
Step 1: Retrieve daily download data from WordPress.org
The endpoint has this form:
https://api.wordpress.org/stats/plugin/1.0/downloads.php
?slug=PLUGIN-SLUG
&limit=30
For example:
https://api.wordpress.org/stats/plugin/1.0/downloads.php?slug=media-bridge-for-etch&limit=30The response is a JSON object whose keys are ISO dates and whose values are daily download counts:
{
"2026-07-27": "3",
"2026-07-28": "3",
"2026-07-29": "6",
"2026-07-30": "3"
}The endpoint returns daily values, not cumulative totals. A cumulative chart can still be produced by adding each day’s value to a running total:
Daily: 3, 3, 6, 3
Cumulative: 3, 6, 12, 15
That cumulative figure represents the selected range. It is not a true all-time total unless the complete daily history is available or a separate all-time baseline is added.
Step 2: Configure HuxxConnect
HuxxConnect provides a server-side connection between WordPress and an external REST API. It supports runtime query parameters, response formatting, request logging, and per-endpoint caching.

I created an API in HuxxConnect with these settings:
API ID: plugin_info
Base URL: https://api.wordpress.org
Auth: None
I then created one endpoint for each plugin. For example:
Endpoint ID: ops_center_stats
Method: GET
Path: /stats/plugin/1.0/downloads.php
The endpoint’s query parameters contain the actual WordPress.org plugin slug and a default limit:
slug = the-wordpress-directory-slug
limit = 90
The endpoint IDs can follow any internal naming convention. In this project they include:
- ops_center_stats
- css_columns_stats
- media_bridge_stats
These are Huxx endpoint IDs, not necessarily WordPress.org plugin slugs. That distinction matters later: the Etch component selects a Huxx endpoint, and that endpoint already knows which WordPress.org slug to request.
After saving each endpoint, I used HuxxConnect’s test panel to confirm that the final URL and JSON response were correct. I also enabled caching because the historical figures do not need to be requested again on every page view.
The PHP call for an endpoint is straightforward:
$data = huxx_api(
'plugin_info',
'ops_center_stats',
[
'query_variables' => [ 'limit' => 90 ],
'cache' => DAY_IN_SECONDS,
'results_format' => 'json_decoded',
]
);At this point, placing HuxxConnect’s own shortcode on a page displays JSON. That is expected: HuxxConnect retrieves the data, but it does not know how I want that data visualized. The next layer converts the response into chart markup.
Step 3: Create a charting shortcode
I registered a new shortcode named plugin_download_chart. It accepts five attributes:
| Attribute | Purpose | Accepted values |
|---|---|---|
endpoint | HuxxConnect endpoint ID | For example, ops_center_stats |
days | Number of recent days | 3 through 90 |
title | Visible chart heading | Any plain text |
chart | Visualization | bar or line |
mode | Data treatment | daily or cumulative |
Example:
- July 27, 2026: 50 downloads
- July 28, 2026: 73 downloads
- July 29, 2026: 89 downloads
- July 30, 2026: 104 downloads
- July 31, 2026: 108 downloads
- August 1, 2026: 113 downloads
- August 2, 2026: 136 downloads
- August 3, 2026: 165 downloads
- August 4, 2026: 167 downloads
- August 5, 2026: 199 downloads
- August 6, 2026: 216 downloads
- August 7, 2026: 225 downloads
- August 8, 2026: 234 downloads
- August 9, 2026: 237 downloads
- August 10, 2026: 237 downloads
- August 11, 2026: 239 downloads
- August 12, 2026: 247 downloads
- August 13, 2026: 248 downloads
- August 14, 2026: 252 downloads
- August 15, 2026: 257 downloads
- August 16, 2026: 258 downloads
- August 17, 2026: 259 downloads
- August 18, 2026: 266 downloads
- August 19, 2026: 270 downloads
The shortcode performs six jobs:
- Validates the shortcode attributes.
- Calls the selected Huxx endpoint.
- Handles Huxx and JSON errors.
- Normalizes the date/value response.
- Optionally calculates a running total.
- Renders either HTML bars or a responsive inline SVG line chart.
The key data-processing section looks like this:
$response = huxx_api(
'plugin_info',
$endpoint,
[
'query_variables' => [
'limit' => $days,
],
'cache' => DAY_IN_SECONDS,
'results_format' => 'json_decoded',
]
);
if ( is_string( $response ) ) {
$response = json_decode( $response, true );
}
$series = isset( $response['data'] ) && is_array( $response['data'] )
? $response['data']
: $response;
$daily_points = [];
foreach ( $series as $date => $value ) {
if (
preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $date )
&& is_numeric( $value )
) {
$daily_points[ $date ] = max( 0, (int) $value );
}
}
ksort( $daily_points );
$daily_points = array_slice(
$daily_points,
-$days,
null,
true
);
$points = $daily_points;
if ( 'cumulative' === $mode ) {
$running_total = 0;
$points = [];
foreach ( $daily_points as $date => $downloads ) {
$running_total += $downloads;
$points[ $date ] = $running_total;
}
}There are two important implementation details here.
First, dates are sorted before the cumulative calculation. A running total calculated from unsorted JSON would produce a misleading line.
Second, the series is trimmed to the requested range before cumulative values are calculated. A 30-day cumulative chart therefore starts with the first day inside that 30-day window.
Rendering the bar chart
For a bar chart, each date becomes a list item. The PHP calculates a percentage relative to the largest value and passes it to CSS through a custom property:
<?php foreach ( $points as $date => $downloads ) : ?>
<?php $height = ( $downloads / $maximum ) * 100; ?>
<li
class="plugin-chart__point"
aria-label="<?php echo esc_attr(
wp_date( 'F j, Y', strtotime( $date ) )
. ': '
. number_format_i18n( $downloads )
. ' downloads'
); ?>"
>
<span class="plugin-chart__value" aria-hidden="true">
<?php echo esc_html( number_format_i18n( $downloads ) ); ?>
</span>
<span class="plugin-chart__track" aria-hidden="true">
<span
class="plugin-chart__bar"
style="--bar-height: <?php echo esc_attr( $height ); ?>%;"
></span>
</span>
<time
class="plugin-chart__date"
datetime="<?php echo esc_attr( $date ); ?>"
aria-hidden="true"
>
<?php echo esc_html( wp_date( 'M j', strtotime( $date ) ) ); ?>
</time>
</li>
<?php endforeach; ?>Bars work particularly well for short daily ranges. Once the chart contains several weeks of data, a line is usually easier to scan.
Rendering the line chart
The line chart uses an inline SVG rather than a charting library. PHP converts each date/value pair into an x,y coordinate inside a fixed viewBox. The browser then scales the SVG responsively.
The essential calculation is:
$x = 1 === $point_count
? $left + ( $plot_width / 2 )
: $left + ( $index / ( $point_count - 1 ) ) * $plot_width;
$y = $bottom - ( $value / $maximum ) * $plot_height;
$line_points[] = sprintf( '%.2f,%.2f', $x, $y );Those coordinates are passed to an SVG polyline:
<polyline
class="plugin-chart__line"
points="<?php echo esc_attr( implode( ' ', $line_points ) ); ?>"
/>The finished shortcode also outputs grid lines, date labels, a subtle gradient area, hover points, an SVG accessible name, and a visually hidden list containing the underlying values.
Step 4: Style the output
Both chart types share the .plugin-chart root class. I used CSS custom properties so the chart can inherit project colors while retaining sensible fallbacks:
.plugin-chart {
--plugin-chart-color: var(--primary, #334155);
--plugin-chart-grid: rgb(51 65 85 / 12%);
--plugin-chart-muted: #64748b;
display: grid;
gap: 1rem;
margin: 0;
}The bar height comes from the inline --bar-height value:
.plugin-chart__bar {
display: block;
width: 100%;
height: max(0.2rem, var(--bar-height));
border-radius: 0.25rem 0.25rem 0 0;
background: var(--plugin-chart-color);
}The line chart uses the same color token:
.plugin-chart__line-chart {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.plugin-chart__grid-line {
stroke: var(--plugin-chart-grid);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.plugin-chart__line {
fill: none;
stroke: var(--plugin-chart-color);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.plugin-chart__area {
opacity: 0.16;
}The short bar chart can stay fully visible, while longer bar ranges use horizontal scrolling. The line chart instead scales to the available width, making it the better default for 30- or 90-day views.
Step 5: Turn the shortcode into an Etch component
The shortcode works on its own, but repeatedly typing endpoint IDs and chart settings is error-prone. The final step was to wrap it in a reusable Etch component.
Etch recommends building a working static version first and then converting its parent into a component. Its component system uses props to expose the values that change between instances. See the Etch guides to creating a component, creating component props, and mapping props.
I created a component named:
Huxx Plugin API Downloads Chart
Its root is a simple element with the shared chart class:
<div class="plugin-chart">
…
</div>Inside that element is one text node containing the shortcode. Each shortcode attribute is mapped to an Etch prop:
[plugin_download_chart endpoint="{props.huxxDataSlug}" days="{props.numberOfDays}" title="{props.chartTitle}" chart="{props.chart}" mode="{props.mode}"]The component has five props:
| Etch label | Prop key | Type | Example |
|---|---|---|---|
| Chart Title | chartTitle | Text | Ops Center Downloads |
| Huxx Data Slug | huxxDataSlug | Text | ops_center_stats |
| Number of Days | numberOfDays | Text | 90 |
| Chart | chart | Select | bar, line |
| Mode | mode | Select | daily, cumulative |
Despite its current label, huxxDataSlug contains the Huxx endpoint ID. Renaming the label to “Huxx Endpoint” would make that intent clearer without necessarily changing the existing prop key.
The chart select uses:
bar
lineThe mode select uses:
daily
cumulativeOne finished component instance uses these values:
Chart Title: Ops Center Downloads
Huxx Data Slug: ops_center_stats
Number of Days: 90
Chart: line
Mode: cumulativeThat single component can now be reused for every plugin by changing its props. A seven-day daily view can use bars, while a 90-day overview can use a cumulative line—all without duplicating the PHP or chart structure.
Example configurations
Seven-day daily bar chart
- July 21, 2026: 2 downloads
- July 22, 2026: 3 downloads
- July 23, 2026: 5 downloads
- July 24, 2026: 4 downloads
- July 25, 2026: 1 downloads
- July 26, 2026: 1 downloads
- July 27, 2026: 1 downloads
- July 28, 2026: 3 downloads
- July 29, 2026: 5 downloads
- July 30, 2026: 3 downloads
- July 31, 2026: 0 downloads
- August 1, 2026: 6 downloads
- August 2, 2026: 1 downloads
- August 3, 2026: 1 downloads
- August 4, 2026: 3 downloads
- August 5, 2026: 1 downloads
- August 6, 2026: 3 downloads
- August 7, 2026: 2 downloads
- August 8, 2026: 4 downloads
- August 9, 2026: 0 downloads
- August 10, 2026: 2 downloads
- August 11, 2026: 4 downloads
- August 12, 2026: 1 downloads
- August 13, 2026: 1 downloads
- August 14, 2026: 5 downloads
- August 15, 2026: 4 downloads
- August 16, 2026: 53 downloads
- August 17, 2026: 113 downloads
- August 18, 2026: 94 downloads
- August 19, 2026: 86 downloads
Equivalent shortcode:
[plugin_download_chart endpoint="ops_center_stats" days="7" title="Ops Center Daily Downloads" chart="bar" mode="daily"]Thirty-day daily line chart
- July 22, 2026: 53 downloads
- July 23, 2026: 18 downloads
- July 24, 2026: 7 downloads
- July 25, 2026: 0 downloads
- July 26, 2026: 2 downloads
- July 27, 2026: 3 downloads
- July 28, 2026: 3 downloads
- July 29, 2026: 6 downloads
- July 30, 2026: 3 downloads
- July 31, 2026: 1 downloads
- August 1, 2026: 5 downloads
- August 2, 2026: 1 downloads
- August 3, 2026: 1 downloads
- August 4, 2026: 37 downloads
- August 5, 2026: 14 downloads
- August 6, 2026: 4 downloads
- August 7, 2026: 4 downloads
- August 8, 2026: 3 downloads
- August 9, 2026: 2 downloads
- August 10, 2026: 1 downloads
- August 11, 2026: 1 downloads
- August 12, 2026: 6 downloads
- August 13, 2026: 0 downloads
- August 14, 2026: 3 downloads
- August 15, 2026: 4 downloads
- August 16, 2026: 1 downloads
- August 17, 2026: 1 downloads
- August 18, 2026: 4 downloads
- August 19, 2026: 21 downloads
Equivalent shortcode:
[plugin_download_chart endpoint="css_columns_stats" days="30" title="CSS Columns Daily Downloads" chart="line" mode="daily"]Ninety-day cumulative line chart
- July 12, 2026: 44 downloads
- July 13, 2026: 92 downloads
- July 14, 2026: 118 downloads
- July 15, 2026: 131 downloads
- July 16, 2026: 138 downloads
- July 17, 2026: 143 downloads
- July 18, 2026: 147 downloads
- July 19, 2026: 150 downloads
- July 20, 2026: 155 downloads
- July 21, 2026: 157 downloads
- July 22, 2026: 160 downloads
- July 23, 2026: 165 downloads
- July 24, 2026: 169 downloads
- July 25, 2026: 170 downloads
- July 26, 2026: 171 downloads
- July 27, 2026: 172 downloads
- July 28, 2026: 175 downloads
- July 29, 2026: 180 downloads
- July 30, 2026: 183 downloads
- July 31, 2026: 183 downloads
- August 1, 2026: 189 downloads
- August 2, 2026: 190 downloads
- August 3, 2026: 191 downloads
- August 4, 2026: 194 downloads
- August 5, 2026: 195 downloads
- August 6, 2026: 198 downloads
- August 7, 2026: 200 downloads
- August 8, 2026: 204 downloads
- August 9, 2026: 204 downloads
- August 10, 2026: 206 downloads
- August 11, 2026: 210 downloads
- August 12, 2026: 211 downloads
- August 13, 2026: 212 downloads
- August 14, 2026: 217 downloads
- August 15, 2026: 221 downloads
- August 16, 2026: 274 downloads
- August 17, 2026: 387 downloads
- August 18, 2026: 481 downloads
- August 19, 2026: 567 downloads
Equivalent shortcode:
[plugin_download_chart endpoint="media_bridge_stats" days="90" title="Media Bridge Downloads" chart="line" mode="cumulative"]The complete data flow
WordPress.org daily download API
↓
HuxxConnect endpoint and cache
↓
plugin_download_chart shortcode
↓
Daily or cumulative transformation
↓
Bar HTML or inline SVG line chart
↓
Reusable Etch component propsEach layer has one job. WordPress.org remains the source of the download data, HuxxConnect handles the external request, PHP turns the response into semantic markup, and Etch controls presentation and reuse.
The result is deliberately small: no external charting library, no browser-side API request, and no manual data entry. More importantly, the labels remain honest. The chart visualizes plugin downloads; it does not present them as active installations or unique users.
Leave a Reply