Skip to content

Commit

Permalink
Charts spike WIP (#95)
Browse files Browse the repository at this point in the history
* Updates to charts front-end code

    - Create a macro that could be repurposed by the design system
    - Tidy up and refactor all the JavaScript
    - Don't include the annotations in the chart config from the back-end, and instead build this in the JavaScript to create the arrows
    - Fix missing annotations JS

* Fix annotations config

* Adapat the bar chart height calculations to be more accurate and adapt for cluster charts

* Improve help text

* Avoid accidentally overriding option values
  • Loading branch information
helenb authored Feb 10, 2025
1 parent 3cfc4f1 commit 2e8c493
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 166 deletions.
47 changes: 11 additions & 36 deletions cms/datavis/models/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ class Chart(Visualisation):

show_legend = models.BooleanField(verbose_name=_("show legend?"), default=True) # type: ignore[var-annotated]
show_value_labels = models.BooleanField( # type: ignore[var-annotated]
verbose_name=_("show value labels?"), default=False
verbose_name=_("show value labels?"),
default=False,
help_text=_("For cluster charts with 3 or more series, the data labels will be hidden."),
)
theme = models.CharField( # type: ignore[var-annotated]
verbose_name=_("theme"),
Expand Down Expand Up @@ -95,8 +97,8 @@ def media(self) -> Media:

def get_context(self, request: Optional["HttpRequest"] = None, **kwargs: Any) -> dict[str, Any]:
config = self.get_component_config(self.primary_data_source.headers, self.primary_data_source.rows)
# theme = self.theme
return super().get_context(request, config=config, **kwargs)
annotations_values = self.get_annotations_values()
return super().get_context(request, config=config, annotations_values=annotations_values, **kwargs)

general_panels: ClassVar[Sequence["Panel"]] = [
FieldPanel("name"),
Expand Down Expand Up @@ -200,13 +202,6 @@ def get_component_config(self, headers: Sequence[str], rows: Sequence[list[str |
},
"xAxis": self.get_x_axis_config(headers, rows),
"yAxis": self.get_y_axis_config(headers, rows),
"navigation": {
"enabled": False,
},
"annotations": self.get_annotations_config(),
"credits": {
"enabled": False,
},
"series": self.get_series_data(headers, rows),
}

Expand Down Expand Up @@ -254,38 +249,18 @@ def get_y_axis_config(
config["max"] = self.y_max
return config

def get_annotations_config(self) -> list[dict[str, Any]]:
config: list[dict[str, Any]] = []
annotation_group: dict[str, Any] = {
"draggable": "",
"labelOptions": {
"backgroundColor": "rgba(255,255,255,0.5)",
"verticalAlign": "top",
},
}
# TODO: It's likely we'll want to support a few different style
# options for annotations, in which case, we'd split annotations
# into multiple groups, each with a separate 'labelOptions' value
# to control the styling.
group_labels: list[dict[str, Any]] = []
def get_annotations_values(self) -> list[dict[str, Any]]:
annotation_values: list[dict[str, Any]] = []
for item in self.annotations.raw_data: # pylint: disable=no-member
value = item["value"]
group_labels.append(
annotation_values.append(
{
"text": value["label"],
"point": {
"x": numberfy(value["x_position"]),
"y": numberfy(value["y_position"]),
"xAxis": 0,
"yAxis": 0,
},
"xValue": numberfy(value["x_position"]),
"yValue": numberfy(value["y_position"]),
}
)

if group_labels:
annotation_group["labels"] = group_labels
config.append(annotation_group)
return config
return annotation_values

def get_series_data(self, headers: Sequence[str], rows: Sequence[list[str | int | float]]) -> list[dict[str, Any]]:
series = []
Expand Down
37 changes: 37 additions & 0 deletions cms/jinja2/templates/datavis/_macro.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% macro onsChart(params) %}
<div data-highcharts-base-chart data-highcharts-type="{{ params.chartType }}" data-highcharts-theme="{{ params.theme }}" data-highcharts-title="{{ params.title }}" {% if params.use_stacked_layout %}data-highcharts-use-stacked-layout{% endif %}>
<figure id="{{ params.domId }}">
{# Heading levels will need to be configurable depending on page surroundings #}
<h3>{{ params.title }}</h3>
<h4>{{ params.subtitle }}</h4>
<div data-highcharts-chart data-highcharts-id="{{ params.uuid }}"></div>
{% if params.caption %}
<figcaption>{{ params.caption }}</figcaption>
{% endif %}
</figure>

<!-- Set scripts to pass the config and annotations values as json to the javascript -->
{% set config_scriptid = ["config--", params.uuid] | join %}
{{ params.config|json_script(config_scriptid) }}

{% if params.annotations_values %}
{% set annotations_values_scriptid = ["annotations-values--", params.uuid] | join %}
{{ params.annotations_values|json_script(annotations_values_scriptid) }}
{% endif %}

{% if params.download_title and (params.download_image or params.download_csv or params.download_excel) %}
{% if params.download_title %}<h4>{{ params.download_title }}</h4>{% endif %}
<ol>
{% if params.download_excel %}
<li><a href="{{ params.download_excel }}">Excel spreadsheet (XLSX format{% if params.download_excel_size %}, {{ params.download_excel_size }}{% endif %})</a></li>
{% endif %}
{% if params.download_csv %}
<li><a href="{{ params.download_csv }}">Simple text file (CSV format{% if params.download_csv_size %}, {{ params.download_csv_size }}{% endif %})</a></li>
{% endif %}
{% if params.download_image %}
<li><a href="{{ params.download_image }}">Image (PNG format{% if params.download_image_size %}, {{ params.download_image_size }}{% endif %})</a></li>
{% endif %}
</ol>
{% endif %}
</div>
{% endmacro %}
43 changes: 28 additions & 15 deletions cms/jinja2/templates/datavis/base_highcharts_chart.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
<div data-highcharts-base-chart data-highcharts-type="{{ object.highcharts_chart_type }}" data-highcharts-theme="{{ object.theme }}" data-highcharts-title="{{ object.title }}">
{% if not media_already_loaded and object.media %}
{{ object.media }}
{% endif %}
<figure class="highcharts-figure" id="chart-figure--{{ object.uuid }}">
<h2>{{ object.title }}</h2>
<h3>{{ object.subtitle }}</h3>
<div data-highcharts-chart data-highcharts-id="{{ object.uuid }}"></div>
{% if object.caption %}
<figcaption>{{ object.caption|richtext }}</figcaption>
{% endif %}
</figure>
{% set scriptid = ["config--", object.uuid] | join %}
{{ config|json_script(scriptid) }}
</div>
{% from "templates/datavis/_macro.njk" import onsChart %}

{% if not media_already_loaded and object.media %}
{{ object.media }}
{% endif %}

{# fmt:off #}
{{
onsChart({
"chartType": object.highcharts_chart_type,
"theme": object.theme,
"title": object.title,
"subtitle": object.subtitle,
"uuid": object.uuid,
"caption": object.caption|richtext,
"use_stacked_layout": object.use_stacked_layout,
"config": config,
"annotations_values": annotations_values,
"download_title": "Figure 1 data",
"download_image": "xyz",
"download_image_size": "18KB",
"download_csv": "xyz",
"download_csv_size": "25KB",
"download_excel": "xyz",
"download_excel_size": "25KB",
})
}}
{# fmt:on #}
37 changes: 37 additions & 0 deletions cms/static_src/javascript/components/bar-chart-plot-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ChartConstants from './chart-constants';

class BarChartPlotOptions {
static plotOptions() {
return this.plotOptions;
}

constructor() {
const constants = ChartConstants.constants();
this.plotOptions = {
bar: {
// Set the width of the bars to be 30px
// The spacing is worked out in the highcharts-base-chart.js file
pointWidth: 30, // Fixed bar height
pointPadding: 0,
groupPadding: 0,
borderWidth: 0,
borderRadius: 0,
// Set the data labels to be enabled and positioned outside the bars
// We can add custom formatting on each chart to move the labels inside the bars if the bar is wide enough
dataLabels: {
enabled: true,
inside: false,
style: {
textOutline: 'none',
// there is no semibold font weight available in the design system fonts, so we use 400 instead
fontWeight: '400',
color: constants.labelColor,
fontSize: constants.mobileFontSize,
},
},
},
};
}
}

export default BarChartPlotOptions;
30 changes: 30 additions & 0 deletions cms/static_src/javascript/components/chart-constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class ChartConstants {
static constants() {
const constants = {
primaryTheme: [
'#206095',
'#27a0cc',
'#003c57',
'#118c7b',
'#a8bd3a',
'#871a5b',
'#f66068',
'#746cb1',
'#22d0b6',
],
// Alternate theme colours from https://service-manual.ons.gov.uk/data-visualisation/colours/using-colours-in-charts
alternateTheme: ['#206095', '#27A0CC', '#871A5B', '#A8BD3A', '#F66068'],
labelColor: '#414042',
axisLabelColor: '#707071',
gridLineColor: '#d9d9d9',
zeroLineColor: '#b3b3b3',
// Responsive font sizes
mobileFontSize: '0.875rem',
desktopFontSize: '1rem',
};

return constants;
}
}

export default ChartConstants;
17 changes: 17 additions & 0 deletions cms/static_src/javascript/components/column-chart-plot-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class ColumnChartPlotOptions {
static plotOptions() {
return this.plotOptions;
}

constructor() {
this.plotOptions = {
column: {
groupPadding: 0.2,
borderWidth: 0,
borderRadius: 0,
},
};
}
}

export default ColumnChartPlotOptions;
Loading

0 comments on commit 2e8c493

Please sign in to comment.