Skip to content

Instantly share code, notes, and snippets.

@mmazanec22
Last active June 5, 2018 12:55
Show Gist options
  • Save mmazanec22/667e1eb57674866bc5a1da2f195757e4 to your computer and use it in GitHub Desktop.
Save mmazanec22/667e1eb57674866bc5a1da2f195757e4 to your computer and use it in GitHub Desktop.
Semiotic Diverging Bar With Line
<!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