-
Notifications
You must be signed in to change notification settings - Fork 114
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
base: main
Are you sure you want to change the base?
Changes from all commits
dc918d1
f9da52b
7510895
c20564e
34d6a44
a81155c
f1a5561
a5fa89f
0fd4ea8
d35a7fe
89bc998
b8e7898
7c52273
5c8dc88
b52b99d
1404498
a31b787
a1e1663
5470dc1
1f70393
0b7ca2a
dc6feec
ebbc7a4
3838ccb
cf564c9
1044cf1
f0b2558
522f943
8309830
4dc98c2
c3c28c1
ab3626e
3e91c66
612c136
7773370
c141340
ce9f110
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} | ||
} |
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 }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
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"), | ||
) | ||
|
||
|
||
CHART_COLORS = ( | ||
("#20aa3f", "CFPB Green"), | ||
("#254b87", "Navy"), | ||
("#7eb7e8", "Pacific 60"), | ||
("#ffb858", "Gold 80"), | ||
("#c55998", "Purple 80"), | ||
("#addc91", "Green 60"), | ||
("#1fa040", "Mid Dark Green"), | ||
("#257675", "Teal"), | ||
("#89b6b5", "Teal 60"), | ||
("#d14124", "Red"), | ||
("#e79e8e", "Red 60"), | ||
("#0072ce", "Pacific"), | ||
("#254b87", "Navy"), | ||
("#dc731c", "Dark Gold"), | ||
("#745745", "Dark Neutral"), | ||
("#baa496", "Neutral 60"), | ||
("#dc9cbf", "Purple 50"), | ||
("#a01b68", "Dark Purple"), | ||
("#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"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
{# ========================================================================== | ||
|
||
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 %}> | ||
</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> | ||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 So this change looks a-okay. 👍 |
||
|
||
# Returns the CSS files required by this page and its StreamField blocks. | ||
@property | ||
|
There was a problem hiding this comment.
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 injs/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?