PluginProbe
M Chart / 2.3
M Chart v2.3
2.3.2 2.3.1 2.3 2.2.2 2.2.1 2.2 trunk 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.10 1.10.1 1.11 1.11.1 1.11.2 1.12 1.2 1.2.1 1.3 1.3.1 1.3.2 All 53 releases
m-chart / components / templates / chartjs-chart.php

chartjs-chart.php in M Chart 2.3, at components/templates/chartjs-chart.php

228 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6 // Fetch the post meta once, each get_post_meta() call re-runs the full defaults + filter pipeline
7 $post_meta = m_chart()->get_post_meta( $post_id );
8
9 $title = get_the_title( $post_id );
10 $height = $post_meta['height'];
11 $subtitle = $post_meta['subtitle'];
12
13 if ( '' != $subtitle ) {
14 $title = $title . ': ' . $subtitle;
15 }
16
17 $width = ( '' !== $args['width'] && 'responsive' !== $args['width'] )
18 ? absint( $args['width'] )
19 : 0;
20
21 $defer_rendering = 'enabled' === m_chart()->get_settings( 'defer_rendering' );
22 $observer_options = apply_filters(
23 'm_chart_defer_rendering_observer_options',
24 [ 'rootMargin' => '100px', 'threshold' => 0 ],
25 $post_id,
26 $args
27 );
28
29 $container_id = 'm-chart-container-' . absint( $post_id ) . '-' . absint( $this->instance );
30 $canvas_id = 'm-chart-' . absint( $post_id ) . '-' . absint( $this->instance );
31 $caption_id = $canvas_id . '-caption';
32 $summary_id = $canvas_id . '-summary';
33 $desc_id = $canvas_id . '-desc';
34
35 // The canvas's accessible description is a short summary rather than the whole data table
36 // The full table is associated via aria-details so AT users can navigate it as a real table
37
38 // Prefer the chart's excerpt as a human-written summary
39 // Read the raw excerpt field rather than get_the_excerpt()
40 // So an empty field is not auto-generated from post content
41 $summary = trim( wp_strip_all_tags( strip_shortcodes( (string) get_post_field( 'post_excerpt', $post_id ) ) ) );
42
43 $source_name = trim( (string) $post_meta['source'] );
44 $source_url = trim( (string) $post_meta['source_url'] );
45 $show_source = $post_meta['include_source'] && '' !== $source_name;
46
47 if ( '' === $summary ) {
48 // No excerpt so generate one: chart type + subtitle + source + what follows
49 // The title is left out since it's already the canvas's accessible name via the figcaption
50 // Escaping happens once at output so the pieces stay plain text here
51 // translators: %s is the chart type (line, bar, pie, etc)
52 $summary = sprintf( __( '%s chart.', 'm-chart' ), ucwords( str_replace( '-', ' ', $post_meta['type'] ) ) );
53
54 if ( '' != $subtitle ) {
55 $summary .= ' ' . trim( (string) $subtitle ) . '.';
56 }
57
58 if ( $show_source ) {
59 // translators: %s is the chart's data source name
60 $summary .= ' ' . sprintf( __( 'Source: %s.', 'm-chart' ), $source_name );
61 }
62
63 // Describe the data table(s) that hold the chart's values
64 // Row/column counts come from the first sheet's populated cells
65 $sets = isset( $post_meta['data']['sets'] ) ? (array) $post_meta['data']['sets'] : [];
66 $set_count = count( $sets );
67
68 if ( 1 < $set_count ) {
69 // translators: %d is the number of data tables
70 $summary .= ' ' . sprintf( __( '%d data tables follow.', 'm-chart' ), $set_count );
71 } else {
72 $row_count = 0;
73 $col_count = 0;
74
75 foreach ( (array) reset( $sets ) as $row ) {
76 $row_width = 0;
77
78 foreach ( array_values( (array) $row ) as $column => $cell ) {
79 if ( '' !== trim( (string) $cell ) ) {
80 $row_width = $column + 1;
81 }
82 }
83
84 if ( $row_width ) {
85 $row_count++;
86 $col_count = max( $col_count, $row_width );
87 }
88 }
89
90 if ( $row_count ) {
91 // translators: 1: number of rows 2: number of columns
92 $summary .= ' ' . sprintf( __( 'Data table with %1$d rows and %2$d columns follows.', 'm-chart' ), $row_count, $col_count );
93 }
94 }
95 }
96 ?>
97 <figure id="<?php echo esc_attr( $container_id ); ?>" class="m-chart-container chartjs">
98 <canvas id="<?php echo esc_attr( $canvas_id ); ?>" class="m-chart" height="<?php echo absint( $height ); ?>"<?php echo $width ? ' width="' . absint( $width ) . '"' : ''; ?> role="img" aria-labelledby="<?php echo esc_attr( $caption_id ); ?>" aria-describedby="<?php echo esc_attr( $summary_id ); ?>" aria-details="<?php echo esc_attr( $desc_id ); ?>" style="height: <?php echo esc_attr( $height ); ?>px; max-width: 100%;">
99 <p><?php echo esc_html( $title ); ?></p>
100 </canvas>
101 <figcaption id="<?php echo esc_attr( $caption_id ); ?>" class="screen-reader-text">
102 <?php echo esc_html( $title ); ?>
103 </figcaption>
104 <p id="<?php echo esc_attr( $summary_id ); ?>" class="m-chart-summary screen-reader-text">
105 <?php echo esc_html( $summary ); ?>
106 </p>
107 <div id="<?php echo esc_attr( $desc_id ); ?>" class="screen-reader-text">
108 <?php
109 // Render the data table(s) as an accessible description for screen-reader users.
110 // build_table() handles multi-sheet, parse_data, and template inclusion.
111 echo m_chart()->build_table( $post_id );
112
113 /**
114 * Fires inside the screen-reader-only context container after the data table.
115 *
116 * Use this to append additional context (data sources, methodology,
117 * trend descriptions, summary text) that helps screen-reader users
118 * understand the chart beyond the raw data values.
119 *
120 * @param int $post_id The chart post ID
121 * @param array $args The chart shortcode args
122 */
123 do_action( 'm_chart_screen_reader_text', $post_id, $args );
124 ?>
125 </div>
126 <?php if ( $show_source ) { ?>
127 <p class="m-chart-source screen-reader-text">
128 <?php
129 // The visual source attribution is painted on the canvas by the helper plugin
130 // This makes it invisible to AT and its click target is mouse-only
131 // So this is the operable version for keyboard and screen-reader users
132 // The paragraph un-hides while the link has focus so sighted keyboard users can see it
133 esc_html_e( 'Source:', 'm-chart' );
134
135 if ( '' !== $source_url ) {
136 ?>
137 <a href="<?php echo esc_url( $source_url ); ?>"><?php echo esc_html( $source_name ); ?></a>
138 <?php
139 } else {
140 echo ' ' . esc_html( $source_name );
141 }
142 ?>
143 </p>
144 <?php } ?>
145 </figure>
146 <?php
147 // Inside iframe.php the M_Chart instance has a CSP nonce set; outside (front-end / admin preview) it's empty
148 // The inline <script> needs a matching nonce ONLY when rendered inside the CSP-protected iframe
149 $iframe_nonce = m_chart()->iframe_csp_nonce ?? '';
150 ?>
151 <script<?php echo $iframe_nonce ? ' nonce="' . esc_attr( $iframe_nonce ) . '"' : ''; ?>>
152 ( () => {
153 const postId = <?php echo absint( $post_id ); ?>;
154 const instance = <?php echo absint( $this->instance ); ?>;
155 const chartArgs = <?php echo json_encode( $this->library( 'chartjs' )->get_chart_args( $post_id, $args ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT ); ?>;
156 const canvas = document.getElementById( 'm-chart-' + postId + '-' + instance ).getContext( '2d' );
157 <?php do_action( 'm_chart_after_chart_args', $post_id, $args, $this->instance ); ?>
158
159 let rendered = false;
160
161 const onComplete = () => {
162 // Guard against Chart.js firing onComplete multiple times (observed in 3.1.0)
163 if ( rendered ) {
164 return;
165 }
166
167 rendered = true;
168
169 document.querySelectorAll( '.m-chart' ).forEach( el => {
170 // Build detail once so both dispatches carry identical data
171 const detail = { post_id: postId, instance };
172
173 // Canonical event name — matches the wp.hooks `m_chart.render_done` action on the admin side
174 // The chart instance type (Chart.js vs Highcharts) is identified by consumer context, not the event name
175 el.dispatchEvent( new CustomEvent( 'm_chart.render_done', {
176 bubbles: true,
177 detail,
178 } ) );
179
180 // Legacy name — deprecated, kept for one major version (planned removal in v3)
181 el.dispatchEvent( new CustomEvent( 'render_done', {
182 bubbles: true,
183 detail,
184 } ) );
185 } );
186 };
187
188 const reducedMotion = window.matchMedia
189 && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
190
191 chartArgs.options.animation = {
192 ...chartArgs.options.animation,
193 // duration: 0 keeps animation lifecycle hooks (onComplete) working
194 // so render_done still fires, but eliminates the motion itself
195 ...( reducedMotion ? { duration: 0 } : {} ),
196 onComplete,
197 };
198
199 const renderChart = () => {
200 new Chart( canvas, chartArgs );
201 };
202
203 document.addEventListener( 'DOMContentLoaded', () => {
204 // Register the Chart.js plugins once per chart script rather than on every render
205 Chart.register( ChartDataLabels );
206 Chart.register( MChartHelper );
207 <?php do_action( 'm_chart_after_chartjs_plugins', $post_id, $args, $this->instance ); ?>
208
209 const defer = <?php echo $defer_rendering ? 'true' : 'false'; ?>;
210
211 if ( ! defer || ! ( 'IntersectionObserver' in window ) ) {
212 renderChart();
213 return;
214 }
215
216 const container = document.getElementById( 'm-chart-container-' + postId + '-' + instance );
217 const observer = new IntersectionObserver( ( entries, obs ) => {
218 if ( entries[0].isIntersecting ) {
219 obs.disconnect();
220 renderChart();
221 }
222 }, <?php echo wp_json_encode( $observer_options ); ?> );
223
224 observer.observe( container );
225 } );
226 } )();
227 </script>
228