Full user and developer documentation is available at https://docs.mch.art/. The developer section covers PHP hooks, JavaScript events, and the admin UI hooks API.
npm install
Note:
package.jsonincludes apostinstallscript that runsnpm run buildautomatically afternpm installcompletes. This means a full build will fire on first install — this is expected.
Build everything:
npm run build
Build CSS only:
npm run build:css
Build JS (minifies components/js/m-chart-chartjs-helper.js):
npm run build:js
Build block only:
npm run build:block
Build admin React app only:
npm run build:admin-ui
Convert readme.txt to README.md:
npm run build:readme
Watch everything (CSS, JS, block, admin app, and readme):
npm run watch
Individual watch targets are also available:
| Command | Watches |
|---|---|
npm run watch:admin-ui |
React admin app |
npm run watch:block |
Gutenberg block |
npm run watch:css |
SCSS → CSS |
npm run watch:js |
m-chart-chartjs-helper.js |
npm run watch:readme |
readme.txt → README.md |
To create a new .pot file with any new string run:
wp i18n make-pot . components/languages/m-chart.pot --exclude=components/admin-ui,components/block
The --exclude skips the compiled bundle directories. Their index.js files still contain the same translatable strings as the -src sources, so without the exclude make-pot would also reference components/admin-ui/index.js and components/block/index.js. wp i18n make-json then emits a stray hash-named JSON for each of those bundle paths that the build:i18n merge step does not clean up (it only consumes -src sourced files), leaving leftover m-chart-{locale}-{md5}.json files behind.
PHP translations use .po / .mo files managed in Poedit. JavaScript translations require additional steps because wp-scripts bundles multiple source files into a single compiled file, and WordPress needs handle-named JSON files to load them.
All locale files (.po, .mo, .l10n.php) live in components/languages/.
- Open the
.pofile for the locale you are updating (e.g.components/languages/m-chart-zh_CN.po) in Poedit. - Go to Translation -> Update from POT file...
- Do not choose Update from Source Code it will fail to retrieve everything and your tanslation will be missing necessary items
- Translate any new or updated strings.
- Save in Poedit (this generates the
.moand.l10n.phpfiles automatically). - Generate per-source-file JSON translation files:
wp i18n make-json components/languages/m-chart-zh_CN.po --no-purge
wp i18n make-json components/languages/m-chart-nl_NL.po --no-purge
wp i18n make-json components/languages/m-chart-ja_JP.po --no-purge
- Merge the hash-based JSON files into handle-named files that WordPress can find:
npm run build:i18n
Repeat steps 1–6 for each locale.
wp i18n make-json generates one JSON file per source file, named with the md5 hash of the source path (e.g. components/admin-ui-src/components/AxisRows.js). However, WordPress looks up translations using the md5 hash of the compiled file path (e.g. components/admin-ui/index.js). Since these hashes don't match, WordPress falls back to looking for {domain}-{locale}-{handle}.json. The build:i18n script merges the per-source-file JSONs into these handle-named files:
m-chart-{locale}-m-chart-admin-ui.json— admin UI translationsm-chart-{locale}-m-chart-editor.json— block editor translations
Each .po file includes Poedit search path headers so that source scanning works correctly. These should exclude:
*.min.js— minified files (duplicates of source)node_modules— third-party dependenciescomponents/external— vendored libraries
If creating a new locale, copy these headers from an existing .po file (e.g. m-chart-en_US.po).
This plugin has automated tests across four layers — PHP unit, PHP integration, JS unit, and Playwright E2E. All test materials live under /tests/ at the repo root.
composer install # PHP dev dependencies (PHPUnit, Brain Monkey, etc.)
npm ci # JS dev dependencies (Jest, Playwright, wp-env)
npx playwright install # Playwright browser binaries (only needed once per machine)Integration and E2E tests run WordPress in Docker via wp-env, so Docker must be running first (colima start or Docker Desktop). The test:e2e / test:a11y scripts run a preflight check and fail with a friendly message if it isn't.
wp-env start handles plugin activation and pretty permalinks automatically via the lifecycleScripts.afterStart entry in .wp-env.json — the same setup runs locally and in CI.
Fast, no Docker, runs against the source directly:
composer test:unitCovers M_Chart_Parse, M_Chart_Parsed_Data_Point, clean_labels() (Wordfence M-Chart-194 CVE regression), and neutralize_csv_cell().
Requires Docker. Spins up a real WP install with the plugin active and runs against a real MariaDB:
npx wp-env start
npx wp-env run tests-cli --env-cwd=wp-content/plugins/m-chart \
vendor/bin/phpunit --testsuite integrationCovers the save_post pipeline, CSV import/export AJAX, the REST API permission callbacks, and a Contributor-role end-to-end XSS regression test.
npm run test:js # one-shot
npm run test:js:watch # interactive
npm run test:js:coverage # with coverage reportCovers the ChartAdminContext reducer (every action type) and useChartRefresh race-condition guards.
npm run test:e2e # all chromium specs (fast loop — reuses a running wp-env, starts one if needed)
npm run test:e2e:ui # interactive Playwright UI
npm run test:a11y # axe-core accessibility suite only (tests/e2e/a11y)
npm run test:e2e:full # one-shot: build assets + start wp-env + run everythingtest:e2e does NOT rebuild assets — after changing plugin source, run npm run build first (or use test:e2e:full). wp-env is left running between test runs so iteration stays fast; stop it with npm run wp-env stop when you're done.
tests/e2e/a11y/ scans the plugin's own markup against the WCAG 2.1 A/AA axe rule set:
frontend.spec.js— the rendered chart region (canvas ARIA wiring, screen-reader summary/table/source link) and theshow="table"outputeditor.spec.js— the chart edit screen's React mounts (the Jspreadsheet grid internals are excluded; the CSV Import flow is the accessible data-entry path)
These run as part of the regular chromium E2E pass in CI. Tag: @a11y.
The Wordfence M-Chart-194 stored-XSS CVE has dedicated regression tests at three layers:
tests/php/unit/CleanLabelsTest.php— pure-function payload neutralizationtests/php/integration/XssRegressionTest.php— Contributor-role end-to-end rendertests/e2e/security-regression-xss.spec.js— real browser execution check
If M_Chart_Parse::clean_labels() ever regresses to a state that lets <script> tags through, all three fail loudly. Tag: @security.
The project uses @wordpress/scripts ESLint config with Prettier formatting rules disabled (.eslintrc.json). Lint scope is restricted to source directories — components/admin-ui-src/, components/block-src/, components/js/m-chart-chartjs-helper.js, tests/js/, tests/e2e/ — to avoid choking on vendored libraries in components/external/ and compiled webpack output in components/admin-ui/ / components/block/.
npm run lint:js # one-shot lint
npm run lint:js:fix # auto-fix what's safe to auto-fix
npm run lint:js:watch # re-lint on file saveFour GitHub Actions workflows under .github/workflows/:
test-php.yml— PHP unit matrix (PHP 8.1–8.4) + integration matrix (PHP 8.2–8.4 × WP 6.8/7.0, plus WP trunk); the PHP 8.3 × WP 7.0 cell collects clover coverage inside the wp-env container and uploads it to Codecovtest-js.yml— Jest + ESLinttest-e2e.yml— Playwright including the a11y suite (chromium on push/PR, full firefox/webkit nightly)deploy.yml— manual WordPress.org deploy, gated on a fast PHP-unit + Jest job
Test workflows trigger on pushes to main, wordpress-7, and pro-support, and on all PRs.
m-chart exposes its color palette as CSS custom properties on :root, so themes and library plugins can re-skin the admin UI without recompiling SASS. The full token list lives at the top of components/sass/_global-mixins-and-variables.scss. Override any of them at any matching selector:
/* Example: change the accent color and the destructive-action red */
.m-chart-container {
--m-chart-accent: #ff6b35;
--m-chart-alert-error: #b00020;
}The accent token chains through to WordPress's --wp-admin-theme-color, so the chart-edit screen's accent (sheet-tab underline, focus rings, hover states) automatically follows the user's Admin Color Scheme (Users → Profile). Other tokens hold direct hex defaults because WP's component library does not reliably emit gray/foreground/notice colors as runtime custom properties — when it does in a future version, those tokens will be migrated to chain through with one-line changes.
Available tokens:
--m-chart-accent, --m-chart-accent-bg-hover, --m-chart-text, --m-chart-text-muted, --m-chart-text-inactive, --m-chart-border, --m-chart-border-strong, --m-chart-bg-sectioned, --m-chart-icon, --m-chart-icon-inactive, --m-chart-disabled, --m-chart-alert-error, --m-chart-alert-success, --m-chart-alert-warning.
The repo ships a library of 17 pickle-themed example charts (one per chart type) — useful for documentation, screenshots, and as data fixtures.
- Canonical data:
tests/fixtures/charts/pickle-*.json— one JSON file per chart type with the full meta +data.setspayload - Human-readable doc:
docs/example-charts.md— concept blurb, data table, and source citations per chart - WP-importable file:
example-charts/pickle-charts.wxr.xml— WordPress eXtended RSS (WXR) export creating all 17 posts as drafts
Each example exercises a different real-world dataset (FAOSTAT cucumber production, KITA kimchi exports, USDA FoodData Central sodium figures, Chinese pao cai / zha cai industry data, Korean kimchi fermentation studies, Google Trends, etc.) and includes a subtitle field that surfaces any data caveats (rounding, interpolation, normalization) directly on the rendered chart.
The JSON fixtures are the source of truth. After editing a fixture, regenerate the WXR with:
npm run build:example-chartsImplementation lives at scripts/generate-example-charts-wxr.js — a hand-rolled Node script that PHP-serializes each fixture's contents and wraps them in a valid WXR 1.2 envelope. No dependencies beyond Node's stdlib.
npx wp-env run cli wp import example-charts/pickle-charts.wxr.xml --authors=skip…or through the WP admin: Tools → Import → WordPress (install the WP Importer plugin if prompted), then upload example-charts/pickle-charts.wxr.xml. The 17 example charts will appear as drafts under M Chart → All Chart Posts.
- Create a new fixture under
tests/fixtures/charts/pickle-<slug>.jsonmatching the existing shape (every fixture haslibrary,type,parse_in,set_names,subtitle,source,source_url, anddata). - Add a
<slug> =>entry to theTITLESmap at the top ofscripts/generate-example-charts-wxr.js. - Add a section to
docs/example-charts.md. - Run
npm run build:example-chartsand commit the regenerated WXR.
- Each chart imports with
wp:status = draft— review before publishing. - The PHP-serialized blob in
<wp:meta_value>is what WP's importer unpacks viamaybe_unserialize()and then re-serializes viaupdate_post_meta(). The end result inwp_postmetais identical to what the m-chart admin UI would produce when saving the same chart manually. - Post IDs in the WXR start at 1000 to avoid collisions with low-numbered posts on the destination site. The importer will reassign IDs if they conflict.
Deploy to WordPress.org via GitHub Actions:
Actions tab → "Deploy to WordPress.org" → "Run workflow"
Before triggering the workflow:
- Bump the version number in
m-chart.php,class-m-chart.php, andreadme.txt - Run
npm run buildand commit all compiled assets - Run
npm run build:readmeand commit the updatedREADME.md - Target the
mainbranch when running the workflow
The chart post-edit screen uses a React app (components/admin-ui-src/) compiled to components/admin-ui/ by @wordpress/scripts. As of v2.0 the React admin UI is used for all charting libraries — the previous jQuery + Handlebars stack has been removed.
components/admin-ui-src/
index.js Entry point — mounts portals into each meta box div
context/
ChartAdminContext.js Single shared reducer (all components read/write here)
hooks/
useChartRefresh.js Debounced AJAX fetch for updated chart args
useFormSubmissionGuard.js Gates Save/Publish buttons on state.formEnabled; blocks
form submission while a chart refresh is in flight
useImageGeneration.js Captures Chart.js canvas → base64 PNG → hidden textarea
useLongPress.js 500ms pointer-event long-press (tab rename on mobile)
utils/
measureTextWidth.js Canvas-based text measurement utility
components/
ChartMetaBox.js Root for the Chart meta box (preview + settings)
ChartPreview.js Imperative Chart.js instance managed via refs
ChartSettings.js Settings form container
TypeAndThemeRow.js Type / Theme / Height inputs
ParseAndFlagsRow.js Parse direction, Labels, Legend, Shared tooltip
AxisRows.js Vertical/horizontal axis title + units, Y-min
ShortcodeAndImageRow.js Shortcode display, image URL, library hidden input
SpreadsheetMetaBox.js Root for the Data meta box
SheetTabs.js Tab bar (conditionally shown for multi-sheet types)
SheetTab.js Individual tab — click/dblclick/long-press rename, delete
JspreadsheetWrapper.js Thin imperative wrapper around a Jspreadsheet worksheet
CsvControls.js CSV import (fetch + FormData) and export (temp form POST)
SubtitleField.js Controlled subtitle input (replaces subtitle-field.php)
- PHP localises initial state into
window.m_chart_adminviawp_localize_script(seecurrent_screen()inclass-m-chart-admin.php). The object contains: plugin metadata (slug,version), settings (performance,image_support,image_multiplier, etc.), the activelibrary, chart post meta, spreadsheet data, available chart types and themes, a nonce, the AJAX URL, and initial chart args. ChartAdminContextseeds auseReducerfrom that object — all components share one context.- User changes (settings, spreadsheet, title, subtitle) update context state.
useChartRefreshdebounces 300 ms then POSTs toadmin-ajax.php?action=m_chart_get_chart_args.- The response updates
chartArgsin context;ChartPreviewpatches its Chart.js instance. - On form submit,
SpreadsheetMetaBoxserialises all sheet data to the hiddentextarea[name="m-chart[data]"].
| PHP method | Mount div | Component |
|---|---|---|
edit_form_before_permalink() |
#m-chart-subtitle-root |
<SubtitleField /> |
spreadsheet_meta_box() |
#m-chart-spreadsheet-root |
<SpreadsheetMetaBox /> |
chart_meta_box() |
#m-chart-chart-root |
<ChartMetaBox /> |
All three share a single ChartAdminProvider rendered into a hidden container appended to <form id="post">, with portals projecting into each mount div.
Library plugins (e.g. M Chart Highcharts Library) integrate with the React admin UI via the wp.hooks API:
m_chart_admin_scriptsaction — enqueue library-specific scripts after M Chart's scripts are loadedm_chart.render_chartfilter — handle chart rendering in the admin preview, returningtrueto prevent the default Chart.js renderer from runningm_chart.settings_componentfilter — replace the default Chart.js settings UI with a library-specific React component
See the Admin UI Hooks reference for the full list of available hooks.
See the Admin UI Hooks reference for the full list of JavaScript hooks available to library plugin authors, and the Migrating to v2 guide for what changed when migrating from v1.x.
The following elements are intentionally kept as native HTML and are not candidates for @wordpress/components swaps:
CsvControls.js— file input. Triggered programmatically viafileInputRef.current.click(). Visually-hidden viascreen-reader-textclass so it remains accessible to screen readers.FormFileUploadhas different semantics.ShortcodeAndImageRow.js—<input type="hidden">for the library name. Not a UI element; hidden inputs have no component counterpart.
This plugin targets WCAG 2.1 Level AA. A few things to know when extending or maintaining it:
Screen-reader users should use CSV Import as the primary data-entry path. The Jspreadsheet CE library used in the spreadsheet UI provides partial keyboard accessibility but is not fully WCAG-compliant in forms mode. CSV import handles the same data shapes and is fully accessible.
Fires inside the visually-hidden context container after the auto-emitted data table for every chart render (canvas, image, AMP). Use it to inject additional context — methodology notes, trend descriptions, alt-text-style summaries — that helps screen-reader users understand the chart beyond the raw data values.
add_action( 'm_chart_screen_reader_text', function ( $post_id, $args ) {
$note = get_post_meta( $post_id, 'chart_a11y_note', true );
if ( $note ) {
echo '<p>' . esc_html( $note ) . '</p>';
}
}, 10, 2 );The React admin UI sits inside meta-box <h2> elements provided by WordPress core. When inserting headings inside React components, level them appropriately (typically <h3> or <h4>) so the document outline stays sensible.
The admin UI uses wp.a11y.speak() from @wordpress/a11y for transient status announcements (chart refreshed, sheet renamed, CSV imported, shortcode copied, etc.). Library plugin authors hooking into the React data-flow should consider doing the same.