Skip to content

Instantly share code, notes, and snippets.

@valex
Created October 24, 2020 15:12
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/24ae66adfeb09f6bb1063d1c6c78aa6a to your computer and use it in GitHub Desktop.
Save valex/24ae66adfeb09f6bb1063d1c6c78aa6a 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_3_class(id, selector){
this.options = {
selector: selector,
viewBox: [400, 300],
levels: 7,
colors: {
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", "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 '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 '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 '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;
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')
return;
that.el.selectAll("g.label_value")
.style("opacity", 1);
})
.on("mouseleave", function(event, data){
if(data.type =='before')
return;
that.el.selectAll("g.label_value")
.style("opacity", 0);
})
},
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", "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": 89.1
};
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);
#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