Skip to content

Instantly share code, notes, and snippets.

@valex
Created October 15, 2020 19:41
Show Gist options
  • Save valex/ea9cfbf2303928c9079b63eeafdddc1c to your computer and use it in GitHub Desktop.
Save valex/ea9cfbf2303928c9079b63eeafdddc1c 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>
<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.42 // relative radius, max: 0.5
}
this.el = null,
this.aspect = null,
this.vis = {
svg: null,
axes: null,
levels: null,
vertices: null,
verticesData: {},
allAxis:[],
axisScales: {},
totalAxes:null,
radius: null,
legendRadius: null,
hand:null,
defs:null,
handDef: null,
gradient: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) => {
// 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]));
}
}
}
console.log(this.vis.verticesData);
that.buildVis(data);
})
.catch((error) => {
throw error;
});
},
//build visualization using the other build helper functions
buildVis: function (data) {
this.buildVisComponents(data);
// this.buildCoordinates(data);
this.buildLevels();
// if (config.showLevelsLabels) buildLevelsLabels();
this.buildAxes(data);
this.buildAxesLabels();
//this.buildLegend(data);
this.buildPolygons(data);
this.buildVertices(data);
},
buildPolygons: function(data){
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.data(lines).enter()
.append("svg:line").classed("polygon-areas", 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]; })
.attr("stroke", function() {
//return 'url(#gradient)';
return group_colors[i]}
)
.attr("stroke-width", "1px");
}
},
buildVertices: function(data){
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];
if(i === 1){
this.vis.verticesData[group]
}
this.vis.vertices.data(this.vis.verticesData[group])
.enter()
.append("svg:g").classed("polygon-vertices", true)
.each(function(d, fingerIndex){
d3.select(this).append("svg:circle").classed("polygon-vertices", true)
.attr("class", "outer-circle")
.attr("r", 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')
d3.select(this).append("svg:circle")
.attr("class", "inner-circle")
.attr("r", 4)
.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(){
d3.selectAll(".polygon-vertices").classed('active', false);
d3.select(this).classed('active', true);
})
}
},
buildVisComponents: function(data){
// build allAxes
for(var i=0; i < this.options.groups.length; i++){
var group = this.options.groups[i];
for (var prop in 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 = data.hand.indexOf('right') === -1 ? 'left' : 'right';
// 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")
.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: 20,
x2: 0,
y2: 40,
hand: 'left',
finger: 'thumb'
},
{
x1: 6,
y1: 10,
x2: 6,
y2: 40,
hand: 'left',
finger: 'pointing'
},
{
x1: 12,
y1: 0,
x2: 12,
y2: 40,
hand: 'left',
finger: 'middle'
},
{
x1: 18,
y1: 9,
x2: 18,
y2: 40,
hand: 'left',
finger: 'ring'
},
{
x1: 24,
y1: 28,
x2: 24,
y2: 40,
hand: 'left',
finger: 'pinky'
},
],
right: [
{
x1: 24,
y1: 20,
x2: 24,
y2: 40,
hand: 'right',
finger: 'thumb'
},
{
x1: 18,
y1: 10,
x2: 18,
y2: 40,
hand: 'right',
finger: 'pointing'
},
{
x1: 12,
y1: 0,
x2: 12,
y2: 40,
hand: 'right',
finger: 'middle'
},
{
x1: 6,
y1: 9,
x2: 6,
y2: 40,
hand: 'right',
finger: 'ring'
},
{
x1: 0,
y1: 28,
x2: 0,
y2: 40,
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")
})
this.vis['gradient'] = this.vis['defs'].append('linearGradient')
.attr('id', 'gradient');
// Create the stops of the main gradient. Each stop will be assigned
// a class to style the stop using CSS.
this.vis['gradient'].append('stop')
.attr('class', 'stop-left')
.attr('offset', '0');
this.vis['gradient'].append('stop')
.attr('class', 'stop-left')
.attr('offset', '0.2');
this.vis['gradient'].append('stop')
.attr('class', 'stop-right')
.attr('offset', '1');
// create levels
this.vis.levels = this.vis.svg.selectAll(".levels")
.append("svg:g").classed("levels", true);
// create axes
this.vis['axes'] = this.vis.svg.selectAll(".axes")
.append("svg:g").classed("axes", true);
// create vertices
this.vis['vertices'] = this.vis.svg.selectAll(".vertices");
},
buildAxes: function(data){
var that = this;
var center = this.getCenter();
this.vis['axes']
.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", "2px");
for(var i=0; i < this.vis.allAxis.length; i++ ){
this.vis.axisScales[this.vis.allAxis[i]] = {
x:d3.scaleLinear()
.domain([0, data['maxValue']])
.range([center[0], x(i)]),
y:d3.scaleLinear()
.domain([0, 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']
.data(this.vis.allAxis).enter()
.append("g")
.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();
for (var level = 0; level < this.options.levels; level++) {
var levelFactor = this.vis.radius * ((level + 1) / this.options.levels);
// build level-lines
this.vis.levels
.data(this.vis.allAxis).enter()
.append("svg:line").classed("level-lines", true)
.attr("x1", function(d, i) { return levelFactor * (1 - Math.sin(i * 2 * Math.PI / that.vis.totalAxes)); })
.attr("y1", function(d, i) { return levelFactor * (1 - Math.cos(i * 2 * Math.PI / that.vis.totalAxes)); })
.attr("x2", function(d, i) { return levelFactor * (1 - Math.sin((i + 1) * 2 * Math.PI / that.vis.totalAxes)); })
.attr("y2", function(d, i) { return levelFactor * (1 - Math.cos((i + 1) * 2 * Math.PI / that.vis.totalAxes)); })
.attr("transform", "translate(" + (center[0] - levelFactor) + ", " + (center[1] - 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();
console.log(chart_left);
console.log(chart_right);
#chart {
width: 49%;
background-color: #ffffff;
}
#chart_right {
width: 49%;
background-color: #ffffff;
}
.polygon-vertices{
cursor: pointer;
}
.polygon-vertices.active .inner-circle{
opacity: 1 !important;
}
.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