-
Notifications
You must be signed in to change notification settings - Fork 0
/
chart-base.js
168 lines (142 loc) · 5.01 KB
/
chart-base.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import * as d3 from 'd3';
import throttle from 'underscore/modules/throttle.js';
/* * * * *
* CHART BASE
*
* The most basic chart template class. This adds an SVG to the given container
* and sets it up with a chart group, config options and responsive sizing. This
* is meant to be extended into all sorts of wacky, beautiful charts.
* * * * */
export default class ChartBase {
// Constructor: Sets the most basic class properties and fills in config defaults.
// Listens for resize.
constructor(config) {
this.checkConfigKeys(config);
this.containerEl = d3.select(`#${config.containerId}`);
this.containerNode = this.containerEl.node();
if (!this.containerNode) {
console.warn(
`Skipped creating chart: container #${config.containerId} not found on page`
);
return;
}
this.setConfigDefaults(config);
this.data = this.config.data;
if (this.config.responsive) {
window.addEventListener(
'resize',
throttle(() => {
this.redrawChart();
}, 100)
);
}
this.initChart();
}
// Check if any required keys are missing from the config.
checkConfigKeys(config) {
this.ensureRequired(config, ['containerId']);
}
// Return error message for each required key missing from config.
ensureRequired(object, requiredKeys) {
const errors = [];
requiredKeys.forEach((key) => {
if (typeof object[key] === 'undefined') {
errors.push(`Required key ${key} missing from config options`);
}
});
if (errors.length > 0) {
console.error(
`Error calling ${this.constructor.name}:\n${errors.join('\n')}`
);
}
}
// Fill in default values for undefined config options. This preserves any
// already-defined config options, which means you can pass literally any
// sort of data through to your graphic.
setConfigDefaults(config) {
const classDefaults = {
responsive: true,
aspectRatio: 4 / 3,
marginTop: 10,
marginRight: 10,
marginBottom: 10,
marginLeft: 10,
};
this.config = Object.assign({}, classDefaults, config);
}
// Initialize the graphic and size it. We call this separately from the
// constructor because this will differ from template to template.
initChart() {
this.initBaseChart();
this.sizeBaseSVG();
}
// Add the SVG and a chart container to the page
initBaseChart() {
this.containerEl.classed('g-tmp-chart', true);
this.svg = this.containerEl.append('svg');
this.chart = this.svg.append('g').attr('class', 'chart-g');
}
// Charts default to filling their container width
getSVGWidth() {
this.svg.attr('width', '100%');
return this.svg.node().getBoundingClientRect().width;
}
// Charts default to basing their height as a proportion of the chart width.
getSVGHeight() {
const svgWidth = this.getSVGWidth();
// However, this proportion may need to be expressed through a function
// rather than a set value.
const aspectDivisor = this.evaluateOption('aspectRatio');
return svgWidth / aspectDivisor;
}
// Return a chartWidth/chartHeight that is useful in scale calculations, so
// we don't have to worry about calculating around margins.
getBaseMeasurements() {
const svgWidth = this.getSVGWidth();
const svgHeight = this.getSVGHeight();
const marginTop = this.evaluateOption('marginTop');
const marginRight = this.evaluateOption('marginRight');
const marginBottom = this.evaluateOption('marginBottom');
const marginLeft = this.evaluateOption('marginLeft');
return {
chartWidth: svgWidth - marginLeft - marginRight,
chartHeight: svgHeight - marginTop - marginBottom,
marginTop: marginTop,
marginRight: marginRight,
marginBottom: marginBottom,
marginLeft: marginLeft,
};
}
// Set the size of the SVG and offset the chart group by the top and left margins.
sizeBaseSVG() {
this.size = this.getBaseMeasurements();
// SVG width is already set to 100%. The height measurement should include margins.
this.svg.attr(
'height',
this.size.chartHeight + this.size.marginTop + this.size.marginBottom
);
// Offset the chart group by top and left margins.
this.chart.attr(
'transform',
`translate(${this.size.marginLeft}, ${this.size.marginTop})`
);
}
// Redraw the graphic, re-calculating the size and positions. This is called
// on `window.resize` in the constructor.
redrawChart() {
this.sizeBaseSVG();
}
// Some config options might be a fixed value, some might be a function.
// Here's a wrapper to get the value given the config option name only.
// If the option is a function, we pass the width of the SVG as one of its
// arguments.
evaluateOption(optionName, args) {
const configOption = this.config[optionName];
let configValue = configOption;
if (typeof configOption === 'function') {
const svgWidth = this.getSVGWidth();
configValue = configOption.call(this, svgWidth, args);
}
return configValue;
}
}