@cdc/chart 4.24.5 → 4.24.9
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/dist/cdcchart.js +44197 -38258
- package/examples/cases-year.json +13379 -0
- package/examples/feature/annotations/index.json +542 -0
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +76 -15
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +5 -5
- package/examples/xaxis.json +493 -0
- package/index.html +20 -10
- package/package.json +5 -4
- package/src/CdcChart.tsx +462 -172
- package/src/_stories/Chart.Legend.Gradient.tsx +19 -0
- package/src/_stories/Chart.stories.tsx +18 -171
- package/src/_stories/ChartAnnotation.stories.tsx +32 -0
- package/src/_stories/_mock/annotation_category_mock.json +473 -0
- package/src/_stories/_mock/annotation_date-linear_mock.json +530 -0
- package/{examples/feature/line/line-chart.json → src/_stories/_mock/annotation_date-time_mock.json} +150 -69
- package/src/_stories/_mock/legend.gradient_mock.json +236 -0
- package/src/_stories/_mock/line_chart_two_points_new_chart.json +128 -0
- package/src/_stories/_mock/line_chart_two_points_regression_test.json +127 -0
- package/src/_stories/_mock/lollipop.json +171 -0
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +31 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +207 -0
- package/src/components/Annotations/components/AnnotationDropdown.styles.css +14 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +72 -0
- package/src/components/Annotations/components/AnnotationList.styles.css +45 -0
- package/src/components/Annotations/components/AnnotationList.tsx +42 -0
- package/src/components/Annotations/components/findNearestDatum.ts +138 -0
- package/src/components/Annotations/components/helpers/index.tsx +46 -0
- package/src/components/Annotations/index.tsx +13 -0
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -1
- package/src/components/AreaChart/components/AreaChart.jsx +1 -1
- package/src/components/Axis/Categorical.Axis.tsx +145 -0
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +47 -44
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +0 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +11 -14
- package/src/components/BarChart/components/BarChart.Vertical.tsx +67 -30
- package/src/components/BarChart/helpers/index.ts +91 -0
- package/src/components/BrushChart.tsx +205 -0
- package/src/components/EditorPanel/EditorPanel.tsx +1794 -403
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +320 -0
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +282 -18
- package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +43 -8
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +4 -4
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -13
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/components/panels.scss +4 -0
- package/src/components/EditorPanel/editor-panel.scss +35 -3
- package/src/components/EditorPanel/{useEditorPermissions.js → useEditorPermissions.ts} +105 -17
- package/src/components/Legend/Legend.Component.tsx +185 -194
- package/src/components/Legend/Legend.Suppression.tsx +146 -0
- package/src/components/Legend/Legend.tsx +21 -5
- package/src/components/Legend/helpers/createFormatLabels.tsx +1 -1
- package/src/components/Legend/helpers/index.ts +35 -0
- package/src/components/LegendWrapper.tsx +26 -0
- package/src/components/LineChart/LineChartProps.ts +1 -15
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +103 -0
- package/src/components/LineChart/components/LineChart.Circle.tsx +47 -8
- package/src/components/LineChart/helpers.ts +72 -14
- package/src/components/LineChart/index.tsx +117 -42
- package/src/components/LinearChart.jsx +179 -136
- package/src/components/LinearChart.tsx +1366 -0
- package/src/components/PairedBarChart.jsx +9 -9
- package/src/components/PieChart/PieChart.tsx +75 -18
- package/src/components/Sankey/index.tsx +89 -30
- package/src/components/ScatterPlot/ScatterPlot.jsx +22 -8
- package/src/components/Sparkline/components/SparkLine.tsx +2 -2
- package/src/components/ZoomBrush.tsx +90 -44
- package/src/data/initial-state.js +25 -7
- package/src/helpers/handleChartTabbing.ts +8 -0
- package/src/helpers/isConvertLineToBarGraph.ts +4 -0
- package/src/hooks/{useBarChart.js → useBarChart.ts} +2 -40
- package/src/hooks/useColorScale.ts +1 -1
- package/src/hooks/useLegendClasses.ts +68 -0
- package/src/hooks/useMinMax.ts +12 -7
- package/src/hooks/useScales.ts +58 -26
- package/src/hooks/useTooltip.tsx +135 -25
- package/src/scss/DataTable.scss +2 -1
- package/src/scss/main.scss +128 -28
- package/src/types/ChartConfig.ts +83 -10
- package/src/types/ChartContext.ts +14 -4
- package/tests-examples/helpers/testZeroValue.test.ts +30 -0
- package/LICENSE +0 -201
- package/src/components/BrushHandle.jsx +0 -17
- package/src/components/LineChart/index.scss +0 -1
- package/src/helpers/filterData.ts +0 -18
- package/src/helpers/tests/computeMarginBottom.test.ts +0 -21
- package/src/hooks/useLegendClasses.js +0 -31
- /package/src/hooks/{useReduceData.js → useReduceData.ts} +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useContext, useState } from 'react'
|
|
2
|
+
import ConfigContext from '../../../ConfigContext'
|
|
3
|
+
import './AnnotationDropdown.styles.css'
|
|
4
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
5
|
+
import Annotation from '..'
|
|
6
|
+
import { fontSizes } from '@cdc/core/helpers/cove/fontSettings'
|
|
7
|
+
|
|
8
|
+
const AnnotationDropdown = () => {
|
|
9
|
+
const { currentViewport: viewport, config } = useContext(ConfigContext)
|
|
10
|
+
const [expanded, setExpanded] = useState(false)
|
|
11
|
+
|
|
12
|
+
const titleFontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? '13px' : `${fontSizes[config?.fontSize]}px`
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
config: { annotations }
|
|
16
|
+
} = useContext(ConfigContext)
|
|
17
|
+
|
|
18
|
+
const limitHeight = {
|
|
19
|
+
maxHeight: config.table.limitHeight && `${config.table.height}px`,
|
|
20
|
+
OverflowY: 'scroll'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const handleAccordionClassName = () => {
|
|
24
|
+
const classNames = ['data-table-heading', 'annotation__dropdown-list']
|
|
25
|
+
if (!expanded) {
|
|
26
|
+
classNames.push('collapsed')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return classNames.join(' ')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleSectionClasses = () => {
|
|
33
|
+
const classes = [`data-table-container`, viewport, `d-block`, `d-lg-none`]
|
|
34
|
+
|
|
35
|
+
if (config.general.showAnnotationDropdown) {
|
|
36
|
+
classes.push('d-lg-block')
|
|
37
|
+
classes.splice(classes.indexOf('d-lg-none'), 1)
|
|
38
|
+
}
|
|
39
|
+
return classes.join(' ')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<section className={handleSectionClasses()}>
|
|
45
|
+
<div
|
|
46
|
+
style={{ fontSize: titleFontSize }}
|
|
47
|
+
role='button'
|
|
48
|
+
className={handleAccordionClassName()}
|
|
49
|
+
onClick={() => {
|
|
50
|
+
setExpanded(!expanded)
|
|
51
|
+
}}
|
|
52
|
+
tabIndex={0}
|
|
53
|
+
onKeyDown={e => {
|
|
54
|
+
if (e.keyCode === 13) {
|
|
55
|
+
setExpanded(!expanded)
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<Icon display={expanded ? 'minus' : 'plus'} base />
|
|
60
|
+
{config.general.annotationDropdownText === '' ? 'Annotations' : config?.general?.annotationDropdownText}
|
|
61
|
+
</div>
|
|
62
|
+
{expanded && (
|
|
63
|
+
<div className='table-container annotation-dropdown__panel' style={limitHeight}>
|
|
64
|
+
<Annotation.List useBootstrapVisibilityClasses={false} />
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</section>
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default AnnotationDropdown
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.cdc-open-viz-module {
|
|
2
|
+
.annotation__title-circle {
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
align-items: center;
|
|
6
|
+
border-radius: 50%;
|
|
7
|
+
padding: 8px;
|
|
8
|
+
width: 16px;
|
|
9
|
+
height: 16px;
|
|
10
|
+
margin-right: 5px;
|
|
11
|
+
line-height: 1.5rem;
|
|
12
|
+
|
|
13
|
+
border: 2px solid #666;
|
|
14
|
+
text-align: center;
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.annotation__title-wrapper {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-wrap: nowrap;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.annotation__title-wrapper .annotation__title-text {
|
|
25
|
+
margin-left: 5px;
|
|
26
|
+
font-size: 16px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.annotation__subtext {
|
|
30
|
+
font-size: 12px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.annotation-list {
|
|
34
|
+
list-style: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.annotation-list li {
|
|
38
|
+
margin-top: 5px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.cove-component__content {
|
|
42
|
+
container-type: inline-size;
|
|
43
|
+
container-name: content;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import ConfigContext from '../../../ConfigContext'
|
|
3
|
+
import './AnnotationList.styles.css'
|
|
4
|
+
import DOMPurify from 'dompurify'
|
|
5
|
+
|
|
6
|
+
type AnnotationListProps = {
|
|
7
|
+
useBootstrapVisibilityClasses?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const AnnotationList: React.FC<AnnotationListProps> = ({ useBootstrapVisibilityClasses = true }) => {
|
|
11
|
+
const { config } = useContext(ConfigContext)
|
|
12
|
+
const annotations = config.annotations || []
|
|
13
|
+
|
|
14
|
+
const ulClasses = () => {
|
|
15
|
+
const classes = ['annotation-list']
|
|
16
|
+
if (useBootstrapVisibilityClasses) {
|
|
17
|
+
classes.push('d-block', 'd-md-none')
|
|
18
|
+
}
|
|
19
|
+
return classes.join(' ')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const annotationListItems = annotations.map((annotation, annotationIndex) => {
|
|
23
|
+
const text = annotation.text || ''
|
|
24
|
+
|
|
25
|
+
// sanitize the text for setting dangerouslySetInnerHTML
|
|
26
|
+
const sanitizedData = () => ({
|
|
27
|
+
__html: DOMPurify.sanitize(text)
|
|
28
|
+
})
|
|
29
|
+
return (
|
|
30
|
+
<li key={`annotation-li-item__annotationIndex`}>
|
|
31
|
+
<div className='annotation__title-wrapper'>
|
|
32
|
+
<div className='annotation__title-circle'>{annotationIndex + 1}</div>
|
|
33
|
+
<p className='annotation__subtext' dangerouslySetInnerHTML={sanitizedData()} />
|
|
34
|
+
</div>
|
|
35
|
+
</li>
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return <ul className={ulClasses()}>{annotationListItems}</ul>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default AnnotationList
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { timeParse } from 'd3-time-format'
|
|
2
|
+
|
|
3
|
+
const getXValueFromCoordinate = (x, isClick = false) => {
|
|
4
|
+
if (visualizationType === 'Pie') return
|
|
5
|
+
if (orientation === 'horizontal') return
|
|
6
|
+
|
|
7
|
+
// Check the type of x equal to point or if the type of xAxis is equal to continuous or date
|
|
8
|
+
if (config.xAxis.type === 'categorical' || (visualizationType === 'Combo' && orientation !== 'horizontal' && visualizationType !== 'Forest Plot')) {
|
|
9
|
+
let range = xScale.range()[1] - xScale.range()[0]
|
|
10
|
+
let eachBand = range / (xScale.domain().length + 1)
|
|
11
|
+
|
|
12
|
+
let numerator = x
|
|
13
|
+
const index = Math.floor((Number(numerator) - eachBand / 2) / eachBand)
|
|
14
|
+
return xScale.domain()[index] // fixes off by 1 error
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (config.xAxis.type === 'date') {
|
|
18
|
+
const xValue = x // Assuming x is the coordinate on the chart
|
|
19
|
+
const xTimestamp = convertXValueToTimestamp(x, 0, xMax, xScale.domain())
|
|
20
|
+
|
|
21
|
+
// Calculate the closest date to the x coordinate
|
|
22
|
+
let closestDate = null
|
|
23
|
+
let minDistance = Number.MAX_VALUE
|
|
24
|
+
|
|
25
|
+
xScale.domain().forEach(timestamp => {
|
|
26
|
+
const distance = Math.abs(xTimestamp - timestamp)
|
|
27
|
+
if (distance < minDistance) {
|
|
28
|
+
minDistance = distance
|
|
29
|
+
closestDate = timestamp
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return closestDate
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return x
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const findNearestDatum = ({ data, xScale, yScale, config, xMax, annotationSeriesKey }, xPosition) => {
|
|
40
|
+
const { xAxis, visualizationType, orientation } = config
|
|
41
|
+
|
|
42
|
+
const convertXValueToTimestamp = (xValue, minX, maxX, domain, xScale) => {
|
|
43
|
+
let ticks = []
|
|
44
|
+
if (config.xAxis.type === 'date-time') {
|
|
45
|
+
minX = new Date(minX)
|
|
46
|
+
maxX = new Date(maxX)
|
|
47
|
+
domain = domain.map(d => new Date(d))
|
|
48
|
+
ticks = xScale.ticks().map(d => new Date(d))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Calculate the percentage position of xValue between minX and maxX
|
|
52
|
+
const percentage = (xValue - minX) / (maxX - minX)
|
|
53
|
+
|
|
54
|
+
// Calculate the index in the domain array corresponding to the percentage position
|
|
55
|
+
const index = Math.round(percentage * (domain.length - 1))
|
|
56
|
+
|
|
57
|
+
if (config.xAxis.type === 'date-time') {
|
|
58
|
+
return ticks[index]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Return the timestamp from the domain array at the calculated index
|
|
62
|
+
return domain[index]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const getXValueFromCoordinate = (x, isClick = false) => {
|
|
66
|
+
if (visualizationType === 'Pie') return
|
|
67
|
+
if (orientation === 'horizontal') return
|
|
68
|
+
|
|
69
|
+
if (config.xAxis.type === 'date-time') {
|
|
70
|
+
// Calculate the percentage position of xValue between minX and maxX
|
|
71
|
+
const invertedValue = new Date(xScale.invert(x))
|
|
72
|
+
const ticks = config.data.map(d => new Date(d[config.xAxis.dataKey]).getTime())
|
|
73
|
+
let minDistance = Infinity
|
|
74
|
+
let closestDate = null
|
|
75
|
+
|
|
76
|
+
ticks.forEach(timestamp => {
|
|
77
|
+
const distance = Math.abs(invertedValue.getTime() - timestamp)
|
|
78
|
+
if (distance < minDistance) {
|
|
79
|
+
minDistance = distance
|
|
80
|
+
closestDate = timestamp
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return new Date(closestDate).getTime()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check the type of x equal to point or if the type of xAxis is equal to continuous or date
|
|
88
|
+
if (config.xAxis.type === 'categorical' || (visualizationType === 'Combo' && orientation !== 'horizontal' && visualizationType !== 'Forest Plot')) {
|
|
89
|
+
const range = xScale.range()[1] - xScale.range()[0]
|
|
90
|
+
const eachBand = range / (xScale.domain().length + 1)
|
|
91
|
+
|
|
92
|
+
let numerator = x
|
|
93
|
+
const index = Math.floor((Number(numerator) - eachBand / 2) / eachBand)
|
|
94
|
+
return xScale.domain()[index] // fixes off by 1 error
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config.xAxis.type === 'date') {
|
|
98
|
+
const xValue = x // Assuming x is the coordinate on the chart
|
|
99
|
+
const xTimestamp = convertXValueToTimestamp(x, 0, xMax, xScale.domain(), xScale)
|
|
100
|
+
|
|
101
|
+
// Calculate the closest date to the x coordinate
|
|
102
|
+
let closestDate = null
|
|
103
|
+
let minDistance = Number.MAX_VALUE
|
|
104
|
+
|
|
105
|
+
xScale.domain().forEach(timestamp => {
|
|
106
|
+
const distance = Math.abs(xTimestamp - timestamp)
|
|
107
|
+
if (distance < minDistance) {
|
|
108
|
+
minDistance = distance
|
|
109
|
+
closestDate = timestamp
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return closestDate
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return x
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const xValue = getXValueFromCoordinate(xPosition - Number(config.yAxis.size || 0))
|
|
120
|
+
|
|
121
|
+
let closestSeries = []
|
|
122
|
+
|
|
123
|
+
if (!xValue) return { x: 0, y: 0 }
|
|
124
|
+
|
|
125
|
+
if (xAxis.type === 'categorical') {
|
|
126
|
+
closestSeries = config.data.filter(d => d[config.xAxis.dataKey] === xValue)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (xAxis.type === 'date' || xAxis.type === 'date-time') {
|
|
130
|
+
closestSeries = config.data.filter(d => new Date(d[config.xAxis.dataKey]).getTime() === xValue)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const y = closestSeries[0][annotationSeriesKey] // Map each key to its corresponding value in data
|
|
134
|
+
const x = xValue
|
|
135
|
+
return { x, y }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { findNearestDatum, getXValueFromCoordinate }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const applyBandScaleOffset = (num: number, config, xScale) => num + Number(config.yAxis.size) + xScale.bandwidth() / 2
|
|
2
|
+
const handleConnectionHorizontalType = (annotation, xScale, config) => {
|
|
3
|
+
const { connectionLocation } = annotation
|
|
4
|
+
if (connectionLocation === 'right') return 'end'
|
|
5
|
+
if (connectionLocation === 'left') return 'start'
|
|
6
|
+
if (connectionLocation === 'bottom' || connectionLocation === 'top') return 'middle'
|
|
7
|
+
return xScale(annotation.xKey) + annotation.dx < config.yAxis.size ? 'middle' : null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const handleConnectionVerticalType = (annotation, xScale, config) => {
|
|
11
|
+
const { connectionLocation } = annotation
|
|
12
|
+
if (connectionLocation === 'top') return 'start'
|
|
13
|
+
if (connectionLocation === 'bottom') return 'end'
|
|
14
|
+
if (connectionLocation === 'right' || connectionLocation === 'left') return 'middle'
|
|
15
|
+
return xScale(annotation.xKey) + annotation.dx < config.yAxis.size ? 'end' : null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const handleMobileXPosition = (annotation, xScale, config) => {
|
|
19
|
+
if (annotation.snapToNearestPoint) {
|
|
20
|
+
return Number(annotation.dx) + xScale(annotation.xKey) + (config.xAxis.type !== 'date-time' ? xScale.bandwidth() / 2 : 0) + Number(config.yAxis.size)
|
|
21
|
+
}
|
|
22
|
+
return Number(annotation.x) + Number(annotation.dx)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const handleMobileYPosition = (annotation, yScale, config) => {
|
|
26
|
+
if (annotation.snapToNearestPoint) {
|
|
27
|
+
return yScale(annotation.yKey) + Number(annotation.dy)
|
|
28
|
+
}
|
|
29
|
+
return Number(annotation.dy) + Number(annotation.y)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const handleTextX = (annotation, xScale, config) => {
|
|
33
|
+
if (annotation.snapToNearestPoint) {
|
|
34
|
+
return Number(annotation.dx) + Number(xScale(annotation.xKey)) + (config.xAxis.type !== 'date-time' ? xScale.bandwidth() / 2 : 0) + Number(config.yAxis.size) - 16 / 3
|
|
35
|
+
}
|
|
36
|
+
return Number(annotation.dx) + Number(annotation.x) - 16 / 3
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleTextY = (annotation, yScale, config) => {
|
|
40
|
+
if (annotation.snapToNearestPoint) {
|
|
41
|
+
return yScale(annotation.yKey) + Number(annotation.dy) + 5
|
|
42
|
+
}
|
|
43
|
+
return Number(annotation.y) + Number(annotation.dy) + 16 / 3
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { applyBandScaleOffset, handleConnectionHorizontalType, handleConnectionVerticalType, handleMobileXPosition, handleMobileYPosition, handleTextX, handleTextY }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import AnnotationDraggable from './components/AnnotationDraggable'
|
|
2
|
+
import AnnotationList from './components/AnnotationList'
|
|
3
|
+
import AnnotationDropdown from './components/AnnotationDropdown'
|
|
4
|
+
|
|
5
|
+
const Annotation = {
|
|
6
|
+
Draggable: AnnotationDraggable,
|
|
7
|
+
// Mobile auto display
|
|
8
|
+
List: AnnotationList,
|
|
9
|
+
// Desktop Accessible Option
|
|
10
|
+
Dropdown: AnnotationDropdown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default Annotation
|
|
@@ -14,7 +14,7 @@ import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
|
|
|
14
14
|
const AreaChartStacked = ({ xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff, isDebug }) => {
|
|
15
15
|
// import data from context
|
|
16
16
|
let { transformedData, config, seriesHighlight, colorScale, rawData } = useContext(ConfigContext)
|
|
17
|
-
const data = config.brush
|
|
17
|
+
const data = config.brush?.active && config.brush.data?.length ? config.brush.data : transformedData
|
|
18
18
|
// Draw transparent bars over the chart to get tooltip data
|
|
19
19
|
// Turn DEBUG on for additional context.
|
|
20
20
|
if (!data) return
|
|
@@ -15,7 +15,7 @@ const AreaChart = props => {
|
|
|
15
15
|
const { xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff, isDebug, children } = props
|
|
16
16
|
// import data from context
|
|
17
17
|
let { transformedData, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight, colorScale, rawData, brushConfig } = useContext(ConfigContext)
|
|
18
|
-
const data = config.brush
|
|
18
|
+
const data = config.brush?.active && brushConfig.data?.length ? brushConfig.data : transformedData
|
|
19
19
|
|
|
20
20
|
if (!data) return
|
|
21
21
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import { BarStack, Line } from '@visx/shape'
|
|
3
|
+
import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale'
|
|
4
|
+
import { Group } from '@visx/group'
|
|
5
|
+
import { Text } from '@visx/text'
|
|
6
|
+
import ConfigContext from '../../ConfigContext'
|
|
7
|
+
import chroma from 'chroma-js'
|
|
8
|
+
import createBarElement from '@cdc/core/components/createBarElement'
|
|
9
|
+
import { useBarChart } from '../../hooks/useBarChart'
|
|
10
|
+
|
|
11
|
+
const CategoricalYAxis = ({ yMax, leftSize, max, xMax }) => {
|
|
12
|
+
const { config, getTextWidth } = useContext(ConfigContext)
|
|
13
|
+
const { fontSize } = useBarChart()
|
|
14
|
+
|
|
15
|
+
const { orientation } = config
|
|
16
|
+
|
|
17
|
+
const getValidColor = (color, defaultColor = '#f1f1f1') => {
|
|
18
|
+
try {
|
|
19
|
+
return chroma(color).hex() // Returns the color if valid
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return defaultColor
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const categories = config.yAxis?.categories
|
|
26
|
+
|
|
27
|
+
const createDataShape = categories => {
|
|
28
|
+
const categoryObj = [...categories].reduce((acc, item) => {
|
|
29
|
+
acc[item.label] = item.height
|
|
30
|
+
return acc
|
|
31
|
+
}, {})
|
|
32
|
+
|
|
33
|
+
return categoryObj
|
|
34
|
+
}
|
|
35
|
+
const updateCategory = categoryObj => {
|
|
36
|
+
// Get all the heights in the object
|
|
37
|
+
const heights = Object.keys(categoryObj)
|
|
38
|
+
|
|
39
|
+
// Get the last label
|
|
40
|
+
const lastheight = heights[heights.length - 1]
|
|
41
|
+
|
|
42
|
+
// Check if the last label's value is empty
|
|
43
|
+
if (categoryObj[lastheight] === '') {
|
|
44
|
+
// Calculate the sum of the numeric values of all other heights
|
|
45
|
+
const sumOfValues = heights.slice(0, -1).reduce((sum, label) => {
|
|
46
|
+
const value = parseInt(categoryObj[label], 10)
|
|
47
|
+
return sum + (isNaN(value) ? 0 : value)
|
|
48
|
+
}, 0)
|
|
49
|
+
|
|
50
|
+
// Calculate the new value for the last emty height
|
|
51
|
+
const newValue = max - sumOfValues
|
|
52
|
+
|
|
53
|
+
// Update the last height with the new value
|
|
54
|
+
categoryObj[lastheight] = newValue.toString()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [categoryObj]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const transformedData = updateCategory(createDataShape(categories))
|
|
61
|
+
|
|
62
|
+
// Define the scales
|
|
63
|
+
const xScaleValue = 0
|
|
64
|
+
|
|
65
|
+
const xScale = scaleBand<number>({
|
|
66
|
+
domain: [xScaleValue],
|
|
67
|
+
padding: 0,
|
|
68
|
+
range: [0, leftSize]
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const yScale = scaleLinear({
|
|
72
|
+
domain: [0, max],
|
|
73
|
+
range: [yMax, 0],
|
|
74
|
+
clamp: true
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const colorScale = scaleOrdinal({
|
|
78
|
+
domain: categories.map(d => d?.label),
|
|
79
|
+
range: categories.map(d => getValidColor(d?.color?.trim()))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const keys = Object.keys(transformedData[0])
|
|
83
|
+
return (
|
|
84
|
+
<Group left={leftSize - xScale.bandwidth()} top={0}>
|
|
85
|
+
<BarStack data={transformedData} keys={keys} x={() => xScale(xScaleValue)} xScale={xScale} yScale={yScale} color={colorScale}>
|
|
86
|
+
{barStacks =>
|
|
87
|
+
barStacks.map(barStack =>
|
|
88
|
+
barStack.bars.map(bar => {
|
|
89
|
+
const isLastIndex = config.yAxis.categories.length - 1 === barStack.index
|
|
90
|
+
const textSize = fontSize[config.fontSize] / 1.3
|
|
91
|
+
const textColor = chroma(bar.color).luminance() < 0.4 ? '#fff' : '#000'
|
|
92
|
+
const textWidth = getTextWidth(bar.key, `normal ${textSize}px sans-serif`)
|
|
93
|
+
const displayText = Number(textWidth) < bar.width && bar.height > textSize
|
|
94
|
+
const tooltip = `<ul>
|
|
95
|
+
<li class="tooltip-heading""> Label : ${bar.key} </li>
|
|
96
|
+
</li></ul>`
|
|
97
|
+
return (
|
|
98
|
+
<Group key={`${barStack.index}--${bar.index}--${orientation}`}>
|
|
99
|
+
<Group key={`bar-stack-${barStack.index}-${bar.index}`} id={`barStack${barStack.index}-${bar.index}`} className='stack vertical'>
|
|
100
|
+
{createBarElement({
|
|
101
|
+
type: 'axisBar',
|
|
102
|
+
config: config,
|
|
103
|
+
index: barStack.index,
|
|
104
|
+
background: colorScale(bar.key),
|
|
105
|
+
borderColor: '#333',
|
|
106
|
+
borderStyle: 'solid',
|
|
107
|
+
borderWidth: 0,
|
|
108
|
+
width: xScale.bandwidth(),
|
|
109
|
+
height: bar.height,
|
|
110
|
+
x: bar.x,
|
|
111
|
+
y: bar.y,
|
|
112
|
+
tooltipHtml: tooltip,
|
|
113
|
+
tooltipId: `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
|
|
114
|
+
})}
|
|
115
|
+
{/* Label for axis stacks */}
|
|
116
|
+
<Text // ignore
|
|
117
|
+
display={!displayText ? 'none' : 'block'}
|
|
118
|
+
key={`text-${barStack.index}-${bar.index}`}
|
|
119
|
+
x={bar.x + xScale.bandwidth() / 2}
|
|
120
|
+
y={bar.y + bar.height / 2}
|
|
121
|
+
fill={textColor}
|
|
122
|
+
textAnchor='middle'
|
|
123
|
+
verticalAnchor='middle'
|
|
124
|
+
style={{ fontSize: textSize }}
|
|
125
|
+
>
|
|
126
|
+
{bar.key}
|
|
127
|
+
</Text>
|
|
128
|
+
{/* gridLines */}
|
|
129
|
+
{config.runtime.yAxis.gridLines && <Line from={{ x: bar.x + xScale.bandwidth(), y: bar.y }} to={{ x: xMax + xScale.bandwidth(), y: bar.y }} stroke='rgba(0,0,0,0.3)' />}
|
|
130
|
+
{/* White background spacing between stackes */}
|
|
131
|
+
{!isLastIndex && <rect x={bar.x} y={bar.y} width={bar.width} height={1} fill={'#fff'}></rect>}
|
|
132
|
+
{/* Right side Axis line */}
|
|
133
|
+
<rect x={bar.x + bar.width} y={0} width={1} height={yMax} fill={'#000'}></rect>
|
|
134
|
+
</Group>
|
|
135
|
+
</Group>
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
</BarStack>
|
|
141
|
+
</Group>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default CategoricalYAxis
|