@cdc/waffle-chart 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/cdcwafflechart.js +6 -0
- package/examples/example-config.json +10 -0
- package/examples/example-data-2.json +32 -0
- package/examples/example-data.json +90 -0
- package/package.json +43 -0
- package/src/CdcWaffleChart.tsx +502 -0
- package/src/components/EditorPanel.js +510 -0
- package/src/context.js +5 -0
- package/src/data/initial-state.js +27 -0
- package/src/index.html +11 -0
- package/src/index.js +16 -0
- package/src/scss/editor-panel.scss +710 -0
- package/src/scss/main.scss +52 -0
- package/src/scss/responsive.scss +1 -0
- package/src/scss/variables.scss +29 -0
- package/src/scss/waffle-chart.scss +127 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import React, { useState, useEffect, memo, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Accordion,
|
|
5
|
+
AccordionItem,
|
|
6
|
+
AccordionItemHeading,
|
|
7
|
+
AccordionItemPanel,
|
|
8
|
+
AccordionItemButton,
|
|
9
|
+
} from 'react-accessible-accordion'
|
|
10
|
+
|
|
11
|
+
import { useDebounce } from 'use-debounce'
|
|
12
|
+
import Context from '../context'
|
|
13
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
14
|
+
import { DATA_OPERATORS, DATA_FUNCTIONS } from '../CdcWaffleChart'
|
|
15
|
+
|
|
16
|
+
const TextField = memo((
|
|
17
|
+
{
|
|
18
|
+
label,
|
|
19
|
+
section = null,
|
|
20
|
+
subsection = null,
|
|
21
|
+
fieldName,
|
|
22
|
+
updateField,
|
|
23
|
+
value: stateValue,
|
|
24
|
+
type = 'input',
|
|
25
|
+
i = null, min = null, max = null,
|
|
26
|
+
...attributes
|
|
27
|
+
}
|
|
28
|
+
) => {
|
|
29
|
+
|
|
30
|
+
const [ value, setValue ] = useState(stateValue)
|
|
31
|
+
const [ debouncedValue ] = useDebounce(value, 500)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
|
|
35
|
+
updateField(section, subsection, fieldName, debouncedValue, i)
|
|
36
|
+
}
|
|
37
|
+
}, [ debouncedValue, section, subsection, fieldName, i, stateValue, updateField ])
|
|
38
|
+
|
|
39
|
+
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
40
|
+
|
|
41
|
+
const onChange = (e) => {
|
|
42
|
+
if ('number' !== type || min === null) {
|
|
43
|
+
setValue(e.target.value)
|
|
44
|
+
} else {
|
|
45
|
+
if (!e.target.value || (parseFloat(min) <= parseFloat(e.target.value) & parseFloat(max) >= parseFloat(e.target.value))) {
|
|
46
|
+
setValue(e.target.value)
|
|
47
|
+
} else {
|
|
48
|
+
setValue(min.toString())
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let formElement = <input type="text" name={name} onChange={onChange} {...attributes} value={value}/>
|
|
54
|
+
|
|
55
|
+
if ('textarea' === type) {
|
|
56
|
+
formElement = (
|
|
57
|
+
<textarea name={name} onChange={onChange} {...attributes} value={value}/>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ('number' === type) {
|
|
62
|
+
formElement = <input type="number" name={name} onChange={onChange} {...attributes} value={value}/>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<label>
|
|
67
|
+
{label && <span className="edit-label column-heading">{label}</span>}
|
|
68
|
+
{formElement}
|
|
69
|
+
</label>
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const Select = memo((
|
|
74
|
+
{
|
|
75
|
+
label,
|
|
76
|
+
value,
|
|
77
|
+
options,
|
|
78
|
+
fieldName,
|
|
79
|
+
section = null,
|
|
80
|
+
subsection = null,
|
|
81
|
+
required = false,
|
|
82
|
+
updateField,
|
|
83
|
+
initial: initialValue,
|
|
84
|
+
...attributes
|
|
85
|
+
}
|
|
86
|
+
) => {
|
|
87
|
+
|
|
88
|
+
let optionsJsx = ''
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(options)) { //Handle basic array
|
|
91
|
+
optionsJsx = options.map(optionName => <option value={optionName} key={optionName}>{optionName}</option>)
|
|
92
|
+
} else { //Handle object with value/name pairs
|
|
93
|
+
optionsJsx = []
|
|
94
|
+
for (const [ optionValue, optionName ] of Object.entries(options)) {
|
|
95
|
+
optionsJsx.push(<option value={optionValue} key={optionValue}>{optionName}</option>)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (initialValue) {
|
|
100
|
+
optionsJsx.unshift(<option value="" key="initial">{initialValue}</option>)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<label>
|
|
105
|
+
{label && <span className="edit-label">{label}</span>}
|
|
106
|
+
<select className={required && !value ? 'warning' : ''} name={fieldName} value={value} onChange={(event) => {
|
|
107
|
+
updateField(section, subsection, fieldName, event.target.value)
|
|
108
|
+
}} {...attributes}>
|
|
109
|
+
{optionsJsx}
|
|
110
|
+
</select>
|
|
111
|
+
</label>
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const headerColors = [ 'theme-blue', 'theme-purple', 'theme-brown', 'theme-teal', 'theme-pink', 'theme-orange', 'theme-slate', 'theme-indigo', 'theme-cyan', 'theme-green', 'theme-amber' ]
|
|
116
|
+
|
|
117
|
+
const EditorPanel = memo(() => {
|
|
118
|
+
const {
|
|
119
|
+
config,
|
|
120
|
+
updateConfig,
|
|
121
|
+
loading,
|
|
122
|
+
data,
|
|
123
|
+
setParentConfig,
|
|
124
|
+
isDashboard
|
|
125
|
+
} = useContext(Context)
|
|
126
|
+
|
|
127
|
+
const [ displayPanel, setDisplayPanel ] = useState(true)
|
|
128
|
+
|
|
129
|
+
const enforceRestrictions = (updatedConfig) => {
|
|
130
|
+
//If there are any dependencies between fields, etc../
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const updateField = (section, subsection, fieldName, newValue) => {
|
|
134
|
+
// Top level
|
|
135
|
+
if (null === section && null === subsection) {
|
|
136
|
+
let updatedConfig = { ...config, [fieldName]: newValue }
|
|
137
|
+
|
|
138
|
+
if ('filterColumn' === fieldName) {
|
|
139
|
+
updatedConfig.filterValue = ''
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
enforceRestrictions(updatedConfig)
|
|
143
|
+
|
|
144
|
+
updateConfig(updatedConfig)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isArray = Array.isArray(config[section])
|
|
149
|
+
|
|
150
|
+
let sectionValue = isArray ? [ ...config[section], newValue ] : { ...config[section], [fieldName]: newValue }
|
|
151
|
+
|
|
152
|
+
if (null !== subsection) {
|
|
153
|
+
if (isArray) {
|
|
154
|
+
sectionValue = [ ...config[section] ]
|
|
155
|
+
sectionValue[subsection] = { ...sectionValue[subsection], [fieldName]: newValue }
|
|
156
|
+
} else if (typeof newValue === 'string') {
|
|
157
|
+
sectionValue[subsection] = newValue
|
|
158
|
+
} else {
|
|
159
|
+
sectionValue = { ...config[section], [subsection]: { ...config[section][subsection], [fieldName]: newValue } }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let updatedConfig = { ...config, [section]: sectionValue }
|
|
164
|
+
|
|
165
|
+
enforceRestrictions(updatedConfig)
|
|
166
|
+
|
|
167
|
+
updateConfig(updatedConfig)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const missingRequiredSections = () => {
|
|
171
|
+
//Whether to show error message if something is required to show a data-bite and isn't filled in
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
// Pass up to Editor if needed
|
|
177
|
+
if (setParentConfig) {
|
|
178
|
+
const newConfig = convertStateToConfig()
|
|
179
|
+
|
|
180
|
+
setParentConfig(newConfig)
|
|
181
|
+
}
|
|
182
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
|
+
}, [ config ])
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
//Verify comparate data type
|
|
187
|
+
let operators = [ '<', '>', '<=', '>=' ]
|
|
188
|
+
if (config.dataConditionalComparate !== '') {
|
|
189
|
+
if (operators.indexOf(config.dataConditionalOperator) > -1 && isNaN(config.dataConditionalComparate)) {
|
|
190
|
+
updateConfig({ ...config, invalidComparate: true })
|
|
191
|
+
} else {
|
|
192
|
+
if (config.invalidComparate) {
|
|
193
|
+
updateConfig({ ...config, invalidComparate: false })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
updateConfig({ ...config, invalidComparate: false })
|
|
198
|
+
}
|
|
199
|
+
}, [ config.dataConditionalOperator, config.dataConditionalComparate ])
|
|
200
|
+
|
|
201
|
+
const onBackClick = () => {
|
|
202
|
+
if (isDashboard) {
|
|
203
|
+
updateConfig({ ...config, editing: false })
|
|
204
|
+
} else {
|
|
205
|
+
setDisplayPanel(!displayPanel)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const Error = () => {
|
|
210
|
+
return (
|
|
211
|
+
<section className="waiting">
|
|
212
|
+
<section className="waiting-container">
|
|
213
|
+
<h3>Error With Configuration</h3>
|
|
214
|
+
<p>{config.runtime.editorErrorMessage}</p>
|
|
215
|
+
</section>
|
|
216
|
+
</section>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const Confirm = () => {
|
|
221
|
+
|
|
222
|
+
const confirmDone = (e) => {
|
|
223
|
+
e.preventDefault()
|
|
224
|
+
|
|
225
|
+
let newConfig = {...config}
|
|
226
|
+
delete newConfig.newViz
|
|
227
|
+
|
|
228
|
+
updateConfig(newConfig)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<section className="waiting">
|
|
233
|
+
<section className="waiting-container">
|
|
234
|
+
<h3>Finish Configuring</h3>
|
|
235
|
+
<p>Set all required options to the left and confirm below to display a preview of the chart.</p>
|
|
236
|
+
<button className="btn" style={{ margin: '1em auto' }} disabled={missingRequiredSections()} onClick={confirmDone}>I'm Done</button>
|
|
237
|
+
</section>
|
|
238
|
+
</section>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const convertStateToConfig = () => {
|
|
243
|
+
let strippedState = JSON.parse(JSON.stringify(config))
|
|
244
|
+
if (false === missingRequiredSections()) {
|
|
245
|
+
delete strippedState.newViz
|
|
246
|
+
}
|
|
247
|
+
delete strippedState.runtime
|
|
248
|
+
|
|
249
|
+
return strippedState
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const removeFilter = (index) => {
|
|
253
|
+
let filters = [ ...config.filters ]
|
|
254
|
+
|
|
255
|
+
filters.splice(index, 1)
|
|
256
|
+
|
|
257
|
+
updateConfig({ ...config, filters })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const updateFilterProp = (name, index, value) => {
|
|
261
|
+
let filters = [ ...config.filters ]
|
|
262
|
+
|
|
263
|
+
filters[index][name] = value
|
|
264
|
+
|
|
265
|
+
updateConfig({ ...config, filters })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const addNewFilter = () => {
|
|
269
|
+
let filters = config.filters ? [ ...config.filters ] : []
|
|
270
|
+
|
|
271
|
+
filters.push({ values: [] })
|
|
272
|
+
|
|
273
|
+
updateConfig({ ...config, filters })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const getColumns = (filter = true) => {
|
|
277
|
+
let columns = {}
|
|
278
|
+
|
|
279
|
+
data.map(row => Object.keys(row).forEach(columnName => columns[columnName] = true))
|
|
280
|
+
|
|
281
|
+
return Object.keys(columns)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const getFilterColumnValues = (index) => {
|
|
285
|
+
let filterDataOptions = []
|
|
286
|
+
const filterColumnName = config.filters[index].columnName
|
|
287
|
+
if (data && filterColumnName) {
|
|
288
|
+
data.forEach(function (row) {
|
|
289
|
+
if (undefined !== row[filterColumnName] && -1 === filterDataOptions.indexOf(row[filterColumnName])) {
|
|
290
|
+
filterDataOptions.push(row[filterColumnName])
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
filterDataOptions.sort()
|
|
294
|
+
}
|
|
295
|
+
return filterDataOptions
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const toggleCustomDenom = () => {
|
|
299
|
+
let denom = { ...config }
|
|
300
|
+
updateConfig({ ...config, customDenom: !denom.customDenom })
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (loading) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<ErrorBoundary component="EditorPanel">
|
|
309
|
+
{!config.newViz && config.runtime && config.runtime.editorErrorMessage && <Error/>}
|
|
310
|
+
{config.newViz && <Confirm/>}
|
|
311
|
+
<button className={displayPanel ? `editor-toggle` : `editor-toggle collapsed`}
|
|
312
|
+
title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={onBackClick}/>
|
|
313
|
+
<section className={displayPanel ? 'editor-panel' : 'hidden editor-panel'}>
|
|
314
|
+
<h2>Configure Waffle Chart</h2>
|
|
315
|
+
<section className="form-container">
|
|
316
|
+
<form>
|
|
317
|
+
<Accordion allowZeroExpanded={true}>
|
|
318
|
+
<AccordionItem> {/* General */}
|
|
319
|
+
<AccordionItemHeading>
|
|
320
|
+
<AccordionItemButton>
|
|
321
|
+
General
|
|
322
|
+
</AccordionItemButton>
|
|
323
|
+
</AccordionItemHeading>
|
|
324
|
+
<AccordionItemPanel>
|
|
325
|
+
<TextField value={config.title} fieldName="title" label="Title" placeholder="Waffle Chart Title"
|
|
326
|
+
updateField={updateField}/>
|
|
327
|
+
<TextField type="textarea" value={config.content} fieldName="content" label="Message"
|
|
328
|
+
updateField={updateField}/>
|
|
329
|
+
<TextField value={config.subtext} fieldName="subtext" label="Subtext/Citation"
|
|
330
|
+
placeholder="Waffle Chart Subtext or Citation" updateField={updateField}/>
|
|
331
|
+
</AccordionItemPanel>
|
|
332
|
+
</AccordionItem>
|
|
333
|
+
<AccordionItem>
|
|
334
|
+
<AccordionItemHeading>
|
|
335
|
+
<AccordionItemButton>
|
|
336
|
+
Data
|
|
337
|
+
</AccordionItemButton>
|
|
338
|
+
</AccordionItemHeading>
|
|
339
|
+
<AccordionItemPanel>
|
|
340
|
+
<h4 style={{ fontWeight: '600' }}>Numerator</h4>
|
|
341
|
+
<div className="accordion__panel-section">
|
|
342
|
+
<Select value={config.dataColumn || ''} fieldName="dataColumn" label="Data Column"
|
|
343
|
+
updateField={updateField} initial="Select" options={getColumns()}/>
|
|
344
|
+
<Select value={config.dataFunction || ''} fieldName="dataFunction" label="Data Function"
|
|
345
|
+
updateField={updateField} initial="Select" options={DATA_FUNCTIONS}/>
|
|
346
|
+
<label><span className="edit-label">Data Conditional</span></label>
|
|
347
|
+
<div className="accordion__panel-row accordion__small-inputs">
|
|
348
|
+
<div className="accordion__panel-col">
|
|
349
|
+
<Select value={config.dataConditionalColumn || ''} fieldName="dataConditionalColumn"
|
|
350
|
+
updateField={updateField} initial="Select" options={getColumns()}/>
|
|
351
|
+
</div>
|
|
352
|
+
<div className="accordion__panel-col">
|
|
353
|
+
<Select value={config.dataConditionalOperator || ''} fieldName="dataConditionalOperator"
|
|
354
|
+
updateField={updateField} initial="Select" options={DATA_OPERATORS}/>
|
|
355
|
+
</div>
|
|
356
|
+
<div className="accordion__panel-col">
|
|
357
|
+
<TextField value={config.dataConditionalComparate} fieldName={'dataConditionalComparate'}
|
|
358
|
+
updateField={updateField}
|
|
359
|
+
className={config.invalidComparate ? 'accordion__input-error' : ''}/>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
{config.invalidComparate &&
|
|
363
|
+
<div className="accordion__panel-error">Non-numerical comparate values can only be used with = or
|
|
364
|
+
≠.</div>
|
|
365
|
+
}
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div className="accordion__panel-row align-center">
|
|
369
|
+
<div className="accordion__panel-col">
|
|
370
|
+
<h4 style={{ fontWeight: '600' }}>Denominator</h4>
|
|
371
|
+
</div>
|
|
372
|
+
<div className="accordion__panel-col">
|
|
373
|
+
<div className="d-flex justify-end">
|
|
374
|
+
<label className={'accordion__panel-label--inline'}>Select from data</label>
|
|
375
|
+
<div className={`accordion__panel-checkbox${config.customDenom ? ' checked' : ''}`}
|
|
376
|
+
onClick={() => toggleCustomDenom()}/>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
<div className="accordion__panel-section">
|
|
381
|
+
{!config.customDenom &&
|
|
382
|
+
<div className="accordion__panel-row align-center">
|
|
383
|
+
<div className="accordion__panel-col">
|
|
384
|
+
<TextField value={config.dataDenom} fieldName="dataDenom" updateField={updateField}/>
|
|
385
|
+
</div>
|
|
386
|
+
<div className="accordion__panel-col">
|
|
387
|
+
<label className={'accordion__panel-label--muted'}>default (100)</label>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
}
|
|
391
|
+
{config.customDenom &&
|
|
392
|
+
<>
|
|
393
|
+
<Select value={config.dataDenomColumn || ''} fieldName="dataDenomColumn" label="Data Column"
|
|
394
|
+
updateField={updateField} initial="Select" options={getColumns()}/>
|
|
395
|
+
<Select value={config.dataDenomFunction || ''} fieldName="dataDenomFunction" label="Data Function"
|
|
396
|
+
updateField={updateField} initial="Select" options={DATA_FUNCTIONS}/>
|
|
397
|
+
</>
|
|
398
|
+
}
|
|
399
|
+
</div>
|
|
400
|
+
<ul className="column-edit">
|
|
401
|
+
<li className="three-col">
|
|
402
|
+
<TextField value={config.prefix} fieldName="prefix" label="Prefix" updateField={updateField}/>
|
|
403
|
+
<TextField value={config.suffix} fieldName="suffix" label="Suffix" updateField={updateField}/>
|
|
404
|
+
<TextField type="number" value={config.roundToPlace} fieldName="roundToPlace" label="Round"
|
|
405
|
+
updateField={updateField}/>
|
|
406
|
+
</li>
|
|
407
|
+
</ul>
|
|
408
|
+
</AccordionItemPanel>
|
|
409
|
+
</AccordionItem>
|
|
410
|
+
<AccordionItem>
|
|
411
|
+
<AccordionItemHeading>
|
|
412
|
+
<AccordionItemButton>
|
|
413
|
+
Filters
|
|
414
|
+
</AccordionItemButton>
|
|
415
|
+
</AccordionItemHeading>
|
|
416
|
+
<AccordionItemPanel>
|
|
417
|
+
<ul className="filters-list">
|
|
418
|
+
{config.filters && config.filters.map((filter, index) => (
|
|
419
|
+
<fieldset className="edit-block" key={index}>
|
|
420
|
+
<button type="button" className="remove-column" onClick={() => {
|
|
421
|
+
removeFilter(index)
|
|
422
|
+
}}>Remove
|
|
423
|
+
</button>
|
|
424
|
+
<label>
|
|
425
|
+
<span className="edit-label column-heading">Column</span>
|
|
426
|
+
<select value={filter.columnName} onChange={(e) => {
|
|
427
|
+
updateFilterProp('columnName', index, e.target.value)
|
|
428
|
+
}}>
|
|
429
|
+
<option value="">- Select Option -</option>
|
|
430
|
+
{getColumns().map((dataKey, index) => (
|
|
431
|
+
<option value={dataKey} key={index}>{dataKey}</option>
|
|
432
|
+
))}
|
|
433
|
+
</select>
|
|
434
|
+
</label>
|
|
435
|
+
<label>
|
|
436
|
+
<span className="edit-label column-heading">Column Value</span>
|
|
437
|
+
<select value={filter.columnValue} onChange={(e) => {
|
|
438
|
+
updateFilterProp('columnValue', index, e.target.value)
|
|
439
|
+
}}>
|
|
440
|
+
<option value="">- Select Option -</option>
|
|
441
|
+
{getFilterColumnValues(index).map((dataKey, index) => (
|
|
442
|
+
<option value={dataKey} key={index}>{dataKey}</option>
|
|
443
|
+
))}
|
|
444
|
+
</select>
|
|
445
|
+
</label>
|
|
446
|
+
</fieldset>
|
|
447
|
+
)
|
|
448
|
+
)}
|
|
449
|
+
</ul>
|
|
450
|
+
|
|
451
|
+
<button type="button" onClick={addNewFilter} className="btn btn-primary">Add Filter</button>
|
|
452
|
+
</AccordionItemPanel>
|
|
453
|
+
</AccordionItem>
|
|
454
|
+
<AccordionItem>
|
|
455
|
+
<AccordionItemHeading>
|
|
456
|
+
<AccordionItemButton>
|
|
457
|
+
Visual
|
|
458
|
+
</AccordionItemButton>
|
|
459
|
+
</AccordionItemHeading>
|
|
460
|
+
<AccordionItemPanel>
|
|
461
|
+
<Select value={config.shape} fieldName="shape" label="Shape"
|
|
462
|
+
updateField={updateField} options={[ 'circle', 'square', 'person' ]}/>
|
|
463
|
+
|
|
464
|
+
<div className="accordion__panel-row accordion__small-inputs" style={{marginTop: '1em'}}>
|
|
465
|
+
<div className="accordion__panel-col">
|
|
466
|
+
<TextField type="number" value={config.nodeWidth} fieldName="nodeWidth" label="Width" updateField={updateField}/>
|
|
467
|
+
</div>
|
|
468
|
+
<div className="accordion__panel-col">
|
|
469
|
+
<TextField type="number" value={config.nodeSpacer} fieldName="nodeSpacer" label="Spacer" updateField={updateField}/>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<Select value={config.orientation} fieldName="orientation" label="Layout"
|
|
474
|
+
updateField={updateField} options={[ 'horizontal', 'vertical' ]}/>
|
|
475
|
+
|
|
476
|
+
<label><span className="edit-label column-heading">Data Point Font Size</span></label>
|
|
477
|
+
<div className="accordion__panel-row accordion__small-inputs align-center">
|
|
478
|
+
<div className="accordion__panel-col">
|
|
479
|
+
<TextField type="number" value={config.fontSize} fieldName="fontSize" updateField={updateField}/>
|
|
480
|
+
</div>
|
|
481
|
+
<div className="accordion__panel-col">
|
|
482
|
+
<label className={'accordion__panel-label--muted'}>default (50px)</label>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<Select value={config.overallFontSize} fieldName="overallFontSize" label="Overall Font Size"
|
|
487
|
+
updateField={updateField} options={[ 'small', 'medium', 'large' ]}/>
|
|
488
|
+
|
|
489
|
+
<label className="header">
|
|
490
|
+
<span className="edit-label">Theme</span>
|
|
491
|
+
<ul className="color-palette">
|
|
492
|
+
{headerColors.map((palette) => (
|
|
493
|
+
<li title={palette} key={palette} onClick={() => {
|
|
494
|
+
updateConfig({ ...config, theme: palette })
|
|
495
|
+
}} className={config.theme === palette ? 'selected ' + palette : palette}>
|
|
496
|
+
</li>
|
|
497
|
+
))}
|
|
498
|
+
</ul>
|
|
499
|
+
</label>
|
|
500
|
+
</AccordionItemPanel>
|
|
501
|
+
</AccordionItem>
|
|
502
|
+
</Accordion>
|
|
503
|
+
</form>
|
|
504
|
+
</section>
|
|
505
|
+
</section>
|
|
506
|
+
</ErrorBoundary>
|
|
507
|
+
)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
export default EditorPanel
|
package/src/context.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
"title": "Waffle Chart",
|
|
3
|
+
"content": "",
|
|
4
|
+
"subtext": "",
|
|
5
|
+
"orientation": "horizontal",
|
|
6
|
+
"data": "",
|
|
7
|
+
"filters": [],
|
|
8
|
+
"fontSize": "",
|
|
9
|
+
"overallFontSize": "medium",
|
|
10
|
+
"dataColumn": "",
|
|
11
|
+
"dataFunction": "",
|
|
12
|
+
"dataConditionalColumn": "",
|
|
13
|
+
"dataConditionalOperator": "",
|
|
14
|
+
"dataConditionalComparate": "",
|
|
15
|
+
"invalidComparate": false,
|
|
16
|
+
"customDenom": false,
|
|
17
|
+
"dataDenom": "100",
|
|
18
|
+
"dataDenomColumn": "",
|
|
19
|
+
"dataDenomFunction": "",
|
|
20
|
+
"suffix": "%",
|
|
21
|
+
"roundToPlace": "0",
|
|
22
|
+
"shape": "circle",
|
|
23
|
+
"nodeWidth": "10",
|
|
24
|
+
"nodeSpacer": "2",
|
|
25
|
+
"theme": "theme-blue",
|
|
26
|
+
"type": "waffle-chart"
|
|
27
|
+
}
|
package/src/index.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<div class="react-container" data-config="/examples/example-config.json"></div>
|
|
9
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'react-dom';
|
|
3
|
+
import CdcWaffleChart from './CdcWaffleChart'
|
|
4
|
+
|
|
5
|
+
const domContainers = document.querySelectorAll('.react-container');
|
|
6
|
+
|
|
7
|
+
let isEditor = window.location.href.includes('editor=true');
|
|
8
|
+
|
|
9
|
+
domContainers.forEach((domContainer) => {
|
|
10
|
+
render(
|
|
11
|
+
<React.StrictMode>
|
|
12
|
+
<CdcWaffleChart configUrl={domContainer.attributes['data-config'].value} isEditor={isEditor} />
|
|
13
|
+
</React.StrictMode>,
|
|
14
|
+
domContainer
|
|
15
|
+
);
|
|
16
|
+
});
|