Created
October 26, 2020 20:49
-
-
Save valex/fdbd3ec3e210c85e45fcf0b200b37922 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
<html> | |
<head> | |
<meta charset="utf-8"> | |
<link rel="stylesheet" href="styles.css"> | |
</head> | |
<body> | |
<div style="display: flex; | |
flex-direction: row; justify-content: space-between;"> | |
<div id="chart"></div> | |
<div id="chart_right"></div> | |
</div> | |
</body> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js" ></script> | |
<script src="scripts.js"></script> | |
</html> |
This file contains hidden or 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
function CHART_3_class(id, selector){ | |
this.options = { | |
selector: selector, | |
viewBox: [400, 300], | |
levels: 7, | |
colors: { | |
black: '#1f2c37', | |
background: '#f7f9fa', | |
background_border: '#d7e5ec', | |
background_text: '#667a8a', | |
better: '#4bc774', | |
better_hover: '#76e582', | |
worse: '#d63a31', | |
worse_hover: '#eb4a41', | |
}, | |
padding_x: 0.1, | |
padding_y: 0.1, | |
} | |
this.id = id, | |
this.el = null, | |
this.aspect = null, | |
this.handSign = 1; | |
this.maxValue = 90; | |
this.originalData = null, | |
this.data = null, | |
this.vis = { | |
svg: null, | |
background: null, | |
sectors: null, | |
circles: null, | |
value_labels: null, | |
radis: null, | |
} | |
this.el = d3.select(this.options.selector); | |
this.aspect = this.options.viewBox[0] / this.options.viewBox[1]; | |
this.vis.radis = d3.min(this.mainAreaWidthAndHeight()); | |
window.addEventListener("resize", this.onResize.bind(this), false); | |
} | |
CHART_3_class.prototype = { | |
setData: function(data){ | |
this.originalData = Object.assign({}, data); | |
this.data = null; | |
this.prepareData(); | |
}, | |
prepareData: function(){ | |
this.data = Object.assign({}, this.originalData); | |
if( this.data.hand == 'right') | |
this.handSign = -1; | |
//build points | |
this.data['points'] = [ | |
{ | |
'type': 'before', | |
'value': this.data['onPeriodStart'], | |
'diff': null, | |
'coord': this.getCoordinates( this.data['onPeriodStart']) | |
}, | |
{ | |
'type': 'after', | |
'value': this.data['onPeriodEnd'], | |
'diff': this.data['onPeriodEnd'] - this.data['onPeriodStart'], | |
'coord': this.getCoordinates( this.data['onPeriodEnd']), | |
}, | |
]; | |
// build sectors | |
this.data['sectors'] = [ | |
{ | |
'type': 'before', | |
'diff': null, | |
'from_angle': 0, | |
'to_angle': this.getAngle(this.data['onPeriodStart']), | |
}, | |
{ | |
'type': 'after', | |
'diff': this.data['onPeriodEnd'] - this.data['onPeriodStart'], | |
'from_angle': this.getAngle(this.data['onPeriodStart']), | |
'to_angle': this.getAngle(this.data['onPeriodEnd']), | |
}, | |
]; | |
}, | |
buildVis: function(){ | |
if( ! this.vis.svg ) this.buildUnchanged(); | |
this.buildBackground(); | |
this.buildSectors(); | |
this.buildCircles(); | |
this.buildValueLabels(); | |
}, | |
buildValueLabels: function(){ | |
var that = this; | |
var basicPoint = this.basicPoint(); | |
var mainArea = this.mainAreaWidthAndHeight(); | |
var labelsPadding = 45; | |
this.vis.value_labels | |
.selectAll("g") | |
.data(this.data['points']) | |
.join('g') | |
.attr('class', function(d, i){ | |
return d.type.toLowerCase(); | |
}) | |
.classed('label_value', true) | |
.style('opacity', '0') | |
.each(function(d){ | |
// text | |
var text_bounding; | |
var text = d3.select(this) | |
.selectAll("text") | |
.data([d]) | |
.join("text") | |
.attr("x", function(d){ | |
var x = basicPoint[0] + that.handSign * mainArea[0]; | |
if( d.type != 'before' ) | |
x = x - that.handSign * labelsPadding; | |
return x; | |
}) | |
.attr("y", function(d){ | |
var value = d.value; | |
return that.getCoordinates(value, that.vis.radis)[1]; | |
}) | |
.attr("dx", function(d){ return 0}) | |
.attr("dy", 0) | |
.attr("font-size", "0.9rem") | |
.attr("fill", that.options.colors.black) | |
.attr("text-anchor", "middle") | |
.style("alignment-baseline", "middle") | |
.style("font-weight", "normal") | |
.attr("class", "noselect") | |
.text(function(d) { | |
return d.value+"\u00B0"; | |
}) | |
.each(function(){ | |
text_bounding = d3.select(this).node().getBBox(); | |
}) | |
//rect | |
var rect_padding = 3; | |
var rect_bounding; | |
d3.select(this) | |
.selectAll("rect") | |
.data( [ text_bounding ] ) | |
.join("rect") | |
.attr('rx', 4) | |
.attr('ry', 4) | |
.attr('x', function(d){ return d.x - rect_padding }) | |
.attr('y', function(d){ return d.y - rect_padding }) | |
.attr('width', function(d){ return d.width + 2*rect_padding }) | |
.attr('height', function(d){ return d.height + 2*rect_padding }) | |
.attr('fill', 'white') | |
.style('fill-opacity', "1") | |
.style('stroke-width', '1px') | |
.style('stroke', function(){ | |
if( ! d.diff) | |
return that.options.colors.black; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.each(function(){ | |
rect_bounding = d3.select(this).node().getBBox(); | |
}) | |
text.raise(); | |
//line | |
d3.select(this) | |
.selectAll('line') | |
.data([ rect_bounding ]) | |
.join("line") | |
.attr('x1', function(bounding){ | |
return that.getCoordinates(d.value)[0]; | |
}) | |
.attr('y1', function(bounding){ | |
return that.getCoordinates(d.value)[1]; | |
}) | |
.attr('x2', function(bounding){ | |
if(that.data.hand === 'right') | |
return bounding.x + rect_bounding.width; | |
return bounding.x; | |
}) | |
.attr('y2', function(bounding){ | |
return that.getCoordinates(d.value)[1]; | |
}) | |
.style('stroke-width', '1px') | |
.style('stroke', function(){ | |
if( ! d.diff) | |
return that.options.colors.black; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
}); | |
}, | |
buildCircles: function(){ | |
var that = this; | |
that.vis.circles | |
.selectAll("circle") | |
.data(this.data['points']) | |
.join( | |
function(enter){ | |
return enter.append('circle') | |
.attr('r', 4) | |
.attr('stroke-width', 2) | |
.attr('stroke', function(d){ | |
if( ! d.diff) | |
return that.options.colors.black; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.attr('fill', 'white') | |
.attr("cx", function(d){return d.coord[0]}) | |
.attr('cy', function(d){return d.coord[1]}) | |
.each(function(d, i) { | |
this._current = d; | |
}) // store the initial data | |
}, | |
function(update){ | |
return update | |
.attr('stroke', function(d){ | |
if( ! d.diff) | |
return that.options.colors.black; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.call(function(update){ | |
return update | |
.transition() | |
.duration(1000) | |
.attrTween("cx", function(d){ | |
var prevValue = this._current.value; | |
var currValue = d.value; | |
var i = d3.interpolate(prevValue, currValue); | |
return function(t) { | |
return that.getCoordinates(i(t))[0]; | |
}; | |
}) | |
.attrTween('cy', function(d){ | |
var prevValue = this._current.value; | |
var currValue = d.value; | |
var i = d3.interpolate(prevValue, currValue); | |
return function(t) { | |
return that.getCoordinates(i(t))[1]; | |
}; | |
}) | |
.on("end", function(d){ | |
this._current = d; | |
}) | |
}) | |
} | |
) | |
}, | |
buildSectors: function(){ | |
var that = this; | |
this.vis.sectors | |
.selectAll("path.body") | |
.data(this.data['sectors']) | |
.join( | |
function(enter){ | |
return enter.append("path") | |
.classed("body", true) | |
.attr("fill", function(d){ | |
if( d.type == 'before' ) | |
return 'white'; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.style("stroke-width", 2) | |
.style("stroke", function(d){ | |
if( d.type == 'before' ) | |
return 'white'; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.attr("d", function(d){ | |
return that.pathCircularSector( | |
d.from_angle, | |
d.to_angle | |
); | |
}) | |
.style("stroke-linejoin", "bevel") | |
.each(function(d, i) { | |
this._current = d; | |
}) // store the initial data | |
.on("mouseenter", function(event, data){ | |
if(data.type =='before'){ | |
that.el.selectAll("g.label_value:first-child") | |
.style("opacity", 1); | |
}else if(data.diff !=0){ | |
that.el.selectAll("g.label_value") | |
.style("opacity", 1); | |
} | |
if(data.type !='before' ){ | |
d3.select(this) | |
.style('stroke', function(){ | |
if( data.diff < 0){ | |
return that.options.colors.worse_hover; | |
} | |
return that.options.colors.better_hover; | |
}) | |
.attr('fill', function(){ | |
if( data.diff < 0){ | |
return that.options.colors.worse_hover; | |
} | |
return that.options.colors.better_hover; | |
}) | |
} | |
}) | |
.on("mouseleave", function(event, data){ | |
// if(data.type =='before') | |
// return; | |
that.el.selectAll("g.label_value") | |
.style("opacity", 0); | |
if(data.type !='before'){ | |
d3.select(this) | |
.style('stroke', function(){ | |
if( data.diff < 0){ | |
return that.options.colors.worse; | |
} | |
return that.options.colors.better; | |
}) | |
.attr('fill', function(){ | |
if( data.diff < 0){ | |
return that.options.colors.worse; | |
} | |
return that.options.colors.better; | |
}) | |
} | |
}) | |
}, | |
function(update){ | |
return update | |
.attr("fill", function(d){ | |
if( d.type == 'before' ) | |
return 'white'; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.style("stroke", function(d){ | |
if( d.type == 'before' ) | |
return 'white'; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.call(function(update){ | |
return update | |
.transition() | |
.duration(1000) | |
.attrTween("d", function(d){ | |
var i_from = d3.interpolate(this._current.from_angle, d.from_angle); | |
var i_to = d3.interpolate(this._current.to_angle, d.to_angle); | |
return function(t){ | |
return that.pathCircularSector( | |
i_from(t), | |
i_to(t) | |
); | |
} | |
}) | |
.on("end", function(d){ | |
this._current = d; | |
}) | |
}) | |
} | |
) | |
this.vis.sectors | |
.selectAll("path.border") | |
.data([this.data['sectors'][0]]) | |
.join( | |
function(enter){ | |
return enter | |
.append("path") | |
.classed("border", true) | |
.attr("fill-opacity", 0) | |
.style("stroke-width", 2) | |
.style("stroke", that.options.colors.black) | |
.style("pointer-events", "none") | |
.style("stroke-linejoin", "bevel") | |
.attr("d", function(d){ | |
return that.pathCircularSector( | |
d.from_angle, | |
d.to_angle | |
); | |
}) | |
.each(function(d, i) { | |
this._current = d; | |
}) // store the initial data | |
}, | |
function(update){ | |
return update | |
.call(function(update){ | |
return update | |
.transition() | |
.duration(1000) | |
.attrTween("d", function(d){ | |
var i_from = d3.interpolate(this._current.from_angle, d.from_angle); | |
var i_to = d3.interpolate(this._current.to_angle, d.to_angle); | |
return function(t){ | |
return that.pathCircularSector( | |
i_from(t), | |
i_to(t) | |
); | |
} | |
}) | |
.on("end", function(d){ | |
this._current = d; | |
}) | |
}) | |
}, | |
function(exit){return exit.remove();} | |
) | |
}, | |
buildBackground: function(){ | |
var that = this; | |
var basicPoint = this.basicPoint(); | |
var step = that.vis.radis / (that.options.levels + 1); | |
var levelsData = []; | |
for (var level = 0; level < this.options.levels; level++) { | |
levelsData.push( | |
step * (1+level) | |
); | |
} | |
// build level-lines | |
this.vis.background.selectAll('line') | |
.data(levelsData) | |
.enter() | |
.append("svg:line") | |
.classed("level-lines", true) | |
.attr("x1", function(d, i) { return basicPoint[0]; }) | |
.attr("y1", function(d, i) { return basicPoint[1] - d; }) | |
.attr("x2", function(d, i) { return basicPoint[0] + that.handSign * 1.04 * that.vis.radis; }) | |
.attr("y2", function(d, i) { return basicPoint[1] - d; }) | |
.style("stroke-dasharray", "4 1") | |
.attr("stroke", that.options.colors.background) | |
.attr("stroke-width", "1px"); | |
var toAngle = this.handSign * Math.PI/2; | |
this.vis.background | |
.selectAll("path") | |
.data(["one"]) | |
.enter() | |
.append("path") | |
.attr("fill", that.options.colors.background) | |
.style("stroke", that.options.colors.background_border) | |
.style("stroke-width", "1") | |
.attr("d", function(){ | |
return that.pathCircularSector( | |
0, | |
toAngle | |
); | |
}) | |
this.vis.background | |
.selectAll("text") | |
.data(["0", this.maxValue]) | |
.enter() | |
.append("text") | |
.attr("x", function(d,i){ | |
switch(i){ | |
case 0: | |
return basicPoint[0]; | |
break; | |
case 1: | |
return basicPoint[0] + that.handSign * that.vis.radis; | |
break; | |
} | |
}) | |
.attr("y", function(d, i){ | |
switch(i){ | |
case 0: | |
return basicPoint[1] - that.vis.radis; | |
break; | |
case 1: | |
return basicPoint[1]; | |
break; | |
} | |
}) | |
.attr("dx",function(d, i){ | |
return -10 * that.handSign; | |
}) | |
.attr("dy", function(d, i){ | |
switch(i){ | |
case 0: | |
return 6; | |
break; | |
case 1: | |
return 18; | |
break; | |
} | |
}) | |
.attr("font-size", "1rem") | |
.attr("fill", that.options.colors.background_text) | |
.attr("text-anchor", function(d, i){ | |
return "middle"; | |
}) | |
.style("alignment-baseline", "auto") | |
.style("font-weight", "normal") | |
.attr("class", "noselect") | |
.text(function(d) { | |
return d+"\u00B0"; | |
}) | |
}, | |
buildUnchanged: function(){ | |
var that = this; | |
// create main vis svg | |
this.vis['svg'] = this.el | |
.append("svg") | |
.classed("svg-vis", true) | |
.attr('xmlns', 'http://www.w3.org/2000/svg') | |
.attr("viewBox", "0 0 "+this.options.viewBox[0]+" "+this.options.viewBox[1]) | |
.attr("perserveAspectRatio", "xMinYMid") | |
.on("click", function(event, d){ | |
that.deselectAllAndHide(); | |
}) | |
.append("svg:g") | |
this.vis.background = this.vis['svg'] | |
.append("svg:g") | |
.classed('background', true); | |
this.vis.sectors = this.vis['svg'] | |
.append("svg:g") | |
.classed('sectors', true); | |
this.vis.value_labels = this.vis['svg'] | |
.append("svg:g") | |
.classed('value_labels', true); | |
this.vis.circles = this.vis['svg'] | |
.append("svg:g") | |
.classed('circles', true); | |
}, | |
pathCircularSector: function( fromAngle, toAngle ) { | |
var basicPoint = this.basicPoint(); | |
var center_x = basicPoint[0]; | |
var center_y = basicPoint[1]; | |
var anticlockwise = false; | |
if(toAngle < fromAngle){ | |
anticlockwise = true; | |
} | |
var path = d3.path(); | |
path.moveTo( | |
center_x, | |
center_y | |
); | |
path.lineTo( | |
center_x + this.vis.radis * Math.sin(fromAngle), | |
center_y - this.vis.radis * Math.cos(fromAngle), | |
); | |
path.arc( center_x, center_y, this.vis.radis, fromAngle - Math.PI/2, toAngle - Math.PI/2, anticlockwise); | |
path.closePath(); | |
return path; | |
}, | |
getAngle: function( degree ){ | |
var scale = d3.scaleLinear() | |
.domain([0, this.maxValue]) | |
.range([0, this.handSign * Math.PI/2]); | |
return scale(degree); | |
}, | |
getCoordinates: function( degree, radius ){ | |
radius = (typeof radius === 'undefined' ) ? this.vis.radis : radius; | |
var basicPoint = this.basicPoint(); | |
var center_x = basicPoint[0]; | |
var center_y = basicPoint[1]; | |
var x = center_x + radius * Math.sin( this.getAngle(degree) ); | |
var y = center_y - radius * Math.cos( this.getAngle(degree) ); | |
return [ | |
x, | |
y | |
]; | |
}, | |
mainAreaWidthAndHeight: function(){ | |
var width = this.options.viewBox[0] - this.paddingLeftWidth() - this.paddingRightWidth(); | |
var height = this.options.viewBox[1] - this.paddingBottomHeight()- this.paddingTopHeight(); | |
return [width, height]; | |
}, | |
basicPoint: function() { | |
var x; | |
y = this.options.viewBox[1] - this.paddingBottomHeight(); | |
switch(this.data.hand){ | |
case 'left': | |
x = this.paddingLeftWidth(); | |
break; | |
case 'right': | |
x = this.options.viewBox[0] - this.paddingRightWidth(); | |
break; | |
} | |
return [ | |
x, | |
y | |
]; | |
}, | |
paddingTopHeight: function() { | |
return this.options.padding_y * this.options.viewBox[1]; | |
}, | |
paddingBottomHeight: function() { | |
return this.options.padding_y * this.options.viewBox[1]; | |
}, | |
paddingRightWidth: function() { | |
return this.options.padding_x * this.options.viewBox[0]; | |
}, | |
paddingLeftWidth: function() { | |
return this.options.padding_x * this.options.viewBox[0]; | |
}, | |
deselectAllAndHide: function(){ | |
}, | |
onResize: function (){ | |
this.deselectAllAndHide(); | |
// this.updateSvgWidthAndHeight(); | |
}, | |
updateSvgWidthAndHeight: function (){ | |
var chartElContainer = d3.select(this.options.selector); | |
var chartEl = d3.select(this.options.selector + " > svg"); | |
var chartContainerBounding = chartElContainer.node().getBoundingClientRect(); | |
var targetWidth = chartContainerBounding.width; | |
chartEl.attr("width", targetWidth); | |
chartEl.attr("height", Math.round(targetWidth / this.aspect)); | |
}, | |
} | |
var id1_data = { | |
"hand": "left", | |
"onPeriodStart": 73.4, | |
"onPeriodEnd": 73.4 | |
}; | |
var chart_left = new CHART_3_class('id1', '#chart'); | |
chart_left.setData(id1_data); | |
chart_left.buildVis(); | |
var id2_data = { | |
"hand": "right", | |
"onPeriodStart": 73.4, | |
"onPeriodEnd": 89.1 | |
}; | |
var chart_right = new CHART_3_class('id2', '#chart_right'); | |
chart_right.setData(id2_data); | |
chart_right.buildVis(); | |
var newData = { | |
"hand": "left", | |
"onPeriodStart": 33.4, | |
"onPeriodEnd": 87.1 | |
}; | |
var newDataRight = { | |
"hand": "right", | |
"onPeriodStart": 89.4, | |
"onPeriodEnd": 22.1 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData); | |
chart_left.buildVis(); | |
}, 5000); | |
setTimeout(function(){ | |
chart_right.setData(newDataRight); | |
chart_right.buildVis(); | |
}, 5000); | |
var newData2 = { | |
"hand": "left", | |
"onPeriodStart": 44.4, | |
"onPeriodEnd": 22.1 | |
}; | |
var newDataRight2 = { | |
"hand": "right", | |
"onPeriodStart": 78.4, | |
"onPeriodEnd": 42.1 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData2); | |
chart_left.buildVis(); | |
}, 11000); | |
setTimeout(function(){ | |
chart_right.setData(newDataRight2); | |
chart_right.buildVis(); | |
}, 11000); | |
var newData3 = { | |
"hand": "left", | |
"onPeriodStart": 44.4, | |
"onPeriodEnd": 89.1 | |
}; | |
var newDataRight3 = { | |
"hand": "right", | |
"onPeriodStart": 68.4, | |
"onPeriodEnd": 85.1 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData3); | |
chart_left.buildVis(); | |
}, 17000); | |
setTimeout(function(){ | |
chart_right.setData(newDataRight3); | |
chart_right.buildVis(); | |
}, 17000); | |
var newData4 = { | |
"hand": "left", | |
"onPeriodStart": 55.4, | |
"onPeriodEnd": 77.1 | |
}; | |
var newDataRight4 = { | |
"hand": "right", | |
"onPeriodStart": 75.4, | |
"onPeriodEnd": 67.1 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData4); | |
chart_left.buildVis(); | |
}, 23000); | |
setTimeout(function(){ | |
chart_right.setData(newDataRight4); | |
chart_right.buildVis(); | |
}, 23000); |
This file contains hidden or 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
#chart { | |
width: 45%; | |
background-color: #ffffff; | |
} | |
#chart_right { | |
width: 45%; | |
background-color: #ffffff; | |
} | |
.noselect { | |
-webkit-touch-callout: none; /* iOS Safari */ | |
-webkit-user-select: none; /* Safari */ | |
-khtml-user-select: none; /* Konqueror HTML */ | |
-moz-user-select: none; /* Old versions of Firefox */ | |
-ms-user-select: none; /* Internet Explorer/Edge */ | |
user-select: none; /* Non-prefixed version, currently | |
supported by Chrome, Edge, Opera and Firefox */ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment