Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@valex
Created October 26, 2020 20:46
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/025263a7e5cab842d67cdfd0968d2488 to your computer and use it in GitHub Desktop.
Save valex/025263a7e5cab842d67cdfd0968d2488 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_2_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.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", that.options.colors.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 that.options.colors.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,
that.options.colors.black
];
if(that.data.hand == 'right'){
strokes = strokes.reverse();
}
if( that.isWorse()){
colors[1] = that.options.colors.worse;
strokes[1] = that.options.colors.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);
d3.select(this)
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
that.el.select(".arrow")
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
}
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);
d3.select(this)
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
that.el.select(".arrow")
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
}
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
.style('stroke', function(d, i){
return strokes[i];
})
.attr("fill", function(d, i){
return colors[i];
})
.call(function(update){
return update
.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 lowData = false;
if(correctiveAngle > data.endAngle - data.startAngle){
lowData = true;
}
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(data.value == 0){
return path;
}
if( that.isWorse()){
if(lowData){
path.moveTo(x3Corr, y3Corr)
path.lineTo(x3, y3)
path.lineTo(x4Corr, y4Corr)
path.closePath();
}else{
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)
.attr("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);
d3.select(this)
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
that.el.select(".arc path:nth-child(2)")
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse_hover;
}
return that.options.colors.better_hover;
})
})
.on("mouseleave", function(){
that.el.select(".end.label_value")
.style("opacity", 0);
d3.select(this)
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
that.el.select(".arc path:nth-child(2)")
.style('stroke', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
.attr('fill', function(){
if( that.isWorse()){
return that.options.colors.worse;
}
return that.options.colors.better;
})
})
.each(function(d) {
this._current = d;
}) // store the initial angles
},
function(update) {
return update
.call(function(update){
return update
.style('stroke', color)
.attr("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": 50
};
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": 50,
"onPeriodEnd": 50
};
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);
#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