Created
October 25, 2020 10:35
-
-
Save valex/cf5f0774f85471f1387b88429bae7477 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_5_class(id, selector){ | |
this.options = { | |
selector: selector, | |
viewBox: [400, 300], | |
colors: { | |
background: '#f7f9fa', | |
background_border: '#d7e5ec', | |
background_text: '#667a8a', | |
better: '#4bc774', | |
better_hover: '#76e582', | |
worse: '#d63a31', | |
worse_hover: '#eb4a41', | |
}, | |
padding_bottom: 0.1, | |
padding_right: 0.15, | |
padding_left:0.15, | |
} | |
this.id = id, | |
this.el = null, | |
this.aspect = null, | |
this.leftMax = 90, | |
this.rightMax = 90, | |
this.originalData = null, | |
this.data = null, | |
this.vis = { | |
svg: null, | |
defs: 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]; | |
window.addEventListener("resize", this.onResize.bind(this), false); | |
} | |
CHART_5_class.prototype = { | |
setData: function(data){ | |
this.originalData = Object.assign({}, data); | |
this.data = null; | |
this.prepareData(); | |
}, | |
prepareData: function(){ | |
this.data = Object.assign({}, this.originalData); | |
var mainArea = this.mainAreaWidthAndHeight(); | |
this.vis.radis = mainArea[0] / 2; | |
//build points | |
this.data['points'] = [ | |
{ | |
'type': 'toForearm', | |
'value': this.data['onPeriodStart']['toForearm'], | |
'diff': null, | |
'coord': this.getCoordinates( -1 * this.data['onPeriodStart']['toForearm']) | |
}, | |
{ | |
'type': 'toForearm', | |
'value': this.data['onPeriodEnd']['toForearm'], | |
'diff': this.data['onPeriodEnd']['toForearm'] - this.data['onPeriodStart']['toForearm'], | |
'coord': this.getCoordinates( -1 * this.data['onPeriodEnd']['toForearm']), | |
}, | |
{ | |
'type': 'fromForearm', | |
'value': this.data['onPeriodStart']['fromForearm'], | |
'diff': null, | |
'coord': this.getCoordinates( this.data['onPeriodStart']['fromForearm']), | |
}, | |
{ | |
'type': 'fromForearm', | |
'value': this.data['onPeriodEnd']['fromForearm'], | |
'diff': this.data['onPeriodEnd']['fromForearm'] - this.data['onPeriodStart']['fromForearm'], | |
'coord': this.getCoordinates( this.data['onPeriodEnd']['fromForearm']), | |
}, | |
]; | |
// build sectors | |
this.data['sectors'] = [ | |
{ | |
'type': 'center', | |
'diff': null, | |
'from_angle': this.getAngle(-1 * this.data['onPeriodStart']['toForearm']), | |
'to_angle': this.getAngle(this.data['onPeriodStart']['fromForearm']), | |
}, | |
{ | |
'type': 'fromForearm', | |
'diff': this.data['onPeriodEnd']['fromForearm'] - this.data['onPeriodStart']['fromForearm'], | |
'from_angle': this.getAngle(this.data['onPeriodEnd']['fromForearm']), | |
'to_angle': this.getAngle(this.data['onPeriodStart']['fromForearm']), | |
}, | |
{ | |
'type': 'toForearm', | |
'diff': this.data['onPeriodEnd']['toForearm'] - this.data['onPeriodStart']['toForearm'], | |
'from_angle': this.getAngle(-1 * this.data['onPeriodEnd']['toForearm']), | |
'to_angle': this.getAngle(-1 * this.data['onPeriodStart']['toForearm']), | |
}, | |
]; | |
}, | |
buildVis: function(){ | |
if( ! this.vis.svg ) this.buildUnchanged(); | |
this.buildDefs(); | |
this.buildBackground(); | |
this.buildSectors(); | |
this.buildCircles(); | |
this.buildValueLabels(); | |
}, | |
buildValueLabels: function(){ | |
var that = this; | |
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,i){ | |
var value = d.value; | |
if(d.type === 'toForearm'){ | |
value = -value; | |
} | |
return that.getCoordinates(value, 22+that.vis.radis)[0]; | |
}) | |
.attr("y", function(d){ | |
var value = d.value; | |
if(d.type === 'toForearm'){ | |
value = -value; | |
} | |
return that.getCoordinates(value, 22+that.vis.radis)[1]; | |
}) | |
.attr("dx", function(d){ return 0}) | |
.attr("dy", 0) | |
.attr("font-size", "0.9rem") | |
.attr("fill", "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 = 2; | |
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 'black'; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
text.raise(); | |
}) | |
}, | |
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 '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 '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; | |
if(d.type ==='toForearm'){ | |
prevValue = -prevValue; | |
currValue = -currValue; | |
} | |
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; | |
if(d.type ==='toForearm'){ | |
prevValue = -prevValue; | |
currValue = -currValue; | |
} | |
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; | |
var basicPoint = this.basicPoint(); | |
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 == 'center' ) | |
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 == 'center' ) | |
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 | |
); | |
}) | |
.each(function(d, i) { | |
this._current = d; | |
}) // store the initial data | |
.on("mouseenter", function(event, data){ | |
if(data.type =='center') | |
return; | |
that.el.selectAll("g.label_value."+data.type.toLowerCase()) | |
.style("opacity", 1); | |
}) | |
.on("mouseleave", function(event, data){ | |
if(data.type =='center') | |
return; | |
that.el.selectAll("g.label_value."+data.type.toLowerCase()) | |
.style("opacity", 0); | |
}) | |
}, | |
function(update){ | |
return update | |
.attr("fill", function(d){ | |
if( d.type == 'center' ) | |
return 'white'; | |
if( d.diff > 0) | |
return that.options.colors.better; | |
return that.options.colors.worse; | |
}) | |
.style("stroke", function(d){ | |
if( d.type == 'center' ) | |
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("line.center") | |
.data(["one"]) | |
.enter() | |
.append("line") | |
.classed("center", true) | |
.attr("x1", function(d){return basicPoint[0] }) | |
.attr("y1", function(d){return basicPoint[1] }) | |
.attr("x2", function(d){return basicPoint[0] }) | |
.attr("y2", function(d){return -10 + basicPoint[1] - that.vis.radis }) | |
.attr("stroke-dasharray", 4 ) | |
.style("stroke", that.options.colors.background_text) | |
.style("stroke-linecap", "round") | |
.style("stroke-width", "1") | |
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", "black") | |
.style("pointer-events", "none") | |
.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 mainArea = this.mainAreaWidthAndHeight(); | |
this.vis.background | |
.selectAll("line") | |
.data(["one"]) | |
.enter() | |
.append("line") | |
.attr("x1", function(d){return basicPoint[0] - mainArea[0]/2}) | |
.attr("y1", function(d){return basicPoint[1]}) | |
.attr("x2", function(d){return basicPoint[0] + mainArea[0]/2}) | |
.attr("y2", function(d){return basicPoint[1]}) | |
.style("stroke", that.options.colors.background_border) | |
.style("stroke-linecap", "round") | |
.style("stroke-width", "1") | |
this.vis.background | |
.selectAll("path") | |
.data(["one"]) | |
.enter() | |
.append("path") | |
.attr("fill", that.options.colors.background) | |
.style("stroke-width", "0") | |
.attr("d", function(){ | |
return that.pathCircularSector( | |
-Math.PI/2, | |
Math.PI/2 | |
); | |
}) | |
this.vis.background | |
.selectAll("text") | |
.data([this.leftMax, "0", this.rightMax]) | |
.enter() | |
.append("text") | |
.attr("x", function(d,i){ | |
switch(i){ | |
case 0: | |
return basicPoint[0] - mainArea[0]/2; | |
break; | |
case 1: | |
return basicPoint[0] ; | |
break; | |
case 2: | |
return basicPoint[0] + mainArea[0]/2; | |
break; | |
} | |
}) | |
.attr("y", function(d, i){ | |
switch(i){ | |
case 0: | |
return basicPoint[1]; | |
break; | |
case 1: | |
return basicPoint[1] - that.vis.radis; | |
break; | |
case 2: | |
return basicPoint[1]; | |
break; | |
} | |
}) | |
.attr("dx",function(d, i){ | |
switch(i){ | |
case 1: | |
return 4; | |
break; | |
default: | |
return 0; | |
break; | |
} | |
}) | |
.attr("dy", function(d, i){ | |
switch(i){ | |
case 0: | |
case 2: | |
return 16; | |
break; | |
case 1: | |
return -20; | |
break; | |
} | |
}) | |
.attr("font-size", "1rem") | |
.attr("fill", that.options.colors.background_text) | |
.attr("text-anchor", function(d, i){ | |
switch(i){ | |
case 0: | |
return "start"; | |
break; | |
case 1: | |
return "middle"; | |
break; | |
case 2: | |
return "end"; | |
break; | |
} | |
}) | |
.style("alignment-baseline", "auto") | |
.style("font-weight", "normal") | |
.attr("class", "noselect") | |
.text(function(d) { | |
return d+"\u00B0"; | |
}) | |
}, | |
getAngle: function( degree ){ | |
var scale = d3.scaleLinear() | |
.domain([-this.leftMax, this.rightMax]) | |
.range([-Math.PI/2, 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 | |
]; | |
}, | |
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; | |
}, | |
buildDefs: function(){ | |
}, | |
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['defs'] = this.vis['svg'] | |
.append("defs"); | |
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.circles = this.vis['svg'] | |
.append("svg:g") | |
.classed('circles', true); | |
this.vis.value_labels = this.vis['svg'] | |
.append("svg:g") | |
.classed('value_labels', true); | |
}, | |
mainAreaWidthAndHeight: function(){ | |
var width = this.options.viewBox[0] - this.paddingLeftWidth() - this.paddingRightWidth(); | |
var height = this.options.viewBox[1] - this.paddingBottomHeight(); | |
return [width, height]; | |
}, | |
basicPoint: function() { | |
var pleftWidth = this.paddingLeftWidth(); | |
var prightWidth = this.paddingRightWidth(); | |
var x = pleftWidth + ( (this.options.viewBox[0] - pleftWidth - prightWidth) / 2 ); | |
var y = this.options.viewBox[1] - this.paddingBottomHeight(); | |
return [ | |
x, | |
y | |
]; | |
}, | |
paddingBottomHeight: function() { | |
return this.options.padding_bottom * this.options.viewBox[1]; | |
}, | |
paddingRightWidth: function() { | |
return this.options.padding_right * this.options.viewBox[0]; | |
}, | |
paddingLeftWidth: function() { | |
return this.options.padding_left * 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": { | |
"toForearm": 70.1, | |
"fromForearm": 60.3 | |
}, | |
"onPeriodEnd": { | |
"toForearm": 2,//88.5, | |
"fromForearm": 15//81.7 | |
} | |
}; | |
var chart_left = new CHART_5_class('id1', '#chart'); | |
chart_left.setData(id1_data); | |
chart_left.buildVis(); | |
var id2_data = { | |
"hand": "right", | |
"onPeriodStart": { | |
"toForearm": 15.1, | |
"fromForearm": 25.3 | |
}, | |
"onPeriodEnd": { | |
"toForearm": 30.5, | |
"fromForearm": 55.7 | |
} | |
}; | |
var chart_right = new CHART_5_class('id2', '#chart_right'); | |
chart_right.setData(id2_data); | |
chart_right.buildVis(); | |
var newData = { | |
"hand": "left", | |
"onPeriodStart": { | |
"toForearm": 60.1, | |
"fromForearm": 50.3 | |
}, | |
"onPeriodEnd": { | |
"toForearm": 78.5, | |
"fromForearm": 71.7 | |
} | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData); | |
chart_left.buildVis(); | |
}, 5000); | |
var newData2 = { | |
"hand": "left", | |
"onPeriodStart": { | |
"toForearm": 11.1, | |
"fromForearm": 41.3 | |
}, | |
"onPeriodEnd": { | |
"toForearm": 54.5, | |
"fromForearm": 18.7 | |
} | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData2); | |
chart_left.buildVis(); | |
}, 11000); | |
var newData3 = { | |
"hand": "left", | |
"onPeriodStart": { | |
"toForearm": 66.1, | |
"fromForearm": 21.3 | |
}, | |
"onPeriodEnd": { | |
"toForearm": 58.5, | |
"fromForearm": 79.7 | |
} | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData3); | |
chart_left.buildVis(); | |
}, 17000); | |
var newData4 = { | |
"hand": "left", | |
"onPeriodStart": { | |
"toForearm": 66.1, | |
"fromForearm": 76.3 | |
}, | |
"onPeriodEnd": { | |
"toForearm": 78.5, | |
"fromForearm": 89.7 | |
} | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData4); | |
chart_left.buildVis(); | |
}, 22000); |
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