@cdc/core 4.23.1 → 4.23.2
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/{helpers/CoveMediaControls.js → components/CoveMediaControls.jsx} +18 -17
- package/components/elements/Button.jsx +21 -19
- package/components/inputs/InputCheckbox.jsx +0 -3
- package/components/managers/DataDesigner.jsx +48 -6
- package/components/ui/Icon.jsx +4 -0
- package/components/ui/Tooltip.jsx +39 -23
- package/helpers/DataTransform.js +94 -55
- package/helpers/cleanData.js +50 -0
- package/helpers/fetchRemoteData.js +0 -3
- package/helpers/isNumber.js +24 -0
- package/helpers/isNumberLog.js +18 -0
- package/package.json +16 -18
- package/styles/_data-table.scss +14 -15
- package/styles/v2/components/button.scss +1 -1
- package/styles/v2/components/ui/tooltip.scss +154 -0
- /package/components/{AdvancedEditor.js → AdvancedEditor.jsx} +0 -0
- /package/{data/dataDesignerTables.js → templates/dataDesignerTables.jsx} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import html2pdf from 'html2pdf.js'
|
|
2
|
+
// import html2pdf from 'html2pdf.js'
|
|
3
3
|
import html2canvas from 'html2canvas'
|
|
4
4
|
|
|
5
5
|
const buttonText = {
|
|
@@ -64,22 +64,23 @@ const generateMedia = (state, type, elementToCapture) => {
|
|
|
64
64
|
})
|
|
65
65
|
return
|
|
66
66
|
case 'pdf':
|
|
67
|
-
let opt = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
html2pdf()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
// let opt = {
|
|
68
|
+
// margin: 0.2,
|
|
69
|
+
// filename: filename + '.pdf',
|
|
70
|
+
// image: { type: 'png' },
|
|
71
|
+
// html2canvas: { scale: 2, logging: false },
|
|
72
|
+
// jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }
|
|
73
|
+
// }
|
|
74
|
+
|
|
75
|
+
// html2pdf()
|
|
76
|
+
// .set(opt)
|
|
77
|
+
// .from(baseSvg)
|
|
78
|
+
// .save()
|
|
79
|
+
// .then(() => {
|
|
80
|
+
// generatedImage.remove() // Remove generated png
|
|
81
|
+
// baseSvg.style.display = null // Re-display initial svg map
|
|
82
|
+
// })
|
|
83
|
+
console.warn('COVE: pdf downloads disabled')
|
|
83
84
|
break
|
|
84
85
|
default:
|
|
85
86
|
console.warn("generateMedia param 2 type must be 'image' or 'pdf'")
|
|
@@ -8,10 +8,10 @@ import '../../styles/v2/components/button.scss'
|
|
|
8
8
|
const Button = ({ style, role, hoverStyle = {}, fluid = false, loading = false, loadingText = 'Loading...', flexCenter, active = false, onClick, children, ...attributes }) => {
|
|
9
9
|
const buttonRef = useRef(null)
|
|
10
10
|
|
|
11
|
-
const [buttonState, setButtonState] = useState('out')
|
|
12
|
-
const [customStyles, setCustomStyles] = useState({ ...style })
|
|
13
|
-
const [childrenWidth, setChildrenWidth] = useState()
|
|
14
|
-
const [loadtextWidth, setLoadtextWidth] = useState()
|
|
11
|
+
const [ buttonState, setButtonState ] = useState('out')
|
|
12
|
+
const [ customStyles, setCustomStyles ] = useState({ ...style })
|
|
13
|
+
const [ childrenWidth, setChildrenWidth ] = useState()
|
|
14
|
+
const [ loadtextWidth, setLoadtextWidth ] = useState()
|
|
15
15
|
|
|
16
16
|
const attributesObj = {
|
|
17
17
|
...attributes,
|
|
@@ -23,6 +23,7 @@ const Button = ({ style, role, hoverStyle = {}, fluid = false, loading = false,
|
|
|
23
23
|
onBlur: () => setButtonState('out')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
useEffect(() => {
|
|
27
28
|
if ('loader' === role && buttonRef.current) {
|
|
28
29
|
//Create ghost object and text nodes for children
|
|
@@ -53,18 +54,19 @@ const Button = ({ style, role, hoverStyle = {}, fluid = false, loading = false,
|
|
|
53
54
|
buttonRef.current.parentNode.removeChild(ghostSpan)
|
|
54
55
|
buttonRef.current.parentNode.removeChild(ghostLoaderSpan)
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
return () => {
|
|
58
|
+
}
|
|
59
|
+
}, [ buttonRef, children, loadingText, role ])
|
|
57
60
|
|
|
58
61
|
useEffect(() => {
|
|
59
62
|
//Adjust button styles depending on cursor, focus, and active, states
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}, [buttonState, active])
|
|
63
|
+
if (buttonState === 'in') setCustomStyles(stateStyles => ({ ...stateStyles, ...hoverStyle }))
|
|
64
|
+
|
|
65
|
+
// If button state is out, check if its 'active'; we want to keep hover styles applied to 'active' buttons
|
|
66
|
+
if (buttonState === 'out')
|
|
67
|
+
// If button state is out, and not 'active', reset display styles back to default
|
|
68
|
+
if (!active) setCustomStyles({ ...style })
|
|
69
|
+
}, [ buttonState, active, style ])
|
|
68
70
|
|
|
69
71
|
return (
|
|
70
72
|
<button
|
|
@@ -80,16 +82,16 @@ const Button = ({ style, role, hoverStyle = {}, fluid = false, loading = false,
|
|
|
80
82
|
<>
|
|
81
83
|
{'loader' === role && (
|
|
82
84
|
<>
|
|
83
|
-
<span className=
|
|
84
|
-
<div className=
|
|
85
|
+
<span className="cove-button__text" style={loading ? { width: loadtextWidth + 'px' } : { width: childrenWidth + 'px' }}>
|
|
86
|
+
<div className="cove-button__text--loading" style={loading ? { opacity: 1 } : null}>
|
|
85
87
|
{loadingText}
|
|
86
88
|
</div>
|
|
87
|
-
<div className=
|
|
89
|
+
<div className="cove-button__text--children" style={loading ? { opacity: 0 } : null}>
|
|
88
90
|
{children}
|
|
89
91
|
</div>
|
|
90
92
|
</span>
|
|
91
|
-
<div className=
|
|
92
|
-
<LoadSpin className=
|
|
93
|
+
<div className="cove-button__load-spin" style={loading ? { width: '28px', opacity: 1 } : null}>
|
|
94
|
+
<LoadSpin className="ml-1" size={20}/>
|
|
93
95
|
</div>
|
|
94
96
|
</>
|
|
95
97
|
)}
|
|
@@ -102,7 +104,7 @@ const Button = ({ style, role, hoverStyle = {}, fluid = false, loading = false,
|
|
|
102
104
|
|
|
103
105
|
Button.propTypes = {
|
|
104
106
|
/** Specify special role type for button */
|
|
105
|
-
role: PropTypes.oneOf(['loader']),
|
|
107
|
+
role: PropTypes.oneOf([ 'loader' ]),
|
|
106
108
|
/** Provide object with styles that overwrite base styles when hovered */
|
|
107
109
|
hoverStyle: PropTypes.object,
|
|
108
110
|
/** Enables button to stretch to the full width of the content */
|
|
@@ -24,9 +24,6 @@ const InputCheckbox = memo(
|
|
|
24
24
|
const [value, setValue] = useState(stateValue)
|
|
25
25
|
|
|
26
26
|
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
27
|
-
if (fieldName === 'border') {
|
|
28
|
-
console.table({ fieldName, value, stateValue })
|
|
29
|
-
}
|
|
30
27
|
|
|
31
28
|
useEffect(() => {
|
|
32
29
|
if (stateValue !== undefined && stateValue !== value) {
|
|
@@ -3,12 +3,13 @@ import React from 'react'
|
|
|
3
3
|
import Button from '../elements/Button'
|
|
4
4
|
import Card from '../elements/Card'
|
|
5
5
|
|
|
6
|
-
import { DATA_TABLE_VERTICAL, DATA_TABLE_HORIZONTAL, DATA_TABLE_SINGLE_ROW, DATA_TABLE_MULTI_ROW } from '../../
|
|
6
|
+
import { DATA_TABLE_VERTICAL, DATA_TABLE_HORIZONTAL, DATA_TABLE_SINGLE_ROW, DATA_TABLE_MULTI_ROW } from '../../templates/dataDesignerTables'
|
|
7
7
|
import '../../styles/v2/components/data-designer.scss'
|
|
8
8
|
|
|
9
9
|
const DataDesigner = props => {
|
|
10
10
|
const { configureData, updateDescriptionProp, visualizationKey, dataKey } = props
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
return (
|
|
13
14
|
<div className='cove-data-designer__container'>
|
|
14
15
|
<div className='mb-2'>
|
|
@@ -165,16 +166,57 @@ const DataDesigner = props => {
|
|
|
165
166
|
</select>
|
|
166
167
|
</div>
|
|
167
168
|
<div className='mb-2'>
|
|
168
|
-
<div className='mb-1'>Which
|
|
169
|
+
<div className='mb-1'>Which properties in the dataset represent the numeric value? (all remaining properties will be treated as filters)</div>
|
|
170
|
+
{configureData.dataDescription.valueKeys && configureData.dataDescription.valueKeys.length > 0 && (
|
|
171
|
+
<ul className="value-list">
|
|
172
|
+
{configureData.dataDescription.valueKeys.map((valueKey, index) => (
|
|
173
|
+
<li key={`value-keys-list-${index}`}>{valueKey}<button onClick={() => {
|
|
174
|
+
let newValueKeys = configureData.dataDescription.valueKeys;
|
|
175
|
+
newValueKeys.splice(index, 1);
|
|
176
|
+
updateDescriptionProp(visualizationKey, dataKey, 'valueKeys', newValueKeys)
|
|
177
|
+
}}>X</button></li>
|
|
178
|
+
))}
|
|
179
|
+
</ul>
|
|
180
|
+
)}
|
|
169
181
|
<select
|
|
170
182
|
onChange={e => {
|
|
171
|
-
|
|
183
|
+
if(e.target.value && (!configureData.dataDescription.valueKeys || configureData.dataDescription.valueKeys.indexOf(e.target.value) === -1)){
|
|
184
|
+
updateDescriptionProp(visualizationKey, dataKey, 'valueKeys', [...(configureData.dataDescription.valueKeys || []), e.target.value])
|
|
185
|
+
}
|
|
172
186
|
}}
|
|
173
|
-
defaultValue={configureData.dataDescription.valueKey}
|
|
174
187
|
>
|
|
175
188
|
<option value=''>Choose an option</option>
|
|
176
|
-
{Object.keys(configureData.data[0]).map((value, index) => (
|
|
177
|
-
<option value={value} key={index}>
|
|
189
|
+
{Object.keys(configureData.data[0]).filter(value => !configureData.dataDescription.valueKeys || configureData.dataDescription.valueKeys.indexOf(value) === -1).map((value, index) => (
|
|
190
|
+
<option value={value} key={`value-keys-option-${index}`}>
|
|
191
|
+
{value}
|
|
192
|
+
</option>
|
|
193
|
+
))}
|
|
194
|
+
</select>
|
|
195
|
+
</div>
|
|
196
|
+
<div className='mb-2'>
|
|
197
|
+
<div className='mb-1'>(Optional) Which properties in the dataset should be ignored? (will not be used or treated as filters)</div>
|
|
198
|
+
{configureData.dataDescription.ignoredKeys && configureData.dataDescription.ignoredKeys.length > 0 && (
|
|
199
|
+
<ul className="value-list">
|
|
200
|
+
{configureData.dataDescription.ignoredKeys.map((ignoredKey, index) => (
|
|
201
|
+
<li key={`value-keys-list-${index}`}>{ignoredKey}<button onClick={() => {
|
|
202
|
+
let newIgnoredKeys = configureData.dataDescription.ignoredKeys;
|
|
203
|
+
newIgnoredKeys.splice(index, 1);
|
|
204
|
+
updateDescriptionProp(visualizationKey, dataKey, 'ignoredKeys', newIgnoredKeys)
|
|
205
|
+
}}>X</button></li>
|
|
206
|
+
))}
|
|
207
|
+
</ul>
|
|
208
|
+
)}
|
|
209
|
+
<select
|
|
210
|
+
onChange={e => {
|
|
211
|
+
if(e.target.value){
|
|
212
|
+
updateDescriptionProp(visualizationKey, dataKey, 'ignoredKeys', [...(configureData.dataDescription.ignoredKeys || []), e.target.value])
|
|
213
|
+
}
|
|
214
|
+
e.target.value = '';
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<option value=''>Choose an option</option>
|
|
218
|
+
{Object.keys(configureData.data[0]).filter(value => !configureData.dataDescription.ignoredKeys || configureData.dataDescription.ignoredKeys.indexOf(value) === -1).map((value, index) => (
|
|
219
|
+
<option value={value} key={`ignored-keys-option-${index}`}>
|
|
178
220
|
{value}
|
|
179
221
|
</option>
|
|
180
222
|
))}
|
package/components/ui/Icon.jsx
CHANGED
|
@@ -28,6 +28,8 @@ import iconWarningTriangle from '../../assets/icon-warning-triangle.svg'
|
|
|
28
28
|
import iconGear from '../../assets/icon-gear.svg'
|
|
29
29
|
import iconTools from '../../assets/icon-tools.svg'
|
|
30
30
|
import iconText from '../../assets/filtered-text.svg'
|
|
31
|
+
import iconPlus from '../../assets/icon-plus.svg'
|
|
32
|
+
import iconMinus from '../../assets/icon-minus.svg'
|
|
31
33
|
|
|
32
34
|
import '../../styles/v2/components/icon.scss'
|
|
33
35
|
|
|
@@ -58,6 +60,8 @@ const iconHash = {
|
|
|
58
60
|
warningTriangle: iconWarningTriangle,
|
|
59
61
|
gear: iconGear,
|
|
60
62
|
tools: iconTools,
|
|
63
|
+
plus: iconPlus,
|
|
64
|
+
minus: iconMinus,
|
|
61
65
|
'filtered-text': iconText
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -1,37 +1,53 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { useId } from 'react'
|
|
2
|
+
|
|
3
|
+
// Third Party
|
|
4
|
+
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
5
|
+
|
|
6
|
+
// Styles
|
|
7
|
+
import '../../styles/v2/components/ui/tooltip.scss'
|
|
3
8
|
|
|
4
9
|
const TooltipTarget = () => null
|
|
5
10
|
const TooltipContent = () => null
|
|
6
11
|
|
|
7
|
-
const Tooltip = ({
|
|
12
|
+
const Tooltip = ({
|
|
13
|
+
place = 'top',
|
|
14
|
+
trigger = 'hover',
|
|
15
|
+
float = false,
|
|
16
|
+
shadow = true,
|
|
17
|
+
border = false,
|
|
18
|
+
children,
|
|
19
|
+
style,
|
|
20
|
+
...attributes
|
|
21
|
+
}) => {
|
|
22
|
+
|
|
8
23
|
const tooltipTargetChildren = children.find(el => el.type === TooltipTarget)
|
|
9
24
|
const tooltipContentChildren = children.find(el => el.type === TooltipContent)
|
|
10
25
|
|
|
11
|
-
const uid = 'tooltip-' +
|
|
26
|
+
const uid = 'tooltip-' + useId()
|
|
27
|
+
|
|
28
|
+
const generateTriggerEvent = (trigger) => {
|
|
29
|
+
const eventList = {
|
|
30
|
+
'hover': null,
|
|
31
|
+
'focus': 'focus',
|
|
32
|
+
'click': 'click focus'
|
|
33
|
+
}
|
|
34
|
+
return eventList[trigger]
|
|
35
|
+
}
|
|
12
36
|
|
|
13
37
|
return (
|
|
14
|
-
<span className=
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
data-event={trigger}
|
|
20
|
-
data-effect={float ? 'float' : 'solid'}
|
|
21
|
-
data-scroll-hide={hideOnScroll}
|
|
22
|
-
data-tip
|
|
23
|
-
onClick={e => {
|
|
24
|
-
e.preventDefault()
|
|
25
|
-
}}
|
|
26
|
-
onMouseEnter={e => ReactTooltip.show(e.target)}
|
|
27
|
-
onMouseLeave={e => ReactTooltip.hide(e.target)}
|
|
28
|
-
onFocus={e => ReactTooltip.show(e.target)}
|
|
29
|
-
onBlur={e => ReactTooltip.hide(e.target)}
|
|
30
|
-
style={{ background: 'transparent', border: 'none' }}
|
|
38
|
+
<span className="cove-tooltip" style={style} {...attributes}>
|
|
39
|
+
<a id={uid} className="cove-tooltip--target"
|
|
40
|
+
data-tooltip-float={float}
|
|
41
|
+
data-tooltip-place={place}
|
|
42
|
+
data-tooltip-events={generateTriggerEvent()}
|
|
31
43
|
>
|
|
32
44
|
{tooltipTargetChildren ? tooltipTargetChildren.props.children : null}
|
|
33
|
-
</
|
|
34
|
-
<ReactTooltip
|
|
45
|
+
</a>
|
|
46
|
+
<ReactTooltip
|
|
47
|
+
id={uid} anchorId={uid}
|
|
48
|
+
className={'cove-tooltip__content' + (' place-' + place) + (!float ? ' cove-tooltip__content--animated' : '') + (trigger === 'click' ? ' interactive' : '') + (border ? (' cove-tooltip--border') : '') + (shadow ? ' has-shadow' : '')}
|
|
49
|
+
globalEventOff="click"
|
|
50
|
+
>
|
|
35
51
|
{tooltipContentChildren ? tooltipContentChildren.props.children : null}
|
|
36
52
|
</ReactTooltip>
|
|
37
53
|
</span>
|
package/helpers/DataTransform.js
CHANGED
|
@@ -11,109 +11,109 @@ export class DataTransform {
|
|
|
11
11
|
|
|
12
12
|
//Performs standardizations that can be completed automatically without use input
|
|
13
13
|
autoStandardize(data) {
|
|
14
|
-
const errorsFound = []
|
|
14
|
+
const errorsFound = [];
|
|
15
15
|
|
|
16
16
|
// Empty data
|
|
17
17
|
if (0 === data.length) {
|
|
18
|
-
errorsFound.push(this.constants.errorMessageEmptyData)
|
|
18
|
+
errorsFound.push(this.constants.errorMessageEmptyData);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Does it have the correct data structure?
|
|
22
22
|
if (!data.filter || data.filter(row => typeof row !== 'object').length > 0) {
|
|
23
|
-
errorsFound.push(this.constants.errorMessageFormat)
|
|
23
|
+
errorsFound.push(this.constants.errorMessageFormat);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (errorsFound.length > 0) {
|
|
27
|
-
console.error(errorsFound)
|
|
28
|
-
return undefined
|
|
27
|
+
console.error(errorsFound);
|
|
28
|
+
return undefined;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
//Convert array of arrays, to array of objects
|
|
32
32
|
if (data.filter(row => row.constructor !== Object).length > 0) {
|
|
33
|
-
let standardizedData = []
|
|
33
|
+
let standardizedData = [];
|
|
34
34
|
for (let row = 1; row < data.length; row++) {
|
|
35
|
-
let standardizedRow = {}
|
|
35
|
+
let standardizedRow = {};
|
|
36
36
|
data[row].forEach((datum, col) => {
|
|
37
|
-
standardizedRow[data[0][col]] = datum
|
|
37
|
+
standardizedRow[data[0][col]] = datum;
|
|
38
38
|
})
|
|
39
|
-
standardizedData.push(standardizedRow)
|
|
39
|
+
standardizedData.push(standardizedRow);
|
|
40
40
|
}
|
|
41
|
-
data = standardizedData
|
|
41
|
+
data = standardizedData;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
return data
|
|
44
|
+
return data;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
//Performs standardizations based on developer provided description of the data
|
|
48
48
|
developerStandardize(data, description) {
|
|
49
49
|
//Validate the description object
|
|
50
50
|
if (!description) {
|
|
51
|
-
return undefined
|
|
51
|
+
return undefined;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (description.horizontal === undefined || description.series === undefined) {
|
|
55
|
-
return undefined
|
|
55
|
+
return undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (description.series === true && description.horizontal === false && description.singleRow === undefined) {
|
|
59
|
-
return undefined
|
|
59
|
+
return undefined;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (description.horizontal === true) {
|
|
63
63
|
if (description.series === true) {
|
|
64
64
|
if (!description.seriesKey) {
|
|
65
|
-
return undefined
|
|
65
|
+
return undefined;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
let standardizedMapped = {}
|
|
69
|
-
let standardized = []
|
|
68
|
+
let standardizedMapped = {};
|
|
69
|
+
let standardized = [];
|
|
70
70
|
data.forEach(row => {
|
|
71
|
-
let nonNumericKeys = []
|
|
71
|
+
let nonNumericKeys = [];
|
|
72
72
|
Object.keys(row).forEach(key => {
|
|
73
73
|
if (key !== description.seriesKey && isNaN(parseFloat(row[key]))) {
|
|
74
|
-
nonNumericKeys.push(key)
|
|
74
|
+
nonNumericKeys.push(key);
|
|
75
75
|
}
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
Object.keys(row).forEach(key => {
|
|
79
79
|
if (key !== description.seriesKey && nonNumericKeys.indexOf(key) === -1) {
|
|
80
|
-
let uniqueKey = key + '|' + nonNumericKeys.map(nonNumericKey => nonNumericKey + '=' + row[nonNumericKey])
|
|
80
|
+
let uniqueKey = key + '|' + nonNumericKeys.map(nonNumericKey => nonNumericKey + '=' + row[nonNumericKey]);
|
|
81
81
|
if (!standardizedMapped[uniqueKey]) {
|
|
82
|
-
standardizedMapped[uniqueKey] = { [row[description.seriesKey]]: row[key], key }
|
|
82
|
+
standardizedMapped[uniqueKey] = { [row[description.seriesKey]]: row[key], key };
|
|
83
83
|
nonNumericKeys.forEach(nonNumericKey => {
|
|
84
|
-
standardizedMapped[uniqueKey][nonNumericKey] = row[nonNumericKey]
|
|
84
|
+
standardizedMapped[uniqueKey][nonNumericKey] = row[nonNumericKey];
|
|
85
85
|
})
|
|
86
86
|
}
|
|
87
|
-
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[key]
|
|
87
|
+
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[key];
|
|
88
88
|
}
|
|
89
89
|
})
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
Object.keys(standardizedMapped).forEach(key => {
|
|
93
|
-
standardized.push(standardizedMapped[key])
|
|
93
|
+
standardized.push(standardizedMapped[key]);
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
-
return standardized
|
|
96
|
+
return standardized;
|
|
97
97
|
} else {
|
|
98
|
-
let standardized = []
|
|
98
|
+
let standardized = [];
|
|
99
99
|
|
|
100
100
|
data.forEach(row => {
|
|
101
|
-
let nonNumericKeys = []
|
|
101
|
+
let nonNumericKeys = [];
|
|
102
102
|
Object.keys(row).forEach(key => {
|
|
103
103
|
if (isNaN(parseFloat(row[key]))) {
|
|
104
|
-
nonNumericKeys.push(key)
|
|
104
|
+
nonNumericKeys.push(key);
|
|
105
105
|
}
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
Object.keys(row).forEach(key => {
|
|
109
109
|
if (nonNumericKeys.indexOf(key) === -1) {
|
|
110
|
-
let newRow = { key, value: row[key] }
|
|
110
|
+
let newRow = { key, value: row[key] };
|
|
111
111
|
|
|
112
112
|
nonNumericKeys.forEach(nonNumericKey => {
|
|
113
|
-
newRow[nonNumericKey] = row[nonNumericKey]
|
|
113
|
+
newRow[nonNumericKey] = row[nonNumericKey];
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
standardized.push(newRow)
|
|
116
|
+
standardized.push(newRow);
|
|
117
117
|
}
|
|
118
118
|
})
|
|
119
119
|
})
|
|
@@ -121,41 +121,80 @@ export class DataTransform {
|
|
|
121
121
|
return standardized
|
|
122
122
|
}
|
|
123
123
|
} else if (description.series === true && description.singleRow === false) {
|
|
124
|
-
if (description.seriesKey !== undefined && description.xKey !== undefined && description.valueKey !== undefined) {
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
if (description.seriesKey !== undefined && description.xKey !== undefined && (description.valueKey !== undefined || (description.valueKeys !== undefined && description.valueKeys.length > 0))) {
|
|
125
|
+
if(description.valueKeys !== undefined){
|
|
126
|
+
let standardizedMapped = {};
|
|
127
|
+
let standardized = [];
|
|
128
|
+
let valueKeys = description.valueKeys;
|
|
129
|
+
if(description.ignoredKeys && description.ignoredKeys.length > 0){
|
|
130
|
+
valueKeys = valueKeys.concat(description.ignoredKeys);
|
|
131
|
+
}
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
data.forEach(row => {
|
|
134
|
+
valueKeys.forEach(valueKey => {
|
|
135
|
+
let extraKeys = [];
|
|
136
|
+
let uniqueKey = row[description.xKey] + '|' + valueKey;
|
|
137
|
+
Object.keys(row).forEach(key => {
|
|
138
|
+
if (key !== description.xKey && key !== description.seriesKey && valueKeys.indexOf(key) === -1) {
|
|
139
|
+
uniqueKey += '|' + key + '=' + row[key];
|
|
140
|
+
extraKeys.push(key);
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if(!standardizedMapped[uniqueKey]){
|
|
145
|
+
standardizedMapped[uniqueKey] = { [description.xKey]: row[description.xKey], '**Numeric Value Property**': valueKey };
|
|
146
|
+
extraKeys.forEach(key => {
|
|
147
|
+
standardizedMapped[uniqueKey][key] = row[key];
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[valueKey];
|
|
152
|
+
});
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
Object.keys(standardizedMapped).forEach(key => {
|
|
156
|
+
if(!description.ignoredKeys || description.ignoredKeys.indexOf(standardizedMapped[key]['**Numeric Value Property**']) === -1){
|
|
157
|
+
standardized.push(standardizedMapped[key]);
|
|
135
158
|
}
|
|
136
159
|
})
|
|
137
160
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
return standardized;
|
|
162
|
+
} else {
|
|
163
|
+
let standardizedMapped = {}
|
|
164
|
+
let standardized = []
|
|
165
|
+
|
|
166
|
+
data.forEach(row => {
|
|
167
|
+
let extraKeys = []
|
|
168
|
+
let uniqueKey = row[description.xKey]
|
|
169
|
+
Object.keys(row).forEach(key => {
|
|
170
|
+
if (key !== description.xKey && key !== description.seriesKey && key !== description.valueKey) {
|
|
171
|
+
uniqueKey += '|' + key + '=' + row[key]
|
|
172
|
+
extraKeys.push(key)
|
|
173
|
+
}
|
|
144
174
|
})
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
175
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
if (standardizedMapped[uniqueKey]) {
|
|
177
|
+
standardizedMapped[uniqueKey][row[description.seriesKey]] = row[description.valueKey]
|
|
178
|
+
} else {
|
|
179
|
+
standardizedMapped[uniqueKey] = { [description.xKey]: row[description.xKey], [row[description.seriesKey]]: row[description.valueKey] }
|
|
180
|
+
extraKeys.forEach(key => {
|
|
181
|
+
standardizedMapped[uniqueKey][key] = row[key]
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
})
|
|
151
185
|
|
|
152
|
-
|
|
186
|
+
Object.keys(standardizedMapped).forEach(key => {
|
|
187
|
+
standardized.push(standardizedMapped[key])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return standardized
|
|
191
|
+
}
|
|
153
192
|
} else {
|
|
154
|
-
return undefined
|
|
193
|
+
return undefined;
|
|
155
194
|
}
|
|
156
195
|
}
|
|
157
196
|
|
|
158
|
-
return data
|
|
197
|
+
return data;
|
|
159
198
|
}
|
|
160
199
|
}
|
|
161
200
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cleanData
|
|
3
|
+
*
|
|
4
|
+
// This cleans a data set by:
|
|
5
|
+
// - removing commas and $ signs from any numbers to try to plot the point
|
|
6
|
+
// - removing any data points that are NOT composed of of all digits (but allow a decimal point)
|
|
7
|
+
// Without this the charts "break" and do not render
|
|
8
|
+
*
|
|
9
|
+
* Inputs: data as array, excludeKey indicates which key to use to NOT clean
|
|
10
|
+
* Example: "Date" should not be cleaned if part of the data
|
|
11
|
+
*
|
|
12
|
+
* Output: returns the cleanedData
|
|
13
|
+
*
|
|
14
|
+
* Set testing = true if you need to see before and after data
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
export default function cleanData (data, excludeKey, testing = false) {
|
|
18
|
+
let cleanedupData = []
|
|
19
|
+
if (testing) console.log('## Data to clean=', data)
|
|
20
|
+
if (excludeKey === undefined) {
|
|
21
|
+
excludeKey = "Date" // have a default value
|
|
22
|
+
}
|
|
23
|
+
data.forEach(function (d, i) {
|
|
24
|
+
if (testing) console.log("clean", i, " d", d);
|
|
25
|
+
let cleanedBar = {}
|
|
26
|
+
Object.keys(d).forEach(function (key) {
|
|
27
|
+
if (key === excludeKey) {
|
|
28
|
+
// pass thru
|
|
29
|
+
cleanedBar[key] = d[key]
|
|
30
|
+
} else {
|
|
31
|
+
// remove comma and dollar signs
|
|
32
|
+
if (testing) console.log("typeof d[key] is ", typeof d[key]);
|
|
33
|
+
let tmp = "";
|
|
34
|
+
if (typeof d[key] === 'string') {
|
|
35
|
+
tmp = d[key] !== null && d[key] !== '' ? d[key].replace(/[,\$]/g, '') : ''
|
|
36
|
+
} else {
|
|
37
|
+
tmp = d[key] !== null && d[key] !== '' ? d[key] : ''
|
|
38
|
+
}
|
|
39
|
+
if ((tmp !== '' && tmp !== null && !isNaN(tmp)) || (tmp !== '' && tmp !== null && /\d+\.?\d*/.test(tmp))) {
|
|
40
|
+
cleanedBar[key] = tmp
|
|
41
|
+
} else { cleanedBar[key] = '' }
|
|
42
|
+
// if you get here, then return nothing to skip bad data point
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
if (testing) console.log("cleanedBar=", cleanedBar);
|
|
46
|
+
cleanedupData.push(cleanedBar)
|
|
47
|
+
})
|
|
48
|
+
if (testing) console.log('## cleanedData =', cleanedupData)
|
|
49
|
+
return cleanedupData
|
|
50
|
+
}
|
|
@@ -2,9 +2,6 @@ import Papa from 'papaparse'
|
|
|
2
2
|
|
|
3
3
|
export default async function (url, visualizationType = '') {
|
|
4
4
|
try {
|
|
5
|
-
// Using URL Object to get pathname without URL paramaters on regex.
|
|
6
|
-
if (visualizationType === 'map') url = decodeURI(url)
|
|
7
|
-
|
|
8
5
|
url = new URL(url)
|
|
9
6
|
|
|
10
7
|
const path = url.pathname
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isNumber
|
|
3
|
+
*
|
|
4
|
+
* There are many ways this could be written but this one is working
|
|
5
|
+
* to check if something is a number or not
|
|
6
|
+
* Input: value
|
|
7
|
+
*
|
|
8
|
+
* Output: boolean: true or false
|
|
9
|
+
*/
|
|
10
|
+
export default function isNumber(value = '') {
|
|
11
|
+
// if you need to check data to see if there is junk in there that can't be handled
|
|
12
|
+
// you can run the points through this and see values on the console
|
|
13
|
+
// in debugging I saw cases where inbound was a 'number'
|
|
14
|
+
// and other times a 'string' so might as well take care of both here
|
|
15
|
+
if (typeof value === 'number') {
|
|
16
|
+
return !Number.isNaN(value)
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return value !== null && value !== '' && /\d+\.?\d*/.test(value)
|
|
20
|
+
// regex explanation: if 1 or more digits, and a decimal followed by 0 or more digits,
|
|
21
|
+
// then its a number
|
|
22
|
+
}
|
|
23
|
+
return false // if we get here something is wrong so return false
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default function isNumberLog(value = '', state = null) {
|
|
2
|
+
// if you need to check data to see if there is junk in there that can't be handled
|
|
3
|
+
// you can run the points through this and see values on the console
|
|
4
|
+
console.log("entering isNumber valuetype is:",typeof value);
|
|
5
|
+
var test;
|
|
6
|
+
if (typeof value === 'number') {
|
|
7
|
+
test = !Number.isNaN(value)
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === 'string') {
|
|
10
|
+
test = value !== null && value !== '' && /\d+\.?\d*/.test(value)
|
|
11
|
+
}
|
|
12
|
+
if (test === false) {
|
|
13
|
+
console.log('# isNumber FALSE on value, result', value, test)
|
|
14
|
+
} else {
|
|
15
|
+
console.log('# isNumber TRUE on value, result', value, test)
|
|
16
|
+
}
|
|
17
|
+
return test
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/core",
|
|
3
|
-
"version": "4.23.
|
|
4
|
-
"description": "Core
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
3
|
+
"version": "4.23.2",
|
|
4
|
+
"description": "Core components, styles, hooks, and helpers, for the CDC Open Visualization project",
|
|
5
|
+
"moduleName": "CdcCore",
|
|
6
|
+
"main": "dist/cdccore",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
9
|
+
},
|
|
8
10
|
"repository": {
|
|
9
11
|
"type": "git",
|
|
10
12
|
"url": "git+https://github.com/CDCgov/cdc-open-viz.git"
|
|
11
13
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
},
|
|
14
|
+
"author": "Rob Shelnutt <rob@blackairplane.com>",
|
|
15
|
+
"homepage": "https://github.com/CDCgov/cdc-open-viz#readme",
|
|
15
16
|
"bugs": {
|
|
16
17
|
"url": "https://github.com/CDCgov/cdc-open-viz/issues"
|
|
17
18
|
},
|
|
18
|
-
"
|
|
19
|
-
"react": "^17.0.2",
|
|
20
|
-
"react-dom": ">=16"
|
|
21
|
-
},
|
|
19
|
+
"license": "Apache-2.0",
|
|
22
20
|
"dependencies": {
|
|
23
21
|
"html2canvas": "^1.4.1",
|
|
24
|
-
"html2pdf.js": "^0.10.1",
|
|
25
22
|
"papaparse": "^5.3.0",
|
|
26
23
|
"prop-types": "^15.8.1",
|
|
27
|
-
"react-accessible-accordion": "^
|
|
24
|
+
"react-accessible-accordion": "^5.0.0",
|
|
28
25
|
"react-select": "^5.3.1",
|
|
29
|
-
"react-tooltip": "
|
|
26
|
+
"react-tooltip": "5.8.2-beta.3",
|
|
30
27
|
"use-debounce": "^6.0.1"
|
|
31
28
|
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": "^18.2.0",
|
|
31
|
+
"react-dom": "^18.2.0"
|
|
34
32
|
},
|
|
35
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "cd4216f47b1c41bfbc1de3b704f70c52cc7293c2"
|
|
36
34
|
}
|
package/styles/_data-table.scss
CHANGED
|
@@ -9,26 +9,25 @@
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
div.data-table-heading {
|
|
12
|
+
position: relative;
|
|
12
13
|
background: rgba(0, 0, 0, 0.05);
|
|
13
14
|
padding: 0.5em 0.7em;
|
|
14
15
|
border: $lightGray 1px solid;
|
|
15
16
|
border-bottom: 0;
|
|
16
17
|
cursor: pointer;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
|
|
19
|
+
svg {
|
|
20
|
+
position: absolute;
|
|
21
|
+
height: 100%;
|
|
22
|
+
width: 15px;
|
|
23
|
+
top: 0;
|
|
24
|
+
right: 1em;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
&:focus {
|
|
22
28
|
z-index: 2;
|
|
23
29
|
position: relative;
|
|
24
30
|
}
|
|
25
|
-
&.collapsed {
|
|
26
|
-
background-image: url(~@cdc/core/assets/icon-plus.svg);
|
|
27
|
-
background-size: 15px 15px; // Need to define both for IE11
|
|
28
|
-
background-position: right 0.7em center;
|
|
29
|
-
background-repeat: no-repeat;
|
|
30
|
-
border-bottom: $lightGray 1px solid;
|
|
31
|
-
}
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
table.data-table {
|
|
@@ -96,12 +95,12 @@ table.data-table {
|
|
|
96
95
|
|
|
97
96
|
th.sort-asc,
|
|
98
97
|
td.sort-asc {
|
|
99
|
-
background-image: url(
|
|
98
|
+
background-image: url(../assets/icon-caret-filled-up.svg);
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
th.sort-desc,
|
|
103
102
|
td.sort-desc {
|
|
104
|
-
background-image: url(
|
|
103
|
+
background-image: url(../assets/icon-caret-filled-down.svg);
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
th:last-child,
|
|
@@ -218,7 +217,7 @@ table.data-table {
|
|
|
218
217
|
button.btn-next {
|
|
219
218
|
&::before {
|
|
220
219
|
content: ' ';
|
|
221
|
-
background-image: url(
|
|
220
|
+
background-image: url(../assets/icon-caret-filled-up.svg);
|
|
222
221
|
background-size: 10px 5px;
|
|
223
222
|
width: 10px;
|
|
224
223
|
height: 5px;
|
|
@@ -229,7 +228,7 @@ table.data-table {
|
|
|
229
228
|
button.btn-prev {
|
|
230
229
|
&::before {
|
|
231
230
|
content: ' ';
|
|
232
|
-
background-image: url(
|
|
231
|
+
background-image: url(../assets/icon-caret-filled-up.svg);
|
|
233
232
|
background-size: 10px 5px;
|
|
234
233
|
width: 10px;
|
|
235
234
|
height: 5px;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
$cove-tooltip-bg: #fff;
|
|
2
|
+
$cove-tooltip-color: #333;
|
|
3
|
+
$cove-tooltip-animation: 500ms cubic-bezier(0.16, 1, 0.3, 1) 50ms 1 forwards;
|
|
4
|
+
|
|
5
|
+
.cove-tooltip {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
position: relative;
|
|
8
|
+
line-height: 1em;
|
|
9
|
+
|
|
10
|
+
&.cove-tooltip--border {
|
|
11
|
+
border: 1px solid #bdbdbd;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@at-root {
|
|
15
|
+
.cove-label + .cove-tooltip {
|
|
16
|
+
top: 1px;
|
|
17
|
+
margin-left: 0.5rem;
|
|
18
|
+
font-size: 0.75rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.cove-accordion__button .cove-tooltip {
|
|
22
|
+
display: inline-flex;
|
|
23
|
+
right: 1.5rem;
|
|
24
|
+
line-height: inherit;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.cove-list-group__item .cove-tooltip {
|
|
28
|
+
width: 100%;
|
|
29
|
+
display: block;
|
|
30
|
+
line-height: inherit;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.cove-tooltip--target {
|
|
36
|
+
display: inherit;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
|
|
39
|
+
@at-root {
|
|
40
|
+
.cove-accordion__button + .cove-tooltip--target {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
line-height: inherit;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.cove-tooltip__content {
|
|
48
|
+
max-width: 280px;
|
|
49
|
+
padding: 10px 8px;
|
|
50
|
+
font-size: 0.875rem;
|
|
51
|
+
line-height: 1.125rem;
|
|
52
|
+
text-align: left;
|
|
53
|
+
color: $cove-tooltip-color;
|
|
54
|
+
background-color: $cove-tooltip-bg;
|
|
55
|
+
border-radius: 5px;
|
|
56
|
+
user-select: none;
|
|
57
|
+
opacity: 0;
|
|
58
|
+
cursor: default;
|
|
59
|
+
z-index: 1;
|
|
60
|
+
|
|
61
|
+
&.place-top {
|
|
62
|
+
&.has-shadow {
|
|
63
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&.cove-tooltip__content--animated[class*='styles-module_show__'] {
|
|
67
|
+
animation: tooltip-btt $cove-tooltip-animation;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&.place-right {
|
|
72
|
+
&.has-shadow {
|
|
73
|
+
box-shadow: -4px 4px 14px rgba(0, 0, 0, 0.15), -4px 4px 8px rgba(0, 0, 0, 0.1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&.cove-tooltip__content--animated[class*='styles-module_show__'] {
|
|
77
|
+
animation: tooltip-ltr $cove-tooltip-animation;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
&.place-bottom {
|
|
82
|
+
&.has-shadow {
|
|
83
|
+
box-shadow: 0 -4px 14px rgba(0, 0, 0, 0.15), 0 8px 8px rgba(0, 0, 0, 0.1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&.cove-tooltip__content--animated[class*='styles-module_show__'] {
|
|
87
|
+
animation: tooltip-ttb $cove-tooltip-animation;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&.place-left {
|
|
92
|
+
&.has-shadow {
|
|
93
|
+
box-shadow: 4px 4px 14px rgba(0, 0, 0, 0.15), 4px 4px 8px rgba(0, 0, 0, 0.1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&.cove-tooltip__content--animated[class*='styles-module_show__'] {
|
|
97
|
+
animation: tooltip-rtl $cove-tooltip-animation;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.interactive {
|
|
103
|
+
a {
|
|
104
|
+
pointer-events: all;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@keyframes tooltip-ltr {
|
|
109
|
+
0% {
|
|
110
|
+
opacity: 0;
|
|
111
|
+
transform: translateX(-8px);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
100% {
|
|
115
|
+
opacity: 1;
|
|
116
|
+
transform: translateX(0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@keyframes tooltip-rtl {
|
|
121
|
+
0% {
|
|
122
|
+
opacity: 0;
|
|
123
|
+
transform: translateX(8px);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
100% {
|
|
127
|
+
opacity: 1;
|
|
128
|
+
transform: translateX(0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes tooltip-ttb {
|
|
133
|
+
0% {
|
|
134
|
+
opacity: 0;
|
|
135
|
+
transform: translateY(-8px);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
100% {
|
|
139
|
+
opacity: 1;
|
|
140
|
+
transform: translateY(0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@keyframes tooltip-btt {
|
|
145
|
+
0% {
|
|
146
|
+
opacity: 0;
|
|
147
|
+
transform: translateY(8px);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
100% {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
transform: translateY(0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
File without changes
|
|
File without changes
|