Skip to content

Instantly share code, notes, and snippets.

@valex
Created October 26, 2020 20:51
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/9d6da4a145725707a0ffff069d69164d to your computer and use it in GitHub Desktop.
Save valex/9d6da4a145725707a0ffff069d69164d 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;">
<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>
// http://bl.ocks.org/chrisrzhou/2421ac6541b68c1680f8
function CHART_4_class(selector){
this.options = {
selector: selector,
viewBox: [400, 300],
colors: {
black: '#1f2c37',
background: '#d7e5ec',
before: '#1f2c37',
after: '#4bc774',
worse: 'red'
},
axesDomains: {
},
groups: ['onPeriodStart', 'onPeriodEnd'],
levels: 3,
radius: 0.28, // relative radius, max: 0.5
legendRadius: 0.37 // relative radius, max: 0.5
}
this.el = null,
this.aspect = null,
this.originalData = null,
this.data = null,
this.vis = {
svg: null,
axes: null,
levels: null,
vertices: null,
verticesData: {},
allAxis:["middle", "ring", "pinky", "thumb", "pointing"],
axisScales: {},
totalAxes:null,
radius: null,
legendRadius: null,
legend:null,
tooltip:null,
hand:null,
defs:null,
handDef: 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_4_class.prototype = {
setData: function(data){
this.originalData = Object.assign({}, data);
this.data = null;
this.prepareData();
},
prepareData: function(){
var that = this;
this.data = Object.assign({}, this.originalData);
// calculate max
for(var i=0; i < this.options.groups.length; i++){
var group = this.options.groups[i];
for (var prop in this.data[group]) {
if( this.data[group].hasOwnProperty( prop ) ) {
if(typeof this.data['maxValue'] === 'undefined'){
this.data['maxValue'] = +this.data[group][prop];
}else{
if(+this.data[group][prop] > this.data['maxValue'])
this.data['maxValue'] = +this.data[group][prop];
}
}
}
}
// add 10% to max
// this.data['maxValue'] = Math.round(1.1 * this.data['maxValue'] * 100)/100;
for(var i=0; i < this.options.groups.length; i++){
var group = this.options.groups[i];
var groupData = {};
for (var prop in this.originalData[group]) {
if( this.originalData[group].hasOwnProperty( prop ) ) {
var value = this.originalData[group][prop];
groupData[prop] = {
value: value,
axis: prop,
};
}
}
that.data[group] = Object.assign({}, groupData);
}
for(var i=0; i < this.options.groups.length; i++){
var group = this.options.groups[i];
this.vis.verticesData[group] = [];
for (var prop in this.data[group]) {
if( this.data[group].hasOwnProperty( prop ) ) {
var worse = false;
if(i === 1){
if(this.data[group][prop].value - this.data[this.options.groups[0]][prop].value < 0){
worse = true;
}
}
this.vis.verticesData[group].push(Object.assign({},{
worse: worse
}, this.data[group][prop]));
}
}
}
// build allAxes
// for(var i=0; i < this.options.groups.length; i++){
// var group = this.options.groups[i];
// for (var prop in this.data[group]) {
// if(this.vis.allAxis.indexOf(prop) === -1 )
// this.vis.allAxis.push(prop);
// }
// }
// console.log(this.vis.allAxis);
this.vis.totalAxes = this.vis.allAxis.length;
this.vis.radius = d3.min([this.options.radius * this.options.viewBox[0], this.options.radius * this.options.viewBox[1]]);
this.vis.legendRadius = d3.min([this.options.legendRadius * this.options.viewBox[0], this.options.legendRadius * this.options.viewBox[1]]);
this.vis.hand = this.data.hand.indexOf('right') === -1 ? 'left' : 'right';
},
//build visualization using the other build helper functions
buildVis: function () {
if( ! this.vis.svg ) this.buildVisComponents();
this.buildLevels();
this.buildAxes();
this.buildAxesLabels();
this.buildLegend();
this.buildPolygons();
this.buildVertices();
},
buildLegend: function(){
var that = this;
var center = that.getCenter();
var label = this.vis.legend
.selectAll('text')
.data([that.data.hand])
.join("text")
.attr("x", function(){return center[0];})
.attr("y", function(){return center[1] + 1.1 * that.vis.radius;})
.attr("font-size", "1rem")
.attr("fill", that.options.colors.black)
.attr("text-anchor", "middle")
.attr("class", "noselect")
.text(function(d) {
switch(d){
case 'left':
return "Левая рука"
break;
default:
return "Правая рука"
break;
}
});
},
buildPolygons: function(){
var that = this;
var group_colors = [that.options.colors.before, that.options.colors.after];
var lines = [];
for(var i=0; i < this.options.groups.length; i++){
var that = this;
var group = this.options.groups[i];
for (var j=0; j < this.vis.verticesData[group].length; j++){
var vertex0 = this.vis.verticesData[group][j];
var point0 = [
that.vis.axisScales[vertex0.axis].x(vertex0.value),
that.vis.axisScales[vertex0.axis].y(vertex0.value)
];
var vertex1 = this.vis.verticesData[group][0];
if(typeof this.vis.verticesData[group][j+1] !== 'undefined'){
vertex1 = this.vis.verticesData[group][j+1];
}
var point1 = [
that.vis.axisScales[vertex1.axis].x(vertex1.value),
that.vis.axisScales[vertex1.axis].y(vertex1.value)
];
lines.push({
x1: point0[0],
y1: point0[1],
x2: point1[0],
y2: point1[1],
color: group_colors[i]
});
}
}
this.vis.vertices
.selectAll('line')
.data(lines)
.join(
function(enter){
return enter.append("svg:line")
.classed("polygon-areas", true)
.attr("x1", function(d) { return d.x1; })
.attr("y1", function(d) { return d.y1; })
.attr("x2", function(d) { return d.x2; })
.attr("y2", function(d) { return d.y2; })
},
function(update){
return update
.call(function(update){
return update.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr("x1", function(d) { return d.x1; })
.attr("y1", function(d) { return d.y1; })
.attr("x2", function(d) { return d.x2; })
.attr("y2", function(d) { return d.y2; })
});
},
function(exit){return exit.remove();}
)
.attr("stroke", function(d) {
return d.color}
)
.attr("stroke-width", "1px");
},
buildVertices: function(){
var that = this;
var group_colors = [that.options.colors.before, that.options.colors.after];
var all_vertices = [];
for(var i=0; i < this.options.groups.length; i++){
var that = this;
var group = this.options.groups[i];
var color = group_colors[i];
for(var j=0; j<this.vis.verticesData[group].length; j++){
all_vertices.push(Object.assign({}, {
color: color
}, this.vis.verticesData[group][j]));
}
}// for
this.vis.vertices.selectAll('g.polygon-vertices')
.data(all_vertices)
.join(
function(enter){
return enter.append("svg:g")
.classed("polygon-vertices ", true)
.each(function(d, fingerIndex){
var outerCircle = d3.select(this)
.selectAll('circle.outer-circle')
.data([d])
.join("svg:circle")
.attr("class", "outer-circle")
.attr("r", 4.6)
.attr("cx", function(d) { return that.vis.axisScales[d.axis].x(d.value); })
.attr("cy", function(d) { return that.vis.axisScales[d.axis].y(d.value); })
.attr("fill", 'white')
.style("stroke", function(d){
if(d.worse){
return that.options.colors.worse;
}
return d.color;
})
.style("stroke-width", '1px')
.style("stroke-opacity", '1')
if(d.worse){
// that.pulsate(outerCircle);
}
d3.select(this)
.selectAll('circle.inner-circle')
.data([d])
.join("svg:circle")
.attr("class", "inner-circle")
.attr("r", 3.2)
.attr("cx", function(d) { return that.vis.axisScales[d.axis].x(d.value); })
.attr("cy", function(d) { return that.vis.axisScales[d.axis].y(d.value); })
.attr("fill", function(d){
if(d.worse){
return that.options.colors.worse;
}
return d.color;
})
.style("opacity", 0.0)
})
.on("click", function(event, d){
event.stopPropagation();
var clicked = d3.select(this);
// if click on active - hide tooltip
if(clicked.classed('active')){
if( +that.vis.tooltip.style("opacity") > 0){
that.vis.tooltip
.style("opacity", 0);
}
}else{
that.vis.tooltip
.style("opacity", .9);
that.vis.tooltip
.style("border-color", d.color);
if(d.worse){
that.vis.tooltip
.style("border-color", that.options.colors.worse);
}
that.vis.tooltip.html("<span class=\"g-content noselect\">"+d.value+"&deg;</span>")
.style("left", (event.pageX) + "px")
.style("top", (event.pageY) + "px");
var tooltipRect = that.vis.tooltip.node().getBoundingClientRect();
that.vis.tooltip
.style("left", (event.pageX - tooltipRect.width / 2) + "px")
.style("top", (event.pageY - 1.4 * tooltipRect.height) + "px");
}
if(clicked.classed('active')){
clicked.classed('active', false);
}else{
that.el.selectAll(".polygon-vertices").classed('active', false);
clicked.classed('active', true)
}
});
},
function(update){
return update.each(function(d){
// deselect all vertices
that.deselectAllAndHide();
var outerCircle = d3.select(this)
.selectAll('circle.outer-circle')
.data([d])
.join("svg:circle")
.call(function(selection){
return selection
.interrupt()
.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr("r", 4.6)
.attr("cx", function(d) { return that.vis.axisScales[d.axis].x(d.value); })
.attr("cy", function(d) { return that.vis.axisScales[d.axis].y(d.value); })
.attr("fill", 'white')
.style("stroke", function(d){
if(d.worse){
return that.options.colors.worse;
}
return d.color;
})
.style("stroke-width", '1px')
.style("stroke-opacity", '1')
.on("end", function(){
if(d.worse){
// that.pulsate(outerCircle);
}
})
})
d3.select(this)
.selectAll('circle.inner-circle')
.data([d])
.join("svg:circle")
.call(function(selection){
return selection.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr("r", 3.2)
.attr("cx", function(d) { return that.vis.axisScales[d.axis].x(d.value); })
.attr("cy", function(d) { return that.vis.axisScales[d.axis].y(d.value); })
.attr("fill", function(d){
if(d.worse){
return that.options.colors.worse;
}
return d.color;
})
.style("opacity", 0.0)
})
});
},
function(exit){
return exit.remove();
},
)
},
deselectAllAndHide: function(){
var that = this;
if(that.vis.tooltip){
that.vis.tooltip
.style("opacity", 0);
}
// deselect all vertices
that.el.selectAll(".polygon-vertices").classed('active', false);
},
pulsate: function(selection){
var that = this;
recursive_transitions();
function recursive_transitions() {
selection.transition()
.duration(1000)
.ease(d3.easeLinear)
.style('stroke-width', '3px')
.style('stroke-opacity', '0.4')
.attr("r", 5.4)
.transition()
.duration(1000)
.ease(d3.easeLinear)
.style('stroke-width', '1px')
.style('stroke-opacity', '1')
.attr("r", 4.6)
.on("end", recursive_transitions);
}
},
buildVisComponents: function(){
var that = this;
// create main vis svg
this.vis['svg'] = this.el.selectAll('svg')
.data(["one"])
.enter()
.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")
// hand def
var active_fingers = [
'left-thumb',
'left-pointing',
'left-middle',
'left-ring',
'left-pinky',
'right-thumb',
'right-pointing',
'right-middle',
'right-ring',
'right-pinky',
];
var fingers = {
left: [
{
x1: 0,
y1: 14,
x2: 0,
y2: 32,
hand: 'left',
finger: 'thumb'
},
{
x1: 6,
y1: 6,
x2: 6,
y2: 32,
hand: 'left',
finger: 'pointing'
},
{
x1: 12,
y1: 0,
x2: 12,
y2: 32,
hand: 'left',
finger: 'middle'
},
{
x1: 18,
y1: 5,
x2: 18,
y2: 32,
hand: 'left',
finger: 'ring'
},
{
x1: 24,
y1: 20,
x2: 24,
y2: 32,
hand: 'left',
finger: 'pinky'
},
],
right: [
{
x1: 24,
y1: 14,
x2: 24,
y2: 32,
hand: 'right',
finger: 'pinky'
},
{
x1: 18,
y1: 6,
x2: 18,
y2: 32,
hand: 'right',
finger: 'ring'
},
{
x1: 12,
y1: 0,
x2: 12,
y2: 32,
hand: 'right',
finger: 'middle'
},
{
x1: 6,
y1: 5,
x2: 6,
y2: 32,
hand: 'right',
finger: 'pointing'
},
{
x1: 0,
y1: 20,
x2: 0,
y2: 32,
hand: 'right',
finger: 'thumb'
},
]
};
this.vis['defs'] = this.vis['svg'].selectAll("defs")
.data(["one"])
.enter()
.append("defs")
this.vis['handDef'] = this.vis['defs'].selectAll('g')
.data(active_fingers)
.enter()
.append('g')
.attr("id", function(d,i){return 'hand-'+d})
.each(function(active_finger,i){
d3.select(this).selectAll("line").data(function(){
if(active_finger.indexOf('left') !== -1)
return fingers.left;
else
return fingers.right;
}).enter()
.append("line")
.attr("x1", function(d){return d.x1})
.attr("y1", function(d){return d.y1})
.attr("x2", function(d){return d.x2})
.attr("y2", function(d){return d.y2})
.style("stroke", function(d, i){
if(active_finger == d.hand+'-'+d.finger)
return that.options.colors.after;
else
return that.options.colors.background
})
.style("stroke-linecap", "round")
.style("stroke-width", "4")
})
// TOOLTIP
this.vis.tooltip = this.el.selectAll("div.g-tooltip")
.data(["one"])
.enter().append("div")
.attr("class", "g-tooltip")
// create levels
this.vis.levels = this.vis.svg.selectAll("g.levels")
.data(["one"])
.enter()
.append("svg:g").classed("levels", true);
// create axes
this.vis['axes'] = this.vis.svg.selectAll("g.axes")
.data(["one"])
.enter()
.append("svg:g").classed("axes", true);
// create vertices
this.vis['vertices'] = this.vis.svg.selectAll("g.vertices")
.data(["one"])
.enter()
.append("svg:g")
.classed("vertices", true);
//Initiate Legend
this.vis.legend = this.vis.svg.selectAll("g.legend")
.data(["one"])
.enter()
.append("svg:g")
.classed("legend", true)
},
buildAxes: function(){
var that = this;
var center = this.getCenter();
this.vis['axes'].selectAll("line")
.data(this.vis.allAxis).enter()
.append("svg:line").classed("axis-lines", true)
.attr("x1", center[0])
.attr("y1", center[1])
.attr("x2", function(d, i) { return x(i); })
.attr("y2", function(d, i) { return y(i); })
.attr("stroke", that.options.colors.background)
.attr("stroke-width", "1px");
for(var i=0; i < this.vis.allAxis.length; i++ ){
var domain = [0, this.data['maxValue']];
if( typeof this.options.axesDomains[this.vis.allAxis[i]] !== 'undefined' &&
this.options.axesDomains[this.vis.allAxis[i]] !== null )
{
domain = this.options.axesDomains[this.vis.allAxis[i]];
}
this.vis.axisScales[this.vis.allAxis[i]] = {
x: d3.scaleLinear()
.domain(domain)
.range([center[0], x(i)]),
y: d3.scaleLinear()
.domain(domain)
.range([center[1], y(i)]),
};
}
function x(i){
return center[0] + that.vis.radius * Math.sin(i * 2 * Math.PI / that.vis.totalAxes);
}
function y(i){
return center[1] + that.vis.radius * -Math.cos(i * 2 * Math.PI / that.vis.totalAxes);
}
},
buildAxesLabels: function(){
var that = this;
var center = this.getCenter();
var boundings = this.vis['handDef'].node().getBBox();
this.vis['axes'].selectAll('g.label').remove();
this.vis['axes'].selectAll('g.label')
.data(this.vis.allAxis)
.enter()
.append("g")
.classed('label', true)
.attr("transform",function(d,i) {
var x = center[0] + that.vis.legendRadius * Math.sin(i * 2 * Math.PI / that.vis.totalAxes);
x -= boundings.width / 2;
var y = center[1] + that.vis.legendRadius * -Math.cos(i * 2 * Math.PI / that.vis.totalAxes)
y -= boundings.height / 2;
return "translate("+x+","+y+")";
})
.append("use")
.attr("xlink:href",function(d,i){return "#hand-"+that.vis.hand+"-"+d})
},
buildLevels: function(){
var that = this;
var center = this.getCenter();
this.vis.levels.selectAll('line').remove();
var data = [];
for (var level = 0; level < this.options.levels; level++) {
var levelFactor = this.vis.radius * ((level + 1) / this.options.levels);
for (var j=0; j < this.vis.allAxis.length; j++){
data.push({
levelFactor: levelFactor,
index: j
});
}
}
// build level-lines
this.vis.levels.selectAll('line')
.data(data).enter()
.append("svg:line").classed("level-lines", true)
.attr("x1", function(d, i) { return d.levelFactor * (1 - Math.sin(d.index * 2 * Math.PI / that.vis.totalAxes)); })
.attr("y1", function(d, i) { return d.levelFactor * (1 - Math.cos(d.index * 2 * Math.PI / that.vis.totalAxes)); })
.attr("x2", function(d, i) { return d.levelFactor * (1 - Math.sin((d.index + 1) * 2 * Math.PI / that.vis.totalAxes)); })
.attr("y2", function(d, i) { return d.levelFactor * (1 - Math.cos((d.index + 1) * 2 * Math.PI / that.vis.totalAxes)); })
.attr("transform", function(d){return "translate(" + (center[0] - d.levelFactor) + ", " + (center[1] - d.levelFactor) + ")" })
.attr("stroke", that.options.colors.background)
.attr("stroke-width", "1px");
},
getCenter: function(){
return [this.options.viewBox[0] / 2, this.options.viewBox[1] / 2];
},
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 data = {
"hand": "left",
"onPeriodStart": {
"thumb": 31.8,
"pointing": 15.4,
"middle": 25.4,
"ring": 10.4,
"pinky": 15.4
},
"onPeriodEnd": {
"thumb": 45.6,
"pointing": 22.4,
"middle": 15.3,
"ring": 17.4,
"pinky": 11.1
}
};
var chart_left = new CHART_4_class('#chart');
chart_left.setData(data);
chart_left.buildVis();
var data_right = {
"hand": "right",
"onPeriodStart": {
"thumb": 31.8,
"pointing": 15.4,
"middle": 0.4,
"ring": 10.4,
"pinky": 15.4
},
"onPeriodEnd": {
"thumb": 45.6,
"pointing": 22.4,
"middle": 0.3,
"ring": 12.4,
"pinky": 11.1
}
}
;
var chart_right = new CHART_4_class('#chart_right');
chart_right.options.axesDomains = {
"thumb": [0, 50],
"pointing": [0, 25],
"middle": [0, 0.5],
"ring": [8, 14],
"pinky": [0, 16],
};
chart_right.setData(data_right);
chart_right.buildVis();
var newData = {
"hand": "left",
"onPeriodStart": {
"thumb": 15.8,
"pointing": 5.4,
"middle": 17.4,
"ring": 6.4,
"pinky": 11.4
},
"onPeriodEnd": {
"thumb": 21.6,
"pointing": 14.4,
"middle": 12.3,
"ring": 16.4,
"pinky": 18.1
}
}
setTimeout(function(){
chart_left.setData(newData);
chart_left.buildVis();
}, 5000);
var newData2 = {
"hand": "left",
"onPeriodStart": {
"thumb": 34.8,
"pointing": 32.4,
"middle": 30.4,
"ring": 28.4,
"pinky": 26.4
},
"onPeriodEnd": {
"thumb": 24.6,
"pointing": 28.4,
"middle": 25.3,
"ring": 22.4,
"pinky": 18.1
}
}
setTimeout(function(){
chart_left.setData(newData2);
chart_left.buildVis();
}, 12000)
var newData3 = {
"hand": "left",
"onPeriodStart": {
"thumb": 24.6,
"pointing": 28.4,
"middle": 25.3,
"ring": 22.4,
"pinky": 18.1
},
"onPeriodEnd": {
"thumb": 34.8,
"pointing": 32.4,
"middle": 30.4,
"ring": 28.4,
"pinky": 26.4
}
}
setTimeout(function(){
chart_left.setData(newData3);
chart_left.buildVis();
}, 19000)
var newData4 = {
"hand": "left",
"onPeriodStart": {
"thumb": 12.6,
"pointing": 11.4,
"middle": 10.3,
"ring": 9.4,
"pinky": 8.1
},
"onPeriodEnd": {
"thumb": 8.8,
"pointing": 3.4,
"middle": 6.4,
"ring": 2.4,
"pinky": 3.4
}
}
setTimeout(function(){
chart_left.setData(newData4);
chart_left.buildVis();
}, 26000)
#chart {
width: 49%;
background-color: #ffffff;
}
#chart_right {
width: 49%;
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 */
}
.polygon-vertices{
cursor: pointer;
}
.polygon-vertices.active .inner-circle{
opacity: 1 !important;
}
.g-tooltip {
position: absolute;
text-align: center;
padding: 4px 8px;
font: 12px sans-serif;
background: white;
border: 2px solid #4bc774;
border-radius: 4px;
pointer-events: none;
opacity: 0;
}
.g-tooltip .g-content {
font-weight: bold;
font-size: 1.2rem;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment