Created
October 22, 2020 21:13
-
-
Save valex/fb9e6211bbd279b94f03387beaa920f4 to your computer and use it in GitHub Desktop.
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
<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 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_2_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.1, | |
padding_left:0.1, | |
} | |
this.id = id, | |
this.el = null, | |
this.aspect = null, | |
this.originalData = null, | |
this.data = null, | |
this.vis = { | |
svg: null, | |
defs: null, | |
background: null, | |
labels: null, | |
value_labels: null, | |
arc_g: null, | |
pies_data: null, | |
pies: null, | |
arc: null, | |
anglesRange: 0.5 * Math.PI, | |
radis: d3.min(this.mainAreaWidthAndHeight()) / 2, | |
thickness: 32 | |
} | |
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_2_class.prototype = { | |
setData: function(data){ | |
this.originalData = Object.assign({}, data); | |
this.data = null; | |
this.prepareData(); | |
}, | |
prepareData: function(){ | |
this.data = Object.assign({}, this.originalData); | |
// build diff | |
this.data['diff'] = this.data['onPeriodEnd'] - this.data['onPeriodStart']; | |
}, | |
makeCalculations: function(){ | |
var that = this; | |
that.vis.pies = d3.pie() | |
.value( function(d){ return d;}) | |
.sort(null) | |
.startAngle( that.vis.anglesRange * -1) | |
.endAngle( that.vis.anglesRange ); | |
that.vis.arc = d3.arc() | |
.outerRadius(that.vis.radis) | |
.innerRadius(that.vis.radis - that.vis.thickness) | |
.padAngle(0.22 * Math.PI / 180); | |
that.vis.pies_data = [ | |
180 - d3.max([that.data['onPeriodStart'], that.data['onPeriodEnd']]), | |
d3.max([that.data['onPeriodStart'], that.data['onPeriodEnd']]) - d3.min([that.data['onPeriodStart'], that.data['onPeriodEnd']]), | |
d3.min([that.data['onPeriodStart'], that.data['onPeriodEnd']]) | |
]; | |
if(that.data.hand == 'right'){ | |
that.vis.pies_data = that.vis.pies_data.reverse(); | |
} | |
}, | |
buildVis: function(){ | |
if( ! this.vis.svg ) this.buildUnchanged(); | |
this.makeCalculations(); | |
this.buildDefs(); | |
this.buildSkeleton(); | |
this.buildArc(); | |
this.buildArrow(); | |
this.buildLabels(); | |
this.buildValueLabels(); | |
}, | |
buildValueLabels:function(){ | |
var that = this; | |
var basicPoint = this.basicPoint(); | |
var pies = that.vis.pies(that.vis.pies_data); | |
var data = []; | |
switch(that.data.hand){ | |
case 'left': | |
data[0] = pies[2]; | |
data[1] = pies[1]; | |
break; | |
case 'right': | |
data[0] = pies[0]; | |
data[1] = pies[1]; | |
break; | |
} | |
this.vis.value_labels | |
.selectAll("g") | |
.data(data) | |
.join('g') | |
.attr('class', function(d, i){ | |
switch(i){ | |
case 0: | |
return 'start'; | |
break; | |
case 1: | |
return 'end'; | |
break; | |
} | |
}) | |
.classed('label_value', true) | |
.style('opacity', '0') | |
.each(function(d, index){ | |
// text | |
var text_bounding; | |
var text = d3.select(this) | |
.selectAll("text") | |
.data([d]) | |
.join("text") | |
.attr("x", function(d,i){ | |
var angle = (d.startAngle + d.endAngle)/2 | |
var x = basicPoint[0] + (that.vis.radis + that.vis.thickness ) * Math.sin(angle); | |
return x; | |
}) | |
.attr("y", function(d){ | |
var angle = (d.startAngle + d.endAngle)/2 | |
var y = basicPoint[1] - (that.vis.radis + that.vis.thickness ) * Math.cos(angle); | |
return y; | |
}) | |
.attr("dx", function(d,i){ return 0}) | |
.attr("dy", 0) | |
.attr("font-size", "1rem") | |
.attr("fill", "black") | |
.attr("text-anchor", "middle") | |
.style("alignment-baseline", "middle") | |
.style("font-weight", "normal") | |
.attr("class", "noselect") | |
.text(function(d) { | |
switch(index){ | |
case 0: | |
return that.data.onPeriodStart+"\u00B0"; | |
break; | |
case 1: | |
return that.data.onPeriodEnd+"\u00B0"; | |
break; | |
} | |
}) | |
.each(function(){ | |
text_bounding = d3.select(this).node().getBBox(); | |
}) | |
//rect | |
var rect_padding = 4; | |
d3.select(this) | |
.selectAll("rect") | |
.data( [ text_bounding ] ) | |
.join("rect") | |
.attr('rx', 6) | |
.attr('ry', 6) | |
.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(d, i){ | |
switch(index){ | |
case 0: | |
return "black"; | |
break; | |
case 1: | |
if(that.isWorse()){ | |
return that.options.colors.worse; | |
} | |
return that.options.colors.better; | |
break; | |
} | |
}) | |
text.raise(); | |
}) | |
}, | |
buildLabels: function(){ | |
var that = this; | |
var basicPoint = this.basicPoint(); | |
var mainAreaWidthAndHeight = this.mainAreaWidthAndHeight(); | |
labelsData = [180,0]; | |
if(that.data.hand === 'right'){ | |
labelsData = labelsData.reverse(); | |
} | |
this.vis.labels | |
.selectAll("text") | |
.data( labelsData ) | |
.enter() | |
.append("text") | |
.attr("x", function(d,i){ return that.paddingRightWidth() + i * mainAreaWidthAndHeight[0]}) | |
.attr("y", function(value){return basicPoint[1]}) | |
.attr("dx", function(d,i){ return 0}) | |
.attr("dy", -10) | |
.attr("font-size", "0.9rem") | |
.attr("fill", that.options.colors.background_text) | |
.attr("text-anchor", "middle") | |
.style("alignment-baseline", "top") | |
.style("font-weight", "normal") | |
.attr("class", "noselect") | |
.text(function(value, i) { | |
return value+"\u00B0"; | |
}) | |
}, | |
buildArc: function(){ | |
var that = this; | |
var basicPoint = this.basicPoint(); | |
var colors = [ | |
that.options.colors.background, | |
that.options.colors.better, | |
'white' | |
]; | |
if(that.data.hand == 'right'){ | |
colors = colors.reverse(); | |
} | |
var strokes = [ | |
that.options.colors.background_border, | |
that.options.colors.better, | |
'black' | |
]; | |
if(that.data.hand == 'right'){ | |
strokes = strokes.reverse(); | |
} | |
if( that.isWorse()){ | |
colors[1] = that.options.colors.worse; | |
strokes[1] = 'black'; | |
} | |
this.vis.arc_g | |
.attr("transform", "translate("+(basicPoint[0])+","+basicPoint[1]+")") | |
.selectAll("path") | |
.data(that.vis.pies(that.vis.pies_data)) | |
.join( | |
function(enter){ | |
return enter.append("svg:path") | |
.style('stroke', function(d, i){ | |
return strokes[i]; | |
}) | |
.style('stroke-width', 1) | |
.attr("fill", function(d, i){ | |
return colors[i]; | |
}) | |
.on("mouseenter", function(event, d){ | |
if(d.index === 1){ | |
that.el.select(".end.label_value") | |
.style("opacity", 1); | |
} | |
if( (d.index === 2 && that.data.hand === 'left')|| | |
(d.index === 0 && that.data.hand === 'right')){ | |
that.el.select(".start.label_value") | |
.style("opacity", 1); | |
} | |
}) | |
.on("mouseleave", function(event, d){ | |
if(d.index === 1){ | |
that.el.select(".end.label_value") | |
.style("opacity", 0); | |
} | |
if( (d.index === 2 && that.data.hand === 'left')|| | |
(d.index === 0 && that.data.hand === 'right')){ | |
that.el.select(".start.label_value") | |
.style("opacity", 0); | |
} | |
}) | |
.each(function(d, i) { | |
this._current = d; | |
}) // store the initial angles | |
.attr("d", function(d){return that.vis.arc(d)}) | |
}, | |
function(update) { | |
return update | |
.call(function(update){ | |
return update | |
.style('stroke', function(d, i){ | |
return strokes[i]; | |
}) | |
.attr("fill", function(d, i){ | |
return colors[i]; | |
}) | |
.transition() | |
.duration(1000) | |
//.ease(d3.easeLinear) | |
.attrTween("d", function (d) { | |
var i = d3.interpolate(this._current, d); | |
this._current = i(0); | |
return function(t) { | |
return that.vis.arc(i(t)); | |
}; | |
}) | |
}); | |
}, | |
function(exit){return exit.remove();} | |
) | |
}, | |
buildSkeleton: 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") | |
}, | |
buildArrow: function(){ | |
var that = this; | |
var color = that.options.colors.better; | |
if( that.isWorse()){ | |
color = that.options.colors.worse; | |
} | |
function arrowPath(data){ | |
var additionalAngle = 6 * Math.PI / 180; | |
var correctiveAngle = 0.66 * Math.PI / 180; | |
var basicPoint = that.basicPoint(); | |
var pie = data; | |
var basicAngle = pie.startAngle; | |
if( that.isWorse()){ | |
basicAngle = pie.endAngle; | |
additionalAngle = -additionalAngle ; | |
} | |
if( that.data.hand=='right' ){ | |
basicAngle = pie.endAngle; | |
additionalAngle = -additionalAngle ; | |
correctiveAngle = -correctiveAngle; | |
if( that.isWorse()){ | |
basicAngle = pie.startAngle/* + (0.22*Math.PI / 180)*/; | |
} | |
} | |
var x1Corr = basicPoint[0] + (that.vis.radis - that.vis.thickness + 1) * Math.sin(basicAngle - correctiveAngle); | |
var y1Corr = basicPoint[1] - (that.vis.radis - that.vis.thickness + 1) * Math.cos(basicAngle - correctiveAngle); | |
var x2Corr = basicPoint[0] + (that.vis.radis -1) * Math.sin(basicAngle - correctiveAngle); | |
var y2Corr = basicPoint[1] - (that.vis.radis -1) * Math.cos(basicAngle - correctiveAngle); | |
var x3Corr = basicPoint[0] + (that.vis.radis - that.vis.thickness +1) * Math.sin(basicAngle + correctiveAngle); | |
var y3Corr = basicPoint[1] - (that.vis.radis - that.vis.thickness +1) * Math.cos(basicAngle + correctiveAngle); | |
var x4Corr = basicPoint[0] + (that.vis.radis -1) * Math.sin(basicAngle + correctiveAngle); | |
var y4Corr = basicPoint[1] - (that.vis.radis -1) * Math.cos(basicAngle + correctiveAngle); | |
var x1 = basicPoint[0] + (that.vis.radis - that.vis.thickness) * Math.sin(basicAngle); | |
var y1 = basicPoint[1] - (that.vis.radis - that.vis.thickness) * Math.cos(basicAngle); | |
var x2 = basicPoint[0] + that.vis.radis * Math.sin(basicAngle); | |
var y2 = basicPoint[1] - that.vis.radis * Math.cos(basicAngle); | |
var x3 = basicPoint[0] + (that.vis.radis - that.vis.thickness / 2) * Math.sin( -additionalAngle + basicAngle ); | |
var y3 = basicPoint[1] - (that.vis.radis - that.vis.thickness / 2) * Math.cos( -additionalAngle + basicAngle ); | |
var path = d3.path(); | |
if( that.isWorse()){ | |
path.moveTo(x1Corr, y1Corr) | |
path.lineTo(x3Corr, y3Corr) | |
path.lineTo(x3, y3) | |
path.lineTo(x4Corr, y4Corr) | |
path.lineTo(x2Corr, y2Corr) | |
path.closePath(); | |
}else{ | |
path.moveTo(x1, y1) | |
path.lineTo(x2, y2) | |
path.lineTo(x3, y3) | |
path.closePath(); | |
} | |
return path; | |
} | |
var pie_data = (that.vis.pies(that.vis.pies_data))[1]; | |
this.vis.svg.selectAll('path.arrow') | |
.data([pie_data]) | |
.join( | |
function(enter){ | |
return enter.append("svg:path") | |
.classed('arrow', true) | |
.style('stroke', color) | |
.style("stroke-width", 1) | |
.style("fill", color) | |
.style("stroke-linejoin", "bevel") | |
.attr('d', function(d){return arrowPath(d)}) | |
.attr("clip-path",function(d){ | |
return "url(#"+that.id+"-clip-arrows)" | |
}) | |
.on("mouseenter", function(){ | |
that.el.select(".end.label_value") | |
.style("opacity", 1); | |
}) | |
.on("mouseleave", function(){ | |
that.el.select(".end.label_value") | |
.style("opacity", 0); | |
}) | |
.each(function(d) { | |
this._current = d; | |
}) // store the initial angles | |
}, | |
function(update) { | |
return update | |
.call(function(update){ | |
return update | |
.style('stroke', color) | |
.style("fill", color) | |
.transition() | |
.duration(1000) | |
//.ease(d3.easeLinear) | |
.attrTween("d", function (d) { | |
// Store the displayed angles in _current. | |
// Then, interpolate from _current to the new angles. | |
// During the transition, _current is updated in-place by d3.interpolate. | |
var i = d3.interpolate(this._current, d); | |
this._current = i(0); | |
return function(t) { | |
return arrowPath(i(t)); | |
}; | |
}) | |
}); | |
}, | |
function(exit){return exit.remove();} | |
) | |
}, | |
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"); | |
var basicPoint = this.basicPoint(); | |
var mainAreaWidthAndHeight = this.mainAreaWidthAndHeight(); | |
this.vis['defs'] | |
.append("clipPath") | |
.attr("id", function(d){ return that.id+"-clip-arrows"}) | |
.append("rect") | |
.attr("x",that.paddingLeftWidth()) | |
.attr("y", basicPoint[1] - mainAreaWidthAndHeight[1]) | |
.attr("width", mainAreaWidthAndHeight[0]) | |
.attr("height", mainAreaWidthAndHeight[1]) | |
this.vis.background = this.vis['svg'] | |
.append("svg:g") | |
.classed('background', true); | |
this.vis.labels = this.vis['svg'] | |
.append("svg:g") | |
.classed('labels', true); | |
this.vis.value_labels = this.vis['svg'] | |
.append("svg:g") | |
.classed('value_labels', true); | |
this.vis.arc_g = this.vis['svg'] | |
.append("svg:g") | |
.classed('arc', true); | |
}, | |
mainAreaWidthAndHeight: function(){ | |
var width = this.options.viewBox[0] - this.paddingLeftWidth() - this.paddingRightWidth(); | |
var height = this.options.viewBox[1] - this.paddingBottomHeight(); | |
return [width, height]; | |
}, | |
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]; | |
}, | |
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 | |
]; | |
}, | |
isWorse: function(){ | |
return this.data['onPeriodEnd'] - this.data['onPeriodStart'] < 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": 50, | |
"onPeriodEnd": 40.4 | |
}; | |
var chart_left = new CHART_2_class('id1', '#chart'); | |
chart_left.setData(id1_data); | |
chart_left.buildVis(); | |
console.log(chart_left); | |
var id2_data = { | |
"hand": "right", | |
"onPeriodStart": 180, | |
"onPeriodEnd": 160 | |
}; | |
var chart_right = new CHART_2_class('id2', '#chart_right'); | |
chart_right.setData(id2_data); | |
chart_right.buildVis(); | |
var newData = { | |
"hand": "left", | |
"onPeriodStart": 110, | |
"onPeriodEnd": 150 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData); | |
chart_left.buildVis(); | |
}, 5000); | |
var newDataRight = { | |
"hand": "right", | |
"onPeriodStart": 20, | |
"onPeriodEnd": 120 | |
}; | |
setTimeout(function(){ | |
chart_right.setData(newDataRight); | |
chart_right.buildVis(); | |
}, 5000); | |
var newData2 = { | |
"hand": "left", | |
"onPeriodStart": 80, | |
"onPeriodEnd": 40 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData2); | |
chart_left.buildVis(); | |
}, 11000); | |
var newDataRight2 = { | |
"hand": "right", | |
"onPeriodStart": 40, | |
"onPeriodEnd": 140 | |
}; | |
setTimeout(function(){ | |
chart_right.setData(newDataRight2); | |
chart_right.buildVis(); | |
}, 11000); | |
var newData3 = { | |
"hand": "left", | |
"onPeriodStart": 50, | |
"onPeriodEnd": 2 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData3); | |
chart_left.buildVis(); | |
}, 17000); | |
var newDataRight3 = { | |
"hand": "right", | |
"onPeriodStart": 120, | |
"onPeriodEnd": 70 | |
}; | |
setTimeout(function(){ | |
chart_right.setData(newDataRight3); | |
chart_right.buildVis(); | |
}, 17000); | |
var newData4 = { | |
"hand": "left", | |
"onPeriodStart": 20, | |
"onPeriodEnd": 40 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData4); | |
chart_left.buildVis(); | |
}, 23000); | |
var newData5 = { | |
"hand": "left", | |
"onPeriodStart": 60, | |
"onPeriodEnd": 120 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData5); | |
chart_left.buildVis(); | |
}, 29000); | |
var newData6 = { | |
"hand": "left", | |
"onPeriodStart": 95, | |
"onPeriodEnd": 130 | |
}; | |
setTimeout(function(){ | |
chart_left.setData(newData6); | |
chart_left.buildVis(); | |
}, 35000); |
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
#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