Skip to content

Instantly share code, notes, and snippets.

@valex
Created October 26, 2020 20:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save valex/7a0e2bff9f62adfcf341d6c7ca2e0d2a to your computer and use it in GitHub Desktop.
Save valex/7a0e2bff9f62adfcf341d6c7ca2e0d2a to your computer and use it in GitHub Desktop.
<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>
function CHART_5_class(id, selector){
this.options = {
selector: selector,
viewBox: [400, 300],
colors: {
black: '#1f2c37',
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", 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 = 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 that.options.colors.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 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;
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-linejoin", "bevel")
.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);
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 =='center')
return;
that.el.selectAll("g.label_value."+data.type.toLowerCase())
.style("opacity", 0);
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 == '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-linejoin", "bevel")
.style("stroke", that.options.colors.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);
#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