Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing implementation of wagtailcharts #7250

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
047dc4e
WIP implementation of WagtailCharts on Browse page
csebianlander Sep 21, 2022
c07eb8b
Properly load Wagtail Charts JS
chosak Sep 21, 2022
6d207c9
Add wagtailcharts to requirements
chosak Sep 21, 2022
255889d
Remove unnecessary template
chosak Sep 21, 2022
379df3f
Get wagtailcharts working with version 0.5
chosak Jul 25, 2024
e511ae5
Adding CFPB data viz color palette
csebianlander Jul 26, 2024
2e346af
Update charts migration
willbarton Aug 26, 2024
7c266cd
Update charts migration
willbarton Sep 19, 2024
9263da1
Update wagtailcharts to fix localization
willbarton Sep 19, 2024
4f5014b
Add custom Chart.js plugin to accommodate CFPB design needs
contolini Sep 24, 2024
8ef9813
Add background patterns to bar chart bars
contolini Sep 25, 2024
0dca81e
Accommodate both horizontal and vertical bar chart patterns
contolini Sep 26, 2024
4e33d03
Remove front-end cruft
contolini Sep 26, 2024
8268935
Add our fields to chart block
willbarton Sep 26, 2024
84161ff
Remove unnecessary imports
willbarton Sep 27, 2024
390a82b
Reorder chart fields for the content editor
willbarton Sep 27, 2024
9c16139
Add a quick test of our chartblock overrides
willbarton Sep 27, 2024
cd85cf1
Remove duplicate chart block
willbarton Sep 27, 2024
5a4625f
Recreate migrations
willbarton Sep 27, 2024
cee7a1d
Position pre-heading above the title field
willbarton Sep 30, 2024
a188c9b
Recreate migrations again
willbarton Sep 30, 2024
35d850e
Fix download links
willbarton Sep 30, 2024
d841739
Customize Chart.js bar chart patterns
contolini Sep 30, 2024
bd434ab
Update bar chart patterns, update wagtailcharts dependencies
contolini Oct 2, 2024
b5f0237
Accommodate row-based entry of data in CMS
contolini Oct 2, 2024
3b741dd
Add wagtail charts stylesheet and clean up HTML template
contolini Oct 4, 2024
027879f
Remove wagtail charts CSS cruft
contolini Oct 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cfgov/cfgov/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"filing_instruction_guide",
"health_check",
"health_check.db",
"wagtailcharts",
# Satellites
"complaint_search",
"countylimits",
Expand Down
18 changes: 18 additions & 0 deletions cfgov/unprocessed/css/on-demand/wagtail-chart.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@use 'sass:math';
@use '@cfpb/cfpb-design-system/src/abstracts' as *;
@import '../main';

.o-wagtail-chart {
max-width: math.div(670px, $base-font-size-px) + em;
margin-bottom: math.div(60px, $base-font-size-px) + em;

&__subtitle {
margin: 0 0 (math.div(30px, $base-font-size-px) + em);
}

&__footnote {
max-width: math.div(670px, $size-vi) + rem;
padding-top: math.div(15px, $size-vi) + em;
font-size: 0.75em;
}
}
126 changes: 126 additions & 0 deletions cfgov/unprocessed/js/routes/on-demand/wagtail-charts-chart-block.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* eslint-disable no-undef */
import pattern from 'patternomaly';

/**
* Set default text color to a dark gray
*
* https://www.chartjs.org/docs/latest/general/colors.html
*/
Chart.defaults.color = '#5a5d61';

/**
* Takes an array of Chart.js datasets and returns a new array
* with a different line pattern assigned to each dataset's
* borderDash property.
*
* The first line pattern is solid, the second is dashed,
* the third is dotted and all subsequent patterns are dashed
* with an increasingly thicker line.
*
* @param {array} datasets - Array of Chart.js datasets
* @returns {array} Array of Chart.js datasets with borderDash property set
*
* https://www.chartjs.org/docs/latest/samples/line/styling.html
* https://www.chartjs.org/docs/latest/configuration/#dataset-configuration
* https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash
*/
const patternizeChartLines = (datasets) => {
const DASH_THICKNESS = 5;
const DASH_PATTERNS = [
[0, 0], // solid
[DASH_THICKNESS, 2], // dashed
[2, 1], // dotted
];
return datasets.map((dataset, i) => {
dataset.borderDash = DASH_PATTERNS[i] || [DASH_THICKNESS * i, 2];
return dataset;
});
};

/**
* Takes an array of Chart.js datasets and returns a new array
* with a different pattern assigned to each dataset's
* backgroundColor property.
*
* Patterns are from the patternomaly library.
*
* @param {array} datasets - Array of Chart.js datasets
* @returns {array} Array of Chart.js datasets with backgroundColor property set
*
* https://www.chartjs.org/docs/latest/general/colors.html#patterns-and-gradients
* https://github.com/ashiguruma/patternomaly
*/
const patternizeChartBars = (datasets) => {
const patterns = [
'dot',
'diagonal',
'dash',
'cross-dash',
'zigzag-vertical',
'dot-dash',
'plus',
'cross',
'disc',
'ring',
'line',
'line-vertical',
'weave',
'zigzag',
'diagonal-right-left',
'square',
'box',
'triangle',
'triangle-inverted',
'diamond',
'diamond-box',
];
return datasets.map((dataset, i) => {
dataset.backgroundColor = dataset.data.map(() => {
// First pattern is just the solid color
if (i === 0)
return Array.isArray(dataset.backgroundColor)
? dataset.backgroundColor[0]
: dataset.backgroundColor;
return pattern.draw(
patterns[i - 1],
dataset.backgroundColor,
'rgba(255, 255, 255, 0.6)',
10,
);
});
return dataset;
});
};

/**
* Change the default Chart.js tooltip options
*/
const tooltipOptions = {
yAlign: 'bottom',
displayColors: false,
};

/**
* Define a Chart.js plugin for our CFPB customizations
*
* https://www.chartjs.org/docs/latest/developers/plugins.html
*/
const ChartjsPluginCFPB = {
id: 'cfpb-charts',
beforeInit: (chart) => {
chart.config.options.plugins.tooltip = tooltipOptions;

if (chart.config.type === 'line') {
patternizeChartLines(chart.config.data.datasets);
}

if (chart.config.type === 'bar') {
patternizeChartBars(chart.config.data.datasets);
}

chart.update();
},
};

Chart.register(ChartjsPluginStacked100.default);
Chart.register({ ChartjsPluginCFPB });
108 changes: 108 additions & 0 deletions cfgov/v1/atomic_elements/charts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from wagtail import blocks

from wagtailcharts.blocks import ChartBlock as WagtailChartBlock

from v1 import blocks as v1_blocks


CHART_TYPES = (
("line", "Line Chart"),
("bar", "Vertical Bar Chart"),
("bar_horizontal", "Horizontal Bar Chart"),
("pie", "Pie Chart"),
)


CHART_COLORS = (
("#addc91", "Green 60"),
("#1fa040", "Mid Dark Green"),
("#257675", "Teal"),
("#89b6b5", "Teal 60"),
("#d14124", "Red"),
("#e79e8e", "Red 60"),
("#0072ce", "Pacific"),
("#7eb7e8", "Pacific 60"),
("#254b87", "Navy"),
("#9daecc", "Navy 50"),
("#dc731c", "Dark Gold"),
("#ffc372", "Gold 70"),
("#745745", "Dark Neutral"),
("#baa496", "Neutral 60"),
("#a01b68", "Dark Purple"),
("#dc9cbf", "Purple 50"),
("#d2d3d5", "Gray 20"),
)


class ChartBlock(WagtailChartBlock):
eyebrow = blocks.CharBlock(
required=False,
help_text=(
"Optional: Adds an H5 eyebrow above H1 heading text. "
"Only use in conjunction with heading."
),
label="Pre-heading",
)
title = v1_blocks.HeadingBlock(required=False)
intro = blocks.RichTextBlock(required=False, icon="edit")
description = blocks.TextBlock(
required=False, help_text="Accessible description of the chart content"
)
data_source = blocks.TextBlock(
required=False,
help_text="Description of the data source",
)
date_published = blocks.CharBlock(
required=False, help_text="When the underlying data was published"
)
download_text = blocks.CharBlock(
required=False,
help_text="Custom text for the chart download field. Required to "
"display a download link.",
)
download_file = blocks.CharBlock(
required=False,
help_text="Location of a file to download",
)
notes = blocks.TextBlock(required=False, help_text="Note about the chart")

def __init__(self, **kwargs):
# Always override chart_types and colors with ours
super().__init__(
chart_types=CHART_TYPES, colors=CHART_COLORS, **kwargs
)

# Create a more user-friendly ordering of this block's child blocks.
#
# This puts our content-focused blocks in front of the
# chart-configuration blocks we inherit from wagtailcharts.
#
# child_blocks is an OrderedDict that comes from Wagtail's
# StructBlock. This just calls OrderedDict.move_to_end() in the
# order we want the blocks to appear.
self.child_blocks.move_to_end("chart_type")
self.child_blocks.move_to_end("datasets")
self.child_blocks.move_to_end("settings")

# We also want the eyebrow to appear above the title field.
self.child_blocks.move_to_end("eyebrow", last=False)

class Meta:
label = "Chart"
icon = "image"
template = "v1/includes/organisms/wagtail-chart.html"

# Load wagtailcharts scripts when block is included on a page instead of
# by rendering a {% render_charts %} template tag.
# https://github.com/overcastsoftware/wagtailcharts/blob/v0.5/wagtailcharts/templates/wagtailcharts/tags/render_charts.html
class Media:
js = [
"wagtailcharts/js/accounting.js?staticroot",
"wagtailcharts/js/chart-types.js?staticroot",
"wagtailcharts/js/chart.js?staticroot",
"wagtailcharts/js/stacked-100.js?staticroot",
"wagtailcharts/js/chartjs-plugin-datalabels.min.js?staticroot",
"wagtail-charts-chart-block.js",
"wagtailcharts/js/wagtailcharts.js?staticroot",
]
css = ["wagtail-chart.css"]
65 changes: 65 additions & 0 deletions cfgov/v1/jinja2/v1/includes/organisms/wagtail-chart.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{# ==========================================================================

wagtail-chart

==========================================================================

Description:

Create a chart block based on wagtailcharts

========================================================================== #}

{# TODO What should this o-class be? #}
<div class="o-wagtail-chart">

{% if value.eyebrow %}
<div class="eyebrow">{{ value.eyebrow }}</div>
{% endif %}

{% if value.title %}
{% include_block value.title %}
{% endif %}

{% if value.intro %}
{% include_block value.intro %}
{% endif %}

{% if value.subtitle %}
<p class="o-wagtail-chart__subtitle">{{ value.subtitle }}</p>
{% endif %}

{# Copied from wagtailcharts/templates/wagtailcharts/blocks/chart_block.html #}
<canvas
id="chart-{{block.id}}"
data-datasets="{{value.datasets}}"
data-config="{{value.settings.config}}"
data-chart-type="{{value.chart_type}}"
{% if self.callbacks %}data-callback="{{value.callbacks}}"{% endif %}
{% if value.description %}aria-label="{{value.description}}"{% endif %}>
>
<p>
Your browser does not support the canvas element.
This canvas shows a chart {% if value.title %} with the title {{value.title}} {%endif%}
</p>
</canvas>

<p class="o-wagtail-chart__footnote block--sub block--border-top block">
{% if value.data_source %}
<strong>Source:</strong> {{value.data_source}}<br>
{% endif %}
{% if value.date_published %}
<strong>Date published:</strong> {{value.date_published}}<br>
{% endif %}

{% if value.download_text and value.download_file %}
<strong>Download:</strong>
<a href="{{value.download_file}}">{{value.download_text}}</a><br>
{% endif %}

{% if value.notes %}
<strong>Notes:</strong> {{value.notes}}
{% endif %}
</p>
</div>

2 changes: 2 additions & 0 deletions cfgov/v1/jinja2/v1/layouts/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@
{% for js in page.media_js %}
{% if 'https://' in js %}
s.push('{{ js }}');
{% elif js.endswith('?staticroot') %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a placeholder approach and there's definitely a better way to do this. After #7303 we assume either that a path is its own URL if it contains https:// or lives in js/routes/on-demand/ if not. The wagtailcharts block type needs to load its own JS file that is being served out of the static root, so neither of these work. Per @anselmbradford's comment on #7303 (comment), maybe this is sufficient motivation to adopt paths as objects?

s.push('{{ static(js[:-11]) }}');
{% else %}
s.push( '{{ static('js/routes/on-demand/' + js) }}' );
{% endif %}
Expand Down
35 changes: 35 additions & 0 deletions cfgov/v1/migrations/0038_wagtail_charts_chart_block.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cfgov/v1/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def page_js(self):
# Returns the JS files required by this page and its StreamField blocks.
@property
def media_js(self):
return sorted(set(self.page_js + self.streamfield_media("js")))
return list(dict.fromkeys(self.page_js + self.streamfield_media("js")))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change allows us to preserve the order of JS script loads for a block, for example given a block like

class MyBlockWithJS(blocks.StructBlock):
    class Media:
        js = ["b.js", "a.js"]

Our template will currently sort these and generate tags like

<script src="/static/js/routes/on-demand/a.js"></script>
<script src="/static/js/routes/on-demand/b.js"></script>

This causes problems if something in a.js relies on something in b.js being loaded first, as is the case with wagtailcharts.

I suppose in theory this change might break some alphabetized dependency we didn't know we needed, but I think it's better to be explicit with ordering.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only place I thought this might cause an issue is with our video player loading youtube's iframe API but the loading order doesn't matter because all the youtube script does is load a second script that explicitly has an async attribute.

So this change looks a-okay. 👍


# Returns the CSS files required by this page and its StreamField blocks.
@property
Expand Down
3 changes: 2 additions & 1 deletion cfgov/v1/models/blog_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from wagtail.fields import StreamField

from v1 import blocks as v1_blocks
from v1.atomic_elements import organisms, schema
from v1.atomic_elements import charts, organisms, schema
from v1.feeds import get_appropriate_rss_feed_url_for_page
from v1.models.learn_page import AbstractFilterPage

Expand All @@ -20,6 +20,7 @@ class BlogContent(blocks.StreamBlock):
simple_chart = organisms.SimpleChart()
faq_schema = schema.FAQ(label="FAQ schema")
how_to_schema = schema.HowTo(label="HowTo schema", max_num=1)
wagtailchart_block = charts.ChartBlock()


class BlogPage(AbstractFilterPage):
Expand Down
Loading
Loading