Skip to content

Instantly share code, notes, and snippets.

@valex
Created October 16, 2020 22:54
Show Gist options
  • Save valex/708c17b011fea6c03b534b2a68ed5db4 to your computer and use it in GitHub Desktop.
Save valex/708c17b011fea6c03b534b2a68ed5db4 to your computer and use it in GitHub Desktop.
{
"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
}
}
{
"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
}
}
<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://code.jquery.com/jquery-3.5.1.min.js" ></script>
<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, data_file){
this.options = {
selector: selector,
data_file: data_file,
viewBox: [400, 300],
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:[],
axisScales: {},
totalAxes:null,
radius: null,
legendRadius: null,
legend:null,
tooltip:null,
hand:null,
defs:null,
handDef: null
}
};
CHART_4_class.prototype = {
init: function(){
var that = this;
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);
d3.json(this.options.data_file)
.then((data) => {
this.originalData = data;
this.prepareData(this.originalData);
this.buildVisComponents();
that.buildVis();
})
.catch((error) => {
throw error;
});
},
setData: function(data){
this.originalData = data;
this.prepareData(this.originalData);
},
prepareData: function(data){
// calculate max
for(var i=0; i < this.options.groups.length; i++){
var group = this.options.groups[i];
for (var prop in data[group]) {
if( data[group].hasOwnProperty( prop ) ) {
if(typeof data['maxValue'] === 'undefined'){
data['maxValue'] = +data[group][prop];
}else{
if(+data[group][prop] > data['maxValue'])
data['maxValue'] = +data[group][prop];
}
}
}
}
// add 10% to max
// data['maxValue'] = Math.round(1.1 * data['maxValue'] * 100)/100;
for(var i=0; i < this.options.groups.length; i++){
var group = this.options.groups[i];
var j = 0;
for (var prop in data[group]) {
if( data[group].hasOwnProperty( prop ) ) {
data[group][prop] = {
value: data[group][prop],
axis: prop,
}
j++;
}
}
}
for(var i=0; i < this.options.groups.length; i++){
var group = this.options.groups[i];
this.vis.verticesData[group] = [];
for (var prop in data[group]) {
if( data[group].hasOwnProperty( prop ) ) {
var worse = false;
if(i === 1){
if(data[group][prop].value - data[this.options.groups[0]][prop].value < 0){
worse = true;
}
}
this.vis.verticesData[group].push(Object.assign({},{
worse: worse
}, data[group][prop]));
}
}
}
this.data = data;
// 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);
}
}
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';
},
buildMain: function(){
},
//build visualization using the other build helper functions
buildVis: function () {
// this.buildCoordinates(data);
this.buildLevels();
// if (config.showLevelsLabels) buildLevelsLabels();
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", "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 = ['#000000', '#4bc774'];
for(var i=0; i < this.options.groups.length; i++){
var that = this;
var group = this.options.groups[i];
var lines = [];
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([point0, point1]);
}
this.vis.vertices
.selectAll('line.'+group)
.data(lines)
.join(
function(enter){
return enter.append("svg:line")
.classed("polygon-areas", true)
.classed(group, true)
.attr("x1", function(d) { return d[0][0]; })
.attr("y1", function(d) { return d[0][1]; })
.attr("x2", function(d) { return d[1][0]; })
.attr("y2", function(d) { return d[1][1]; })
},
function(update){
return update
.call(function(update){
return update.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr("x1", function(d) { return d[0][0]; })
.attr("y1", function(d) { return d[0][1]; })
.attr("x2", function(d) { return d[1][0]; })
.attr("y2", function(d) { return d[1][1]; });
});
},
function(exit){return exit.remove();}
)
.attr("stroke", function() {
return group_colors[i]}
)
.attr("stroke-width", "1px");
}
},
buildVertices: function(){
var that = this;
var group_colors = ['#000000', '#4bc774'];
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).append("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 'red';
}
return d.color;
})
.style("stroke-width", '1px')
if(d.worse){
that.pulsate(outerCircle);
}
d3.select(this).append("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 'red';
}
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.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)
}
})
.on("mouseenter", function(event, d) {
// that.vis.tooltip
// //.transition()
// //.duration(100)
// .style("opacity", .9);
// that.vis.tooltip.html("<span class=\"g-content\">"+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");
})
.on("mousemove", function(event, d){
// 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");
})
.on("mouseleave", function(event, d) {
//that.vis.tooltip
// .style("opacity", 0);
});
},
function(update){
return update.each(function(d){
d3.select(this).selectAll('circle').remove();
var outerCircle = d3.select(this).append("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 'red';
}
return d.color;
})
.style("stroke-width", '1px')
if(d.worse){
that.pulsate(outerCircle);
}
d3.select(this).append("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 'red';
}
return d.color;
})
.style("opacity", 0.0)
});
},
function(exit){
return exit.remove();
},
)
// .enter()
// .append("svg:g").classed("polygon-vertices", true)
// .each(function(d, fingerIndex){
// var outerCircle = d3.select(this).append("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 'red';
// }
// return group_colors[i]
// })
// .style("stroke-width", '1px')
// if(d.worse){
// that.pulsate(outerCircle);
// }
// d3.select(this).append("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 'red';
// }
// return group_colors[i]
// })
// .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.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)
// }
// })
// .on("mouseenter", function(event, d) {
// // that.vis.tooltip
// // //.transition()
// // //.duration(100)
// // .style("opacity", .9);
// // that.vis.tooltip.html("<span class=\"g-content\">"+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");
// })
// .on("mousemove", function(event, d){
// // 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");
// })
// .on("mouseleave", function(event, d) {
// //that.vis.tooltip
// // .style("opacity", 0);
// });
},
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
.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){
// hide tooltips
if(that.vis.tooltip){
that.vis.tooltip
.style("opacity", 0);
}
// deselect all vertices
that.el.selectAll(".polygon-vertices").classed('active', false);
})
.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',
];
// for left hand
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: 'thumb'
},
{
x1: 18,
y1: 6,
x2: 18,
y2: 32,
hand: 'right',
finger: 'pointing'
},
{
x1: 12,
y1: 0,
x2: 12,
y2: 32,
hand: 'right',
finger: 'middle'
},
{
x1: 6,
y1: 5,
x2: 6,
y2: 32,
hand: 'right',
finger: 'ring'
},
{
x1: 0,
y1: 20,
x2: 0,
y2: 32,
hand: 'right',
finger: 'pinky'
},
]
};
this.vis['defs'] = this.vis['svg'].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 '#4bc774'; // green
else
return "#d7e5ec"
})
.style("stroke-linecap", "round")
.style("stroke-width", "4")
})
// TOOLTIP
this.vis.tooltip = this.el.append("div")
.attr("class", "g-tooltip")
// create levels
this.vis.levels = this.vis.svg
.append("svg:g").classed("levels", true);
// create axes
this.vis['axes'] = this.vis.svg
.append("svg:g").classed("axes", true);
// create vertices
this.vis['vertices'] = this.vis.svg.append("svg:g")
.classed("vertices", true);
//Initiate Legend
this.vis.legend = this.vis.svg.append("svg:g")
.classed("legend", true)
// .attr("height", config.h / 2)
// .attr("width", config.w / 2)
// .attr("transform", "translate(" + 0 + ", " + 1.1 * config.h + ")");
},
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", "#d7e5ec")
.attr("stroke-width", "1px");
for(var i=0; i < this.vis.allAxis.length; i++ ){
this.vis.axisScales[this.vis.allAxis[i]] = {
x:d3.scaleLinear()
.domain([0, this.data['maxValue']])
.range([center[0], x(i)]),
y:d3.scaleLinear()
.domain([0, this.data['maxValue']])
.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", "#d7e5ec")
.attr("stroke-width", "1px");
},
getCenter: function(){
return [this.options.viewBox[0] / 2, this.options.viewBox[1] / 2];
},
onResize: function (){
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 chartBounding = chartEl.node().getBoundingClientRect();
var targetWidth = chartContainerBounding.width;
chartEl.attr("width", targetWidth);
chartEl.attr("height", Math.round(targetWidth / this.aspect));
},
};
var chart_left = new CHART_4_class('#chart', 'chart4.json');
var chart_right = new CHART_4_class('#chart_right', 'chart4_right.json');
chart_left.init();
chart_right.init();
#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;
}
.stop-left {
stop-color: #4bc774; /* Indigo */
}
.stop-right {
stop-color:red;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment