Last active
June 5, 2018 12:55
-
-
Save mmazanec22/667e1eb57674866bc5a1da2f195757e4 to your computer and use it in GitHub Desktop.
Semiotic Diverging Bar With Line
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3-color.v1.min.js"></script> | |
<script src="https://unpkg.com/react@16/umd/react.development.js"></script> | |
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/semiotic.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/babel.min.js"></script> | |
<style> | |
body { | |
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; | |
} | |
.tooltip-content { | |
background: white; | |
border: 1px solid black; | |
color: black; | |
z-index: 99; | |
opacity: 1; | |
} | |
.tooltip-content:before { | |
background: inherit; | |
content: ""; | |
padding: 0px; | |
transform: rotate(45deg); | |
width: 15px; | |
height: 15px; | |
position: absolute; | |
z-index: 99; | |
border-right: inherit; | |
border-bottom: inherit; | |
bottom: -8px; | |
left: calc(50% - 15px); | |
} | |
.tooltip { | |
opacity: 1; | |
background-color: white; | |
border: 0.1em solid black; | |
} | |
.semiotic-axis path { | |
opacity: 0.5; | |
} | |
</style> | |
</head> | |
<body> | |
<script type="text/babel"> | |
const dataKeysArr = [ | |
'Incoming', | |
'Outgoing', | |
]; | |
const dataArr = [ | |
{ | |
month: '4/2016', | |
Incoming: 40, | |
'Remaining to be housed': 103, | |
Outgoing: -27, | |
'Net change': 13, | |
}, | |
{ | |
month: '5/2016', | |
Incoming: 45, | |
'Remaining to be housed': 124, | |
Outgoing: -30, | |
'Net change': 5, | |
}, | |
{ | |
month: '6/2016', | |
Incoming: 42, | |
'Remaining to be housed': 126, | |
Outgoing: -44, | |
'Net change': -2, | |
}, | |
{ | |
month: '7/2016', | |
Incoming: 32, | |
'Remaining to be housed': 117, | |
Outgoing: -31, | |
'Net change': 1, | |
}, | |
{ | |
month: '8/2016', | |
Incoming: 35, | |
'Remaining to be housed': 112, | |
Outgoing: -33, | |
'Net change': 2, | |
}, | |
{ | |
month: '9/2016', | |
Incoming: 38, | |
'Remaining to be housed': 105, | |
Outgoing: -35, | |
'Net change': 3, | |
}, | |
{ | |
month: '10/2016', | |
Incoming: 40, | |
'Remaining to be housed': 108, | |
Outgoing: -41, | |
'Net change': -1, | |
}, | |
{ | |
month: '11/2016', | |
Incoming: 39, | |
'Remaining to be housed': 105, | |
Outgoing: -50, | |
'Net change': -11, | |
}, | |
{ | |
month: '12/2016', | |
Incoming: 32, | |
'Remaining to be housed': 84, | |
Outgoing: -37, | |
'Net change': -5, | |
}, | |
{ | |
month: '1/2017', | |
Incoming: 35, | |
'Remaining to be housed': 100, | |
Outgoing: -31, | |
'Net change': 4, | |
}, | |
{ | |
month: '2/2017', | |
Incoming: 25, | |
'Remaining to be housed': 89, | |
Outgoing: -41, | |
'Net change': -16, | |
}, | |
{ | |
month: '3/2017', | |
Incoming: 30, | |
'Remaining to be housed': 95, | |
Outgoing: -24, | |
'Net change': 6, | |
}, | |
{ | |
month: '4/2017', | |
Incoming: 34, | |
'Remaining to be housed': 95, | |
Outgoing: -28, | |
'Net change': 6, | |
}, | |
{ | |
month: '5/2017', | |
Incoming: 28, | |
'Remaining to be housed': 95, | |
Outgoing: -31, | |
'Net change': -3, | |
}, | |
{ | |
month: '6/2017', | |
Incoming: 38, | |
'Remaining to be housed': 95, | |
Outgoing: -20, | |
'Net change': 18, | |
}, | |
{ | |
month: '7/2017', | |
Incoming: 37, | |
'Remaining to be housed': 95, | |
Outgoing: -43, | |
'Net change': -6, | |
}, | |
]; | |
const mainAxisDataKey = 'month'; | |
const colorSchemes = { | |
orange_purple_diverging: ['#7f3b08', '#b2abd2', '#b35806', '#d8daeb', '#e08214', '#8073ac', '#fdb863', '#542788', '#fee0b6', '#2d004b', '#f7f7f7'], // color brewer diverging 11 orange-purple | |
} | |
const xAccessor = (d) => { | |
const arrDate = d.month.split('/'); | |
return new Date(arrDate[1], arrDate[0] - 1); | |
} | |
const formatDataForStackedBar = (data, dataKeys, mainAxisDataKey, colorScheme) => { | |
const formattedData = dataKeys.map((k, kIndex) => { | |
const thisData = data.map((d) => { | |
const rVal = {}; | |
rVal[mainAxisDataKey] = d[mainAxisDataKey]; | |
rVal.label = k || '[error]'; | |
rVal.value = d[k] ? d[k] : 0; | |
const thisScheme = colorSchemes[colorScheme]; | |
rVal.color = thisScheme[kIndex % thisScheme.length]; | |
return rVal; | |
}); | |
const sum = thisData.reduce((total, num) => { | |
const innerReturnObj = {}; | |
innerReturnObj.value = total.value + num.value; | |
return innerReturnObj; | |
}).value; | |
return { data: thisData, sum }; | |
}).sort((a, b) => b.sum - a.sum) | |
.map(d => d.data) | |
.reduce((p, c) => p.concat(c)); | |
return formattedData; | |
}; | |
const labelOrder = (formattedData, valueAccessor = 'value') => JSON.parse(JSON.stringify(formattedData)) | |
.filter((item, pos, thisArray) => | |
// Limit it to just the first occurrence | |
pos === thisArray.findIndex(d => d.label === item.label && d.color === item.color) | |
).map((item) => { | |
item.sum = formattedData | |
.filter(d => d.label === item.label) | |
.reduce((total, num) => { | |
const returnObj = {}; | |
returnObj[valueAccessor] = total[valueAccessor] + num.value; | |
return returnObj; | |
}); | |
return item; | |
}).sort((a, b) => b.sum[valueAccessor] - a.sum[valueAccessor]); | |
const container = document.body.appendChild(document.createElement('div')); | |
// import React from 'react'; | |
// import PropTypes from 'prop-types'; | |
// import Tooltip from './Tooltip'; | |
const HorizontalLegend = (props) => { | |
const rectWidth = 15; | |
const labelItems = labelOrder(props.formattedData, props.valueAccessor); | |
return (<div | |
style={props.style} | |
> | |
{labelItems.map((item, index) => { | |
const label = item.label; | |
return (<div | |
key={`${label}-legendItem-${index}`} | |
style={{ | |
padding: `0px ${rectWidth / 3}px 0px 0px`, | |
whiteSpace: 'normal', | |
display: 'inline-block', | |
textAlign: 'left', | |
}} | |
> | |
<svg | |
height={rectWidth} | |
width={rectWidth} | |
style={{ | |
margin: `0px ${rectWidth / 4}px`, | |
}} | |
> | |
<rect | |
style={{ | |
fill: item.color, | |
}} | |
x={0} | |
y={0} | |
width={rectWidth} | |
height={rectWidth} | |
/> | |
</svg> | |
<span> | |
{label} | |
</span> | |
</div>); | |
})} | |
</div>); | |
}; | |
const Tooltip = (props) => { | |
const styles = props.style || {}; | |
styles.fontSize = '1rem'; | |
styles.padding = '0.5rem'; | |
const minWidth = Math.min( | |
(props.textLines.map(line => line.text).join('').length) / props.textLines.length, | |
20 | |
); | |
styles.minWidth = `${minWidth / 2}em`; | |
return (<div style={styles}> | |
<div style={{ fontWeight: 'bolder', textAlign: 'center' }}> | |
{props.title} | |
</div> | |
{props.textLines.map((lineObj, i) => | |
<div key={`textLine-${i}`} style={{ color: lineObj.color }}>{lineObj.text}</div> | |
)} | |
</div>); | |
}; | |
class DivergingLineBar extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
hover: null, | |
} | |
} | |
render() { | |
const formattedData = formatDataForStackedBar( | |
this.props.data, | |
this.props.dataKeys, | |
this.props.mainAxisDataKey, | |
this.props.colorScheme, | |
); | |
const margin = { | |
top: 30, | |
right: 10, | |
bottom: 70, | |
left: 45, | |
}; | |
const annotations = this.props.data.map((d) => { | |
return { | |
type: 'or', | |
month: d.month, | |
value: d['Net change'], | |
linePoint: true, | |
}; | |
}); | |
annotations.slice(0, annotations.length - 1).forEach((a, i) => { | |
annotations.push({ | |
type: 'line', | |
coordinates: [a, annotations[i + 1]], | |
}); | |
}); | |
return ( | |
<div style={{ width: '100%', textAlign: 'center' }}> | |
<div className="row visualization-container"> | |
<Semiotic.ResponsiveOrdinalFrame | |
responsiveWidth | |
annotations={annotations} | |
margin={margin} | |
data={formattedData} | |
type="bar" | |
projection="vertical" | |
oAccessor={(d) => { | |
if (d && !d.piece) { | |
const datum = d.data ? d.data : d; | |
return this.props.xAccessor(datum); | |
} | |
}} | |
oLabel={(d) => { | |
let textAnchor = 'middle'; | |
let transform = 'translate(0,0)'; | |
if (this.props.rotateXLabels && this.props.layout === 'vertical') { | |
textAnchor = 'end'; | |
transform = 'translate(8,0)'; | |
} else if (this.props.layout === 'horizontal') { | |
textAnchor = 'end'; | |
} | |
if (this.props.rotateXLabels) { transform += 'rotate(-45)'; } | |
const label = `${d.getMonth() + 1}/${d.getFullYear()}`; | |
return ( | |
<text | |
textAnchor={textAnchor} | |
transform={transform} | |
key={label} | |
> | |
{label} | |
</text> | |
); | |
}} | |
rAccessor={(d) => { return d ? d.value : null; }} | |
style={(d) => { | |
return this.state.hover && this.state.hover.getTime() === this.props.xAccessor(d).getTime() ? | |
// For the currently hovered bar, return a brighter fill and add a stroke | |
{ | |
fill: d3.color(d.color).brighter(0.6).toString(), | |
stroke: d3.color(d.color).toString(), | |
strokeWidth: 3, | |
} : | |
{ fill: d.color }; | |
} | |
} | |
oPadding={8} | |
axis={{ | |
orient: 'left', | |
}} | |
pieceHoverAnnotation | |
// hoverAnnotation | |
customHoverBehavior={(d) => { | |
if (d) { | |
this.setState({ hover: this.props.xAccessor(d) }); | |
} else { | |
this.setState({ hover: null }); | |
} | |
}} | |
svgAnnotationRules={(d) => { | |
if (d.d.type !== 'line' && !d.d.linePoint) { | |
return null; | |
} | |
if (d.d.type === 'line') { | |
return ( | |
<line | |
key={`${d.d.type}-${d.i}`} | |
stroke="black" | |
strokeWidth={1.5} | |
x1={d.screenCoordinates[0][0]} | |
x2={d.screenCoordinates[1][0]} | |
y1={d.screenCoordinates[0][1]} | |
y2={d.screenCoordinates[1][1]} | |
/> | |
); | |
} | |
const circleStyle = { | |
stroke: 'black', | |
strokeWidth: 1.5, | |
fill: 'none', | |
}; | |
if (this.state.hover && this.state.hover.getTime() === this.props.xAccessor(d.d).getTime()) { | |
circleStyle.strokeWidth = 2; | |
circleStyle.fill = 'white'; | |
circleStyle.fillOpacity = 0.5; | |
} | |
return ( | |
<circle | |
key={d.i + d.d.month} | |
cx={d.screenCoordinates[0]} | |
cy={d.screenCoordinates[1]} | |
r={5} | |
style={circleStyle} | |
/> | |
); | |
}} | |
tooltipContent={(d) => { | |
const datum = d.data ? d.data : d; | |
const textLines = formattedData | |
.filter(el => el.month === datum.month) | |
.map(inOut => ({ | |
color: inOut.color, | |
text: `${inOut.label}: ${inOut.value}`, | |
})); | |
textLines.push({ | |
color: 'black', | |
text: `Net Change: ${ | |
this.props.data.find(el => el.month === datum.month)['Net change'] | |
}`, | |
}); | |
return ( | |
<Tooltip | |
textLines={textLines} | |
title={datum.month} | |
/> | |
); | |
}} | |
/> | |
</div> | |
<div | |
className="row" | |
style={{ width: '100%' }} | |
> | |
<div className="col-xs-10 col-xs-offset-1"> | |
<HorizontalLegend | |
formattedData={formattedData} | |
style={{ textAlign: 'center' }} | |
/> | |
</div> | |
</div> | |
</div> | |
); | |
} | |
} | |
ReactDOM.render( | |
<DivergingLineBar | |
data={dataArr} | |
dataKeys={dataKeysArr} | |
mainAxisDataKey='month' | |
xAccessor={(d) => { | |
const arrDate = d.month.split('/'); | |
return new Date(arrDate[1], arrDate[0] - 1); | |
}} | |
colorScheme="orange_purple_diverging" | |
/>, | |
container | |
); | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment