diff --git a/README.txt b/README.txt index 33a44744bda070fce0676c7dd2e05416803451f8..9290dc70009770fc136ad15b8985a6f8b4bdb823 100644 --- a/README.txt +++ b/README.txt @@ -88,6 +88,34 @@ Tip: You may find it easier to start with a "Table" display and convert it to a chart display after setting up the data. It can be easier to visualize what the result of the chart will be if it's been laid out in a table first. +Creating Multiple Series and Combo Charts in the UI +--------------------------------------------------- + +When using Views to build your charts, you may find it difficult to retrieve +more than a single set of data generated by a COUNT() query. For example if you +wanted to retrieve the age of all your site users, but display "Male" and +"Female" values in a column chart at the same time, constructing the underlying +table of data is quite difficult. + +To solve this problem, you can combine multiple charts on top of each other. The +"parent" chart provides the global information, such as the height, width, +title, and other properties. Charts that are "children" provide only data and +(optionally) a secondary axis. After you've assembled the first series of data +in your chart according to the instructions in the "Creating Charts in the UI" +section, add a new display to the same view of the type "Chart Add-on". The +"Chart Add-on" type is added the same way you would add a new Page or Block +display, from the "+ Add" menu at the top of the view configuration. + +After this new display has been added, find the setting for "Combine with parent +chart" and change this value to point at the parent chart you have already +assembled. Then adjust the settings for the child chart to pull in different +data (often by overriding the filter settings). Now you can go back to your +parent display, and see that the results from the child chart have been +merged into the results from the parent chart. You can even use this approach +to combine different types of charts, such as a line chart over the top of +a column chart. Note that not all chart types can be combined together and +invalid combinations may cause your chart to throw errors. + Create Charts through the API ----------------------------- diff --git a/charts.info b/charts.info index 193d8a986f3d146ef9af5204f0183535b6446bce..2bb80e779529b4479917e678d922ffea8b40834f 100644 --- a/charts.info +++ b/charts.info @@ -5,3 +5,4 @@ core = 7.x php = 5.2 files[] = views/charts_plugin_style_chart.inc +files[] = views/charts_plugin_display_chart.inc diff --git a/js/charts.admin.js b/js/charts.admin.js index 74c2f0946cb94ab83f208fdb8d55b631a1fcf4fd..af616524f125da38dea5d0030531f90edc3695c7 100644 --- a/js/charts.admin.js +++ b/js/charts.admin.js @@ -10,7 +10,9 @@ Drupal.behaviors.chartsAdmin.attach = function(context, settings) { $(context).find('.form-radios.chart-type-radios').once('charts-axis-inverted-processed', function() { // Manually attach collapsible fieldsets first. - Drupal.behaviors.collapse.attach(context, settings); + if (Drupal.behaviors.collapse) { + Drupal.behaviors.collapse.attach(context, settings); + } var xAxisLabel = $('fieldset.chart-xaxis .fieldset-title').html(); var yAxisLabel = $('fieldset.chart-yaxis .fieldset-title').html(); diff --git a/views/charts.views.inc b/views/charts.views.inc index d4e54a96382070fc7a70f4e60f84528c523b6448..eaf21a2f323b15b0595a28756e14faeedfe9735b 100644 --- a/views/charts.views.inc +++ b/views/charts.views.inc @@ -23,6 +23,22 @@ function charts_views_plugins() { 'uses grouping' => FALSE, 'type' => 'normal', ); + $plugins['style']['chart_extension'] = $plugins['style']['chart']; + $plugins['style']['chart_extension']['type'] = 'chart'; + + $plugins['display']['chart'] = array( + 'title' => t('Chart add-on'), + 'admin' => t('Chart add-on'), + 'help' => t('Add chart data to a new or existing chart.'), + 'handler' => 'charts_plugin_display_chart', + 'theme' => 'views_view', + 'register theme' => FALSE, + 'use ajax' => FALSE, + 'use pager' => FALSE, + 'use more' => FALSE, + 'accept attachments' => FALSE, + 'type' => 'chart', + ); return $plugins; } diff --git a/views/charts_plugin_display_chart.inc b/views/charts_plugin_display_chart.inc new file mode 100644 index 0000000000000000000000000000000000000000..7d7bb29da08ca095105955ec879d618c9d97c978 --- /dev/null +++ b/views/charts_plugin_display_chart.inc @@ -0,0 +1,130 @@ + ''); + $options['inherit_yaxis'] = array('default' => '1'); + + return $options; + } + + /** + * Provide the summary for page options in the views UI. + * + * This output is returned as an array. + */ + function options_summary(&$categories, &$options) { + // It is very important to call the parent function here: + parent::options_summary($categories, $options); + + $categories['chart'] = array( + 'title' => t('Chart settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $parent_title = NULL; + $parent_display = $this->get_option('parent_display'); + if (!empty($this->view->display[$parent_display])) { + $parent_title = check_plain($this->view->display[$parent_display]->display_title); + } + $options['parent_display'] = array( + 'category' => 'chart', + 'title' => t('Combine with parent chart'), + 'value' => $parent_title ? $parent_title : t('None'), + ); + $options['inherit_yaxis'] = array( + 'category' => 'chart', + 'title' => t('Axis settings'), + 'value' => $this->get_option('inherit_yaxis') ? t('Use primary Y-axis') : t('Create secondary axis'), + ); + + } + + /** + * Provide the default form for setting options. + */ + function options_form(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_form($form, $form_state); + + switch ($form_state['section']) { + case 'parent_display': + $form['#title'] .= t('Parent display'); + + // Filter down the list of displays to include only those that use + // the chart display style. + $display_options = array(); + foreach ($this->view->display as $display_name => $display) { + if ($this->view->display[$display_name]->display_options['style_plugin'] === 'chart' && $display_name !== $this->view->current_display) { + $display_options[$display_name] = $this->view->display[$display_name]->display_title; + } + } + $form['parent_display'] = array( + '#title' => t('Parent display'), + '#type' => 'select', + '#options' => $display_options, + '#empty_option' => t('- None - '), + '#required' => TRUE, + '#default_value' => $this->get_option('parent_display'), + '#description' => t('Select a parent display onto which this chart will be overlaid. Only other displays using a "Chart" format are included here. This option may be used to create charts with several series of data or to create combination charts.'), + ); + break; + case 'inherit_yaxis': + $form['#title'] .= t('Axis settings'); + $form['inherit_yaxis'] = array( + '#title' => t('Y-Axis settings'), + '#type' => 'radios', + '#options' => array( + 1 => t('Inherit primary of parent display'), + 0 => t('Create a secondary axis'), + ), + '#default_value' => $this->get_option('inherit_yaxis'), + '#description' => t('In most charts, the X and Y axis from the parent display are both shared with each attached child chart. However, if this chart is going to use a different unit of measurement, a secondary axis may be added on the opposite side of the normal Y-axis.'), + ); + break; + + } + } + + /** + * Perform any necessary changes to the form values prior to storage. + * There is no need for this function to actually store the data. + */ + function options_submit(&$form, &$form_state) { + // It is very important to call the parent function here: + parent::options_submit($form, $form_state); + switch ($form_state['section']) { + case 'parent_display': + case 'inherit_yaxis': + $this->set_option($form_state['section'], $form_state['values'][$form_state['section']]); + break; + } + } +} + diff --git a/views/charts_plugin_style_chart.inc b/views/charts_plugin_style_chart.inc index 67758761281c4022ad6729e0adbbab24f0c414ec..cf27d28816635b3261e6d8a63e4eb211b29d3fdb 100644 --- a/views/charts_plugin_style_chart.inc +++ b/views/charts_plugin_style_chart.inc @@ -1,7 +1,7 @@ plugin_name === 'chart_extension') { + $options['type']['default'] = NULL; + } + return $options; } @@ -44,6 +50,27 @@ class charts_plugin_style_chart extends views_plugin_style { module_load_include('inc', 'charts', 'includes/charts.pages'); $field_options = $this->display->handler->get_field_labels(); $form = charts_settings_form($form, $this->options, $field_options, array('style_options')); + + // Reduce the options if this is a chart extension. + if ($parent_display = $this->get_parent_chart_display()) { + $parent_chart_type = chart_get_type($parent_display->display_options['style_options']['type']); + if (empty($form['type']['#default_value'])) { + $form['type']['#default_value'] = $parent_display->display_options['style_options']['type']; + } + + $form['type']['#description'] = empty($form['type']['#description']) ? '' : $form['type']['#description'] . ' '; + $form['type']['#description'] .= t('This chart will be combined with the parent display "@display_title", which is a "@type" chart. Not all chart types may be combined. Selecting a different chart type than the parent may cause errors.', array('@display_title' => $parent_display->display_title, '@type' => $parent_chart_type['label'])); + $form['fields']['label_field']['#disabled'] = TRUE; + $form['display']['#access'] = FALSE; + $form['xaxis']['#access'] = FALSE; + if ($this->display->handler->options['inherit_yaxis']) { + $form['yaxis']['#access'] = FALSE; + } + else { + $form['yaxis']['#title'] = t('Secondary axis'); + $form['yaxis']['#attributes']['class'] = array(); + } + } } /** @@ -68,7 +95,12 @@ class charts_plugin_style_chart extends views_plugin_style { return; } - if (count($field_handlers)) { + // Make sure that all chart extensions first have a parent chart selected. + if ($this->plugin_name === 'chart_extension' && $this->get_parent_chart_display() === FALSE) { + $errors[] = t('This chart add-on must have a parent chart selected under the chart settings.'); + } + // Make sure that at least one data column has been selected. + elseif (count($field_handlers)) { $data_field_key = !empty($this->options['data_fields']) ? current($this->options['data_fields']) : NULL; if (empty($data_field_key)) { $errors[] = t('At least one data field must be selected in the chart configuration before this chart may be shown'); @@ -88,14 +120,17 @@ class charts_plugin_style_chart extends views_plugin_style { * Define and display a chart from the grouped values. */ function render() { - // Calculate the labels field alias. $field_handlers = $this->display->handler->get_handlers('field'); + + // Calculate the labels field alias. $label_field = FALSE; $label_field_key = NULL; if ($this->options['label_field'] && array_key_exists($this->options['label_field'], $field_handlers)) { $label_field = $field_handlers[$this->options['label_field']]; $label_field_key = $this->options['label_field']; } + + // Assemble the fields to be used to provide data access. $data_field_options = array_filter($this->options['data_fields']); $data_fields = array(); foreach ($data_field_options as $field_key) { @@ -145,7 +180,7 @@ class charts_plugin_style_chart extends views_plugin_style { $chart['#legend_title'] = $label_field->options['label']; } - $chart['series'] = array( + $chart[$this->view->current_display . '_series'] = array( '#type' => 'chart_data', '#data' => $data, '#title' => $data_field->options['label'], @@ -165,7 +200,7 @@ class charts_plugin_style_chart extends views_plugin_style { '#min' => $this->options['yaxis_min'], ); foreach ($data_fields as $field_key => $field_handler) { - $chart[$field_key] = array( + $chart[$this->view->current_display . '__' . $field_key] = array( '#type' => 'chart_data', '#data' => array(), '#color' => isset($this->options['field_colors'][$field_key]) ? $this->options['field_colors'][$field_key] : NULL, @@ -182,13 +217,101 @@ class charts_plugin_style_chart extends views_plugin_style { $chart['xaxis']['#labels'][] = $renders[$row_number][$label_field_key]; } foreach ($data_fields as $field_key => $field_handler) { - $chart[$field_key]['#data'][] = (float) $renders[$row_number][$field_key]; + $chart[$this->view->current_display . '__' . $field_key]['#data'][] = (float) $renders[$row_number][$field_key]; + } + } + } + + // Check if this display has any children charts that should be applied + // on top of it. + $parent_display_id = $this->view->current_display; + $children_displays = $this->get_children_chart_displays(); + foreach ($children_displays as $child_display_id => $child_display) { + // If the user doesn't have access to the child display, skip. + if (!$this->view->access($child_display_id)) { + continue; + } + + // Generate the subchart by executing the child display. We load a fresh + // view here to avoid collisions in shifting the current display while in + // a display. + $subview = $this->view->clone_view(); + $subview->set_display($child_display_id); + + // Copy the settings for our axes over to the child view. + foreach ($this->options as $option_name => $option_value) { + if (strpos($option_name, 'yaxis') === 0 && $child_display->display_options['inherit_yaxis']) { + $subview->display_handler->options['style_options'][$option_name] = $option_value; + } + elseif (strpos($option_name, 'xaxis') === 0) { + $subview->display_handler->options['style_options'][$option_name] = $option_value; + } + } + + // Execute the subview and get the result. + $subview->pre_execute(); + $subview->execute(); + $subchart = $subview->style_plugin->render($subview->result); + $subview->post_execute(); + unset($subview); + + // Create a secondary axis if needed. + if (!$child_display->display_options['inherit_yaxis'] && isset($subchart['yaxis'])) { + $chart['secondary_yaxis'] = $subchart['yaxis']; + $chart['secondary_yaxis']['#opposite'] = TRUE; + } + + // Merge in the child chart data. + foreach (element_children($subchart) as $key) { + if ($subchart[$key]['#type'] === 'chart_data') { + $chart[$key] = $subchart[$key]; + // If the subchart is a different type than the parent chart, set + // the #chart_type property on the individual chart data elements. + if ($chart[$key]['#chart_type'] !== $chart['#chart_type']) { + $chart[$key]['#chart_type'] = $subchart['#chart_type']; + } + if (!$child_display->display_options['inherit_yaxis']) { + $chart[$key]['#target_axis'] = 'secondary_yaxis'; + } } } } // Print the chart. - return drupal_render($chart); + return $chart; + } + + /** + * Utility function to check if this chart has a parent display. + */ + function get_parent_chart_display() { + $parent_display = FALSE; + if ($this->plugin_name === 'chart_extension' && $this->display && $this->display->handler->options['parent_display']) { + $parent_display_name = $this->display->handler->options['parent_display']; + if (isset($this->view->display[$parent_display_name])) { + $parent_display = $this->view->display[$parent_display_name]; + } + } + // Ensure the parent is a chart. + if ($parent_display && $parent_display->display_options['style_plugin'] !== 'chart') { + $parent_display = FALSE; + } + return $parent_display; } + /** + * Utility function to check if this chart has children displays. + */ + function get_children_chart_displays() { + $children_displays = array(); + foreach ($this->view->display as $display_name => $display) { + if ($display->display_plugin === 'chart' && $display->display_options['parent_display']) { + $parent_display_name = $display->display_options['parent_display']; + if ($parent_display_name === $this->view->current_display) { + $children_displays[$display_name] = $this->view->display[$display_name]; + } + } + } + return $children_displays; + } }