From 6ed4c50375fba5c550c6897751ffdd173d72c902 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Thu, 26 Mar 2026 20:30:39 +0530 Subject: [PATCH 1/2] feat: add color pallets --- classes/Visualizer/Module/Admin.php | 101 ++++++++++++++++++ classes/Visualizer/Module/Chart.php | 7 +- classes/Visualizer/Module/Utility.php | 90 ++++++++++++++-- classes/Visualizer/Render/Sidebar/Google.php | 18 ++++ .../Render/Sidebar/Type/GoogleCharts/Geo.php | 10 ++ templates/global-settings.php | 89 +++++++++++++++ 6 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 templates/global-settings.php diff --git a/classes/Visualizer/Module/Admin.php b/classes/Visualizer/Module/Admin.php index 22ea34cff..f4b0e1272 100644 --- a/classes/Visualizer/Module/Admin.php +++ b/classes/Visualizer/Module/Admin.php @@ -30,6 +30,8 @@ class Visualizer_Module_Admin extends Visualizer_Module { const NAME = __CLASS__; + const OPTION_GLOBAL_SETTINGS = 'visualizer_global_settings'; + /** * Library page suffix. * @@ -48,6 +50,14 @@ class Visualizer_Module_Admin extends Visualizer_Module { */ private $_supportPage; + /** + * Settings page suffix. + * + * @access private + * @var string + */ + private $_settingsPage; + /** * Constructor. * @@ -65,6 +75,8 @@ public function __construct( Visualizer_Plugin $plugin ) { $this->_addAction( 'admin_footer', 'renderTemplates' ); $this->_addAction( 'admin_enqueue_scripts', 'enqueueLibraryScripts', null, 0 ); $this->_addAction( 'admin_enqueue_scripts', 'enqueue_support_page' ); + $this->_addAction( 'admin_enqueue_scripts', 'enqueueSettingsScripts' ); + $this->_addAction( 'admin_post_visualizer_save_global_settings', 'saveGlobalSettings' ); $this->_addAction( 'admin_menu', 'registerAdminMenu' ); $this->_addFilter( 'media_view_strings', 'setupMediaViewStrings' ); $this->_addFilter( 'plugin_action_links', 'getPluginActionLinks', 10, 2 ); @@ -835,6 +847,15 @@ public function registerAdminMenu() { 'admin.php?page=' . Visualizer_Plugin::NAME . '&vaction=addnew' ); + $this->_settingsPage = add_submenu_page( + Visualizer_Plugin::NAME, + __( 'Settings', 'visualizer' ), + __( 'Settings', 'visualizer' ), + 'manage_options', + 'viz-settings', + array( $this, 'renderSettingsPage' ) + ); + $this->_supportPage = add_submenu_page( Visualizer_Plugin::NAME, __( 'Support', 'visualizer' ), @@ -1058,6 +1079,86 @@ public function renderSupportPage() { include_once VISUALIZER_ABSPATH . '/templates/support.php'; } + /** + * Enqueues scripts/styles for the Settings page. + * + * @access public + */ + public function enqueueSettingsScripts( string $hook_suffix ): void { + if ( $this->_settingsPage !== $hook_suffix ) { + return; + } + wp_enqueue_style( 'wp-color-picker' ); + wp_enqueue_script( 'wp-color-picker' ); + } + + /** + * Renders the global style settings page. + * + * @access public + */ + public function renderSettingsPage(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'visualizer' ) ); + } + $settings = self::getGlobalSettings(); + include_once VISUALIZER_ABSPATH . '/templates/global-settings.php'; + } + + /** + * Returns the global style settings option. + * + * @return array + * @access public + * @static + */ + public static function getGlobalSettings(): array { + $defaults = array( + 'color_primary' => '', + 'color_secondary' => '', + ); + $saved = get_option( self::OPTION_GLOBAL_SETTINGS, array() ); + return wp_parse_args( $saved, $defaults ); + } + + /** + * Handles saving of the global style settings. + * + * @access public + */ + public function saveGlobalSettings(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'visualizer' ) ); + } + + check_admin_referer( 'visualizer_save_global_settings' ); + + $color_primary = sanitize_hex_color( wp_unslash( $_POST['visualizer_color_primary'] ?? '' ) ); + $color_secondary = sanitize_hex_color( wp_unslash( $_POST['visualizer_color_secondary'] ?? '' ) ); + $clear = ! empty( $_POST['visualizer_clear_settings'] ); + + if ( $clear ) { + delete_option( self::OPTION_GLOBAL_SETTINGS ); + } else { + $settings = array( + 'color_primary' => $color_primary ? $color_primary : '', + 'color_secondary' => $color_secondary ? $color_secondary : '', + ); + update_option( self::OPTION_GLOBAL_SETTINGS, $settings ); + } + + wp_safe_redirect( + add_query_arg( + array( + 'page' => 'viz-settings', + 'updated' => $clear ? 'cleared' : 'true', + ), + admin_url( 'admin.php' ) + ) + ); + exit; + } + /** * Renders visualizer library page. * diff --git a/classes/Visualizer/Module/Chart.php b/classes/Visualizer/Module/Chart.php index 206778388..5ba67addd 100644 --- a/classes/Visualizer/Module/Chart.php +++ b/classes/Visualizer/Module/Chart.php @@ -804,7 +804,12 @@ private function _handleDataAndSettingsPage() { } // save meta data only when it is NOT being canceled. if ( ! $is_canceled ) { - update_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, $_POST ); + $post_settings = $_POST; + $existing = get_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, true ); + if ( isset( $existing['colors'] ) && is_array( $existing['colors'] ) && ! isset( $post_settings['colors'] ) ) { + $post_settings['colors'] = $existing['colors']; + } + update_post_meta( $this->_chart->ID, Visualizer_Plugin::CF_SETTINGS, $post_settings ); // we will keep a parameter called 'internal_title' that will be set to the given title or, if empty, the chart ID // this will help in searching with the chart id. diff --git a/classes/Visualizer/Module/Utility.php b/classes/Visualizer/Module/Utility.php index 0ac3b17a4..177cc6b7c 100644 --- a/classes/Visualizer/Module/Utility.php +++ b/classes/Visualizer/Module/Utility.php @@ -36,7 +36,7 @@ class Visualizer_Module_Utility extends Visualizer_Module { * @since 3.3.0 * * @access private - * @var _CHART_COLORS + * @var string[] */ private static $_CHART_COLORS = array( '#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', @@ -106,17 +106,83 @@ private static function hex2rgba( $color, $opacity = false ) { } /** - * Gets a random color from the array of chart colors and returns it as well as its transparent equivalent. - * - * @since 3.3.0 + * Returns a color at a specific index from the palette, with its transparent equivalent. * + * @return array{string, string} * @access private */ - private static function get_random_color() { - $color = self::$_CHART_COLORS[ rand( 0, count( self::$_CHART_COLORS ) - 1 ) ]; + private static function get_color_at( int $index ): array { + $colors = self::get_color_palette(); + $color = $colors[ $index % count( $colors ) ]; return array( self::hex2rgba( $color, 0.5 ), $color ); } + /** + * Mixes a hex color toward white by the given factor (0 = original, 1 = white). + * + * @access private + */ + private static function tint_color( string $hex, float $factor ): string { + $hex = ltrim( $hex, '#' ); + if ( strlen( $hex ) === 3 ) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + $r = (int) round( hexdec( substr( $hex, 0, 2 ) ) + ( 255 - hexdec( substr( $hex, 0, 2 ) ) ) * $factor ); + $g = (int) round( hexdec( substr( $hex, 2, 2 ) ) + ( 255 - hexdec( substr( $hex, 2, 2 ) ) ) * $factor ); + $b = (int) round( hexdec( substr( $hex, 4, 2 ) ) + ( 255 - hexdec( substr( $hex, 4, 2 ) ) ) * $factor ); + return sprintf( '#%02x%02x%02x', $r, $g, $b ); + } + + /** + * Returns the color palette to use for charts. + * + * When global primary/secondary colors are configured, generates a palette of + * alternating tints: [primary, secondary, primary@25%, secondary@25%, ...]. + * Falls back to the built-in colors otherwise. + * + * @return string[] + * @access private + */ + private static function get_color_palette(): array { + $global = self::get_global_style_defaults(); + $primary = $global['color_primary']; + $secondary = $global['color_secondary']; + + if ( empty( $primary ) && empty( $secondary ) ) { + return self::$_CHART_COLORS; + } + + $bases = array_filter( array( $primary, $secondary ) ); + $factors = array( 0, 0.25, 0.5, 0.75 ); + $palette = array(); + + foreach ( $factors as $factor ) { + foreach ( $bases as $base ) { + $palette[] = $factor === 0 ? $base : self::tint_color( $base, $factor ); + } + } + + return $palette; + } + + /** + * Returns the global style defaults stored in the plugin settings. + * + * @return array + * @access public + * @static + */ + public static function get_global_style_defaults(): array { + $option = get_option( Visualizer_Module_Admin::OPTION_GLOBAL_SETTINGS, array() ); + return wp_parse_args( + $option, + array( + 'color_primary' => '', + 'color_secondary' => '', + ) + ); + } + /** * Sets some defaults in the chart. * @@ -178,6 +244,14 @@ private static function set_defaults_google( $chart, $post_status ) { $attributes['candlestick']['risingColor']['fill'] = '#3366cc'; break; } + + // Apply global color defaults to new Google Charts (not Geo — it uses colorAxis). + if ( 'geo' !== $type ) { + $global = self::get_global_style_defaults(); + if ( ! empty( $global['color_primary'] ) || ! empty( $global['color_secondary'] ) ) { + $attributes['colors'] = self::get_color_palette(); + } + } } if ( $attributes ) { @@ -218,7 +292,7 @@ private static function set_defaults_chartjs( $chart, $post_status ) { // fall through. case 'bar': for ( $i = 0; $i < $max; $i++ ) { - $colors = self::get_random_color(); + $colors = self::get_color_at( $i ); $attributes[] = array( 'backgroundColor' => $colors[0], 'hoverBackgroundColor' => $colors[1] ); } break; @@ -228,7 +302,7 @@ private static function set_defaults_chartjs( $chart, $post_status ) { // fall through. case 'area': for ( $i = 0; $i < $max; $i++ ) { - $colors = self::get_random_color(); + $colors = self::get_color_at( $i ); $attributes[] = array( 'borderColor' => $colors[0] ); } break; diff --git a/classes/Visualizer/Render/Sidebar/Google.php b/classes/Visualizer/Render/Sidebar/Google.php index 5810cc1ed..78ea92812 100644 --- a/classes/Visualizer/Render/Sidebar/Google.php +++ b/classes/Visualizer/Render/Sidebar/Google.php @@ -453,4 +453,22 @@ protected function _renderViewSettings() { self::_renderSectionEnd(); self::_renderGroupEnd(); } + + /** + * Renders advanced settings and hidden inputs for programmatically-set keys + * (e.g. the global-preset colors palette) that have no visible sidebar control. + * + * @access protected + */ + protected function _renderAdvancedSettings(): void { + parent::_renderAdvancedSettings(); + + // @phpstan-ignore-next-line (property accessed via magic __get from _data array) + $colors = $this->colors; + if ( ! empty( $colors ) && is_array( $colors ) ) { + foreach ( $colors as $color ) { + echo ''; + } + } + } } diff --git a/classes/Visualizer/Render/Sidebar/Type/GoogleCharts/Geo.php b/classes/Visualizer/Render/Sidebar/Type/GoogleCharts/Geo.php index 2c07589c1..8d32fd0c5 100644 --- a/classes/Visualizer/Render/Sidebar/Type/GoogleCharts/Geo.php +++ b/classes/Visualizer/Render/Sidebar/Type/GoogleCharts/Geo.php @@ -465,4 +465,14 @@ protected function _renderViewSettings() { self::_renderSectionEnd(); self::_renderGroupEnd(); } + + /** + * Geo charts use colorAxis for their colour scheme, not the global preset palette. + * Skip the hidden colors[] inputs that the Google base class would otherwise inject. + * + * @access protected + */ + protected function _renderAdvancedSettings(): void { + Visualizer_Render_Sidebar::_renderAdvancedSettings(); + } } diff --git a/templates/global-settings.php b/templates/global-settings.php new file mode 100644 index 000000000..929ef8657 --- /dev/null +++ b/templates/global-settings.php @@ -0,0 +1,89 @@ + +
+

+ + +
+

+
+ +
+

+
+ + +
+ + + + + + + + + + + + + + + + + + + +

+ + +

+
+
+ + From 098f8bd2e5867f82124ba7466910b78dc134b938 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Fri, 27 Mar 2026 19:31:37 +0530 Subject: [PATCH 2/2] feat: add option to toggle palette for existing charts --- classes/Visualizer/Module/Admin.php | 3 + classes/Visualizer/Module/Utility.php | 150 ++++++++++++++++++++++++++ templates/global-settings.php | 18 ++++ 3 files changed, 171 insertions(+) diff --git a/classes/Visualizer/Module/Admin.php b/classes/Visualizer/Module/Admin.php index f4b0e1272..dd76f5f16 100644 --- a/classes/Visualizer/Module/Admin.php +++ b/classes/Visualizer/Module/Admin.php @@ -1116,6 +1116,7 @@ public static function getGlobalSettings(): array { $defaults = array( 'color_primary' => '', 'color_secondary' => '', + 'apply_existing' => '0', ); $saved = get_option( self::OPTION_GLOBAL_SETTINGS, array() ); return wp_parse_args( $saved, $defaults ); @@ -1135,6 +1136,7 @@ public function saveGlobalSettings(): void { $color_primary = sanitize_hex_color( wp_unslash( $_POST['visualizer_color_primary'] ?? '' ) ); $color_secondary = sanitize_hex_color( wp_unslash( $_POST['visualizer_color_secondary'] ?? '' ) ); + $apply_existing = ! empty( $_POST['visualizer_apply_existing'] ) ? '1' : '0'; $clear = ! empty( $_POST['visualizer_clear_settings'] ); if ( $clear ) { @@ -1143,6 +1145,7 @@ public function saveGlobalSettings(): void { $settings = array( 'color_primary' => $color_primary ? $color_primary : '', 'color_secondary' => $color_secondary ? $color_secondary : '', + 'apply_existing' => $apply_existing, ); update_option( self::OPTION_GLOBAL_SETTINGS, $settings ); } diff --git a/classes/Visualizer/Module/Utility.php b/classes/Visualizer/Module/Utility.php index 177cc6b7c..395ae1407 100644 --- a/classes/Visualizer/Module/Utility.php +++ b/classes/Visualizer/Module/Utility.php @@ -54,6 +54,7 @@ class Visualizer_Module_Utility extends Visualizer_Module { */ public function __construct( Visualizer_Plugin $plugin ) { parent::__construct( $plugin ); + $this->_addFilter( Visualizer_Plugin::FILTER_GET_CHART_SETTINGS, 'apply_global_style_settings', 999, 3 ); } @@ -179,10 +180,159 @@ public static function get_global_style_defaults(): array { array( 'color_primary' => '', 'color_secondary' => '', + 'apply_existing' => '0', ) ); } + /** + * Applies global styles to existing charts at render time. + * + * @param array|mixed $settings Chart settings. + * @param int $chart_id Chart ID. + * @param string $type Chart type. + * @return array + * @access public + */ + public function apply_global_style_settings( $settings, $chart_id, $type ): array { + $settings = is_array( $settings ) ? $settings : array(); + $global = self::get_global_style_defaults(); + + if ( empty( $global['apply_existing'] ) ) { + return $settings; + } + + if ( empty( $global['color_primary'] ) && empty( $global['color_secondary'] ) ) { + return $settings; + } + + if ( empty( $chart_id ) ) { + return $settings; + } + + $library = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true ); + $library = is_string( $library ) ? strtolower( $library ) : ''; + if ( empty( $type ) ) { + $type = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, true ); + } + $type = is_string( $type ) ? strtolower( $type ) : ''; + + if ( empty( $library ) ) { + $library = in_array( $type, array( 'datatable', 'tabular' ), true ) ? 'datatable' : 'googlecharts'; + } + + if ( 'googlecharts' === $library || 'google' === $library ) { + if ( 'geo' !== $type ) { + $has_explicit = false; + if ( ! empty( $settings['colors'] ) ) { + $has_explicit = true; + } + if ( ! $has_explicit && isset( $settings['series'] ) && is_array( $settings['series'] ) ) { + foreach ( $settings['series'] as $series_settings ) { + if ( ! empty( $series_settings['color'] ) ) { + $has_explicit = true; + break; + } + } + } + if ( ! $has_explicit && isset( $settings['slices'] ) && is_array( $settings['slices'] ) ) { + foreach ( $settings['slices'] as $slice_settings ) { + if ( ! empty( $slice_settings['color'] ) ) { + $has_explicit = true; + break; + } + } + } + if ( ! $has_explicit ) { + $settings['colors'] = self::get_color_palette(); + } + } + return $settings; + } + + if ( 'chartjs' === $library ) { + $has_explicit = false; + if ( isset( $settings['series'] ) && is_array( $settings['series'] ) ) { + foreach ( $settings['series'] as $series_settings ) { + if ( ! empty( $series_settings['backgroundColor'] ) || ! empty( $series_settings['borderColor'] ) || ! empty( $series_settings['hoverBackgroundColor'] ) ) { + $has_explicit = true; + break; + } + } + } + if ( ! $has_explicit && isset( $settings['slices'] ) && is_array( $settings['slices'] ) ) { + foreach ( $settings['slices'] as $slice_settings ) { + if ( ! empty( $slice_settings['backgroundColor'] ) || ! empty( $slice_settings['borderColor'] ) || ! empty( $slice_settings['hoverBackgroundColor'] ) ) { + $has_explicit = true; + break; + } + } + } + if ( $has_explicit ) { + return $settings; + } + $series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true ); + if ( is_array( $series ) ) { + $settings = self::apply_chartjs_palette( $settings, $type, $series, $chart_id ); + } + } + + return $settings; + } + + /** + * Applies the global palette to ChartJS settings for a chart. + * + * @param array $settings Current chart settings. + * @param string $type Chart type. + * @param array $series Series definitions. + * @param int $chart_id Chart ID. + * @return array + * @access private + * @static + */ + private static function apply_chartjs_palette( array $settings, string $type, array $series, int $chart_id ): array { + $attributes = array(); + $name = 'series'; + $count = count( $series ); + $max = $count - 1; + + switch ( $type ) { + case 'polarArea': + // fall through. + case 'pie': + $chart = get_post( $chart_id ); + $data = $chart instanceof WP_Post ? maybe_unserialize( $chart->post_content ) : array(); + $name = 'slices'; + $max = is_array( $data ) ? count( $data ) : $count; + // fall through. + case 'column': + // fall through. + case 'bar': + for ( $i = 0; $i < $max; $i++ ) { + $colors = self::get_color_at( $i ); + $attributes[] = array( 'backgroundColor' => $colors[0], 'hoverBackgroundColor' => $colors[1] ); + } + break; + case 'radar': + // fall through. + case 'line': + // fall through. + case 'area': + for ( $i = 0; $i < $max; $i++ ) { + $colors = self::get_color_at( $i ); + $attributes[] = array( 'borderColor' => $colors[0] ); + } + break; + } + + if ( $attributes ) { + $settings[ $name ] = $attributes; + } + + return $settings; + } + /** * Sets some defaults in the chart. * diff --git a/templates/global-settings.php b/templates/global-settings.php index 929ef8657..2e4a03bd5 100644 --- a/templates/global-settings.php +++ b/templates/global-settings.php @@ -66,6 +66,24 @@ class="visualizer-color-picker" + + + + + + + + +