Built with blockbuilder.org
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="data.js"></script>
<!--<script src="data2.js"></script>-->
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<script>
const radius = states.length * 8;
const dimensions = (radius * 2) + 50;
const candidateRadius = (((Math.PI * (radius * radius)) / candidates.length) / radius) + 8;
const stateRadius = 20;
let activeState = null;
let activeCandidate = null;
const t = d3.transition().duration(1000);
const svg = d3.select('body')
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('xml:lang', 'he')
// .attr("viewBox", '0 0 200 200')
.attr('style', 'font-family: "Open Sans Hebrew","Helvetica Neue",Helvetica,Arial,sans-serif; overflow: visible;');
const statesNodes = states.map((state, index) => {
const angle = (index / (states.length / 2)) * Math.PI; // Calculate the angle at which the element will be placed.
state.x = (radius * Math.sin(angle)) + (dimensions / 2); // Calculate the x position of the element.
state.y = (radius * Math.cos(angle)) + (dimensions / 2); // Calculate the y position of the element.
return state;
});
const candidatesNodes = candidates
.reduce((candidates, candidate)=> {
const positionMap = { '-2': 0, '-1': 1, 0: 2, 1: 3, 2: 4, };
const arrayPosition = positionMap[candidate.position];
candidates[arrayPosition].push(candidate);
return candidates;
}, [[],[],[],[],[],])
.reduce((result, candidatesOfType) => {
candidatesOfType
.forEach((candidate, index) => {
if (candidate.position === 0) {
const angle = (index / (candidatesOfType.length / 2)) * Math.PI;
candidate.x = (radius / 5 * Math.cos(angle)) + (dimensions / 2);
candidate.y = (radius / 5 * Math.sin(angle)) + (dimensions / 2);
}
if (candidate.position === 1) {
const angle = (index / (candidatesOfType.length)) * Math.PI;
candidate.x = (radius / 2 * Math.sin(angle)) + (dimensions / 2) + candidateRadius;
candidate.y = (radius / 2 * Math.cos(angle)) + (dimensions / 2);
}
if (candidate.position === 2) {
const angle = (index / (candidatesOfType.length)) * Math.PI;
candidate.x = (radius / 1.25 * Math.sin(angle)) + (dimensions / 2) + candidateRadius;
candidate.y = (radius / 1.25 * Math.cos(angle)) + (dimensions / 2);
}
if (candidate.position === -1) {
const angle = (index / (candidatesOfType.length * -1)) * Math.PI;
candidate.x = (radius / 2 * Math.sin(angle)) + (dimensions / 2) - candidateRadius;
candidate.y = (radius / 2 * Math.cos(angle)) + (dimensions / 2);
}
if (candidate.position === -2) {
const angle = (index / (candidatesOfType.length * -1)) * Math.PI;
candidate.x = (radius / 1.25 * Math.sin(angle)) + (dimensions / 2) - candidateRadius;
candidate.y = (radius / 1.25 * Math.cos(angle)) + (dimensions / 2);
}
result.push(candidate);
});
return result;
}, []);
const toggleTooltip = d => {
if (!activeCandidate) {
const info = svg.selectAll('.info').filter(`.${d.name.split(' ')[0]}-${d.state}`);
const name = svg.selectAll('.name').filter(`.${d.name.split(' ')[0]}-${d.state}`);
name
.transition(t)
.attr('opacity', 0);
info
.transition(t)
.attr('opacity', 1);
activeCandidate = d;
}
else {
if (!d || activeCandidate.name === d.name) {
const info = svg.selectAll('.info')
.filter(`.${activeCandidate.name.split(' ')[0]}-${activeCandidate.state}`);
const name = svg.selectAll('.name')
.filter(`.${activeCandidate.name.split(' ')[0]}-${activeCandidate.state}`);
name
.transition(t)
.attr('opacity', 1);
info
.transition(t)
.attr('opacity', 0);
activeCandidate = null;
}
else {
const prevInfo = svg.selectAll('.info')
.filter(`.${activeCandidate.name.split(' ')[0]}-${activeCandidate.state}`);
const info = svg.selectAll('.info').filter(`.${d.name.split(' ')[0]}-${d.state}`);
const prevName = svg.selectAll('.name')
.filter(`.${activeCandidate.name.split(' ')[0]}-${activeCandidate.state}`);
const name = svg.selectAll('.name').filter(`.${d.name.split(' ')[0]}-${d.state}`);
prevName
.transition(t)
.attr('opacity', 1);
name
.transition(t)
.attr('opacity', 0);
prevInfo
.transition(t)
.attr('opacity', 0);
info
.transition(t)
.attr('opacity', 1);
activeCandidate = d;
}
}
};
const changeState = state => {
if (activeCandidate) {
toggleTooltip(null)
}
if (!activeState || activeState.symbol !== state.symbol) {
update(state)
}
else{
update(null);
}
};
const getColor = position => {
switch (position) {
case -2: return '#0049B6';
case -1: return '#7FA3DB';
case 1: return '#F08C91';
case 2: return '#E11923';
default: return '#B2B2B2';
}
};
const buildParagraph = text => (
text.each(function() {
const text = d3.select(this);
if (text.text()) {
const words = text.text().split(/\s+/).reverse();
const data = text.data();
const width = 120;
let line = [];
let lineNumber = 0;
const padding = 20;
const lineHeight = 24;
const className = text.attr('class');
const tooltip = svg.select('.tooltip').append('g')
.attr('class', className)
.attr('opacity', 0)
.attr('style', 'pointer-events: none; direction: rtl; font-size: 18px;')
.data(data);
const bg = tooltip.append('rect');
tooltip.append('text')
.attr('x', width + padding)
.attr('y', 0)
.attr('dy', `${(++lineNumber * lineHeight) + (padding / 2)}px`)
.attr('style', 'font-weight: 700; Line-height:30px;')
.text(data[0].name);
let newText = tooltip.append('text').attr('x', width + padding).attr('y', 0).attr('dy', `${(++lineNumber * lineHeight) + padding}px`);
while (word = words.pop()) {
line.push(word);
newText.text(line.join(' '));
if (newText.node().getComputedTextLength() > width) {
line.pop();
newText.text(line.join(' '));
line = [ word, ];
newText = tooltip.append('text').attr('x', width + padding).attr('y', 0).attr('dy', `${(++lineNumber * lineHeight) + padding}px`)
.text(word);
}
}
bg
.attr('width', `${width + (padding * 2)}px`)
.attr('height', `${(lineNumber * lineHeight) + (padding * 2)}px`)
.attr('fill', '#fff')
.attr('stroke', getColor(data[0].position));
svg.select('.candidates')
.data(data)
.append('text')
.attr('class', d => `${d.name.split(' ')[0]}-${d.state} name`)
.attr('opacity', 0)
.attr('style', 'font-weight: 700; Line-height:30px; pointer-events: none;')
.text(d => d.name);
}
text.remove();
})
);
svg.append('g').classed('links', true);
svg.append('g').classed('states', true);
svg.append('g').classed('statesNames', true);
svg.append('g').classed('candidates', true);
svg.append('g').classed('tooltip', true);
const update = (selectedState) => {
activeState = selectedState;
const link = svg.select('.links')
.selectAll('line')
.data(candidatesNodes, d => d.name);
link.exit()
.transition(t)
.attr('opacity', 0)
.remove();
const linkEnter = link.enter()
.append('line')
.attr('opacity', 0);
linkEnter.merge(link)
.classed('link', true)
.attr('stroke', d => getColor(d.position))
.attr('x1', d => statesNodes.find(state => state.symbol === d.state).x)
.attr('y1', d => statesNodes.find(state => state.symbol === d.state).y)
.transition(t)
.attr('opacity', d => (selectedState
? selectedState.symbol === d.state
? 0.4
: 0
: 0.15)
);
const candidate = svg.select('.candidates')
.selectAll('circle')
.data(candidatesNodes, d => d.name);
candidate.exit()
.transition(t)
.attr('opacity', 0)
.attr('r', 0)
.remove();
const candidateEnter = candidate.enter()
.append('circle')
.attr('opacity', 0)
.attr('r', 0);
candidateEnter.merge(candidate)
.classed('candidate', true)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('fill', d => getColor(d.position))
.on('click', d => selectedState && toggleTooltip(d))
.transition(t)
.attr('opacity', 1)
.attr('r', d => {
if (selectedState) {
if (selectedState.symbol === d.state) {
if (d.info) return candidateRadius;
return candidateRadius / 2;
}
return 0;
}
return candidateRadius;
});
candidate.enter()
.append('text')
.attr('class', d => `${d.name.split(' ')[0]}-${d.state} info`)
.text(d => d.info)
.call(buildParagraph);
svg.selectAll('.candidates > text')
.transition(t)
.attr('opacity', d => {
if (activeState) {
if (activeState.symbol === d.state) {
return 1;
}
return 0;
}
return 0;
});
const state = svg.select('.states')
.selectAll('circle')
.data(statesNodes, d => d.symbol);
state.enter()
.append('circle')
.attr('fill', '#000')
.merge(state)
.classed('state', true)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', stateRadius)
.transition(t)
.attr('fill', d => (selectedState
? selectedState.symbol === d.symbol
? '#000'
: '#CCC'
: '#000')
);
state.enter().append('text')
.classed('symbol', true)
.attr('text-anchor', 'middle ')
.attr('style', 'pointer-events: none;')
.attr('fill', '#fff')
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('dx', '0')
.attr('dy', '5')
.text(d => d.symbol);
const stateNameBG = svg.select('.statesNames')
.selectAll('rect')
.data(statesNodes, d => d.symbol);
stateNameBG.exit()
.transition(t)
.attr('width', 0)
.remove();
const stateNameBGEnter = stateNameBG.enter()
.append('rect')
.attr('width', 0)
.attr('transform', `translate(0 -${(stateRadius * 1.25) / 2})`
);
stateNameBGEnter.merge(stateNameBG)
.attr('fill', '#000')
.attr('height', stateRadius * 1.25)
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('rx', 5)
.attr('ry', 5)
.transition(t)
.attr('transform', d =>
`translate(-${
selectedState && selectedState.symbol === d.symbol
? (d.name.length * 10) / 2
: 0
} -${(stateRadius * 1.25) / 2})`
)
.attr('width', d =>
(selectedState && selectedState.symbol === d.symbol
? d.name.length * 10
: 0)
);
const stateName = svg.select('.statesNames')
.selectAll('text')
.data(statesNodes, d => d.symbol);
stateName.exit()
.transition(t)
.attr('opacity', 0)
.remove();
const stateNameEnter = stateName.enter()
.append('text')
.attr('width', 0);
stateNameEnter.merge(stateName)
.classed('name', true)
.attr('text-anchor', 'middle ')
.attr('style', 'pointer-events: none;')
.attr('fill', '#fff')
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('dx','0')
.attr('dy', '5')
.text(d => d.name)
.transition(t)
.attr('opacity', d =>
(selectedState && selectedState.symbol === d.symbol
? 1
: 0)
);
svg.selectAll('.state').on('click', state => changeState(state));
stateNameBG.on('click', state => changeState(state));
const simulation = d3.forceSimulation(candidatesNodes)
.force('x', d3.forceX().strength(0.007))
.force('y', d3.forceY().strength(0.007))
.force('center', d3.forceCenter(radius + stateRadius * 1.5, radius + stateRadius * 1.5))
.force('collide', d3.forceCollide(candidateRadius * 1.5).strength(0.5))
.on('tick', () => {
svg.selectAll('.candidate')
.attr('cx', d => d.x)
.attr('cy', d => d.y);
svg.selectAll('.link')
.attr('x2', d => d.x)
.attr('y2', d => d.y);
svg.selectAll('.info')
.attr('transform', d => `translate(${d.x} ${d.y})`);
svg.selectAll('.candidates > text')
.attr('x', d => d.x + candidateRadius + 2)
.attr('y', d => d.y + candidateRadius * 0.5);
});
simulation.nodes(candidatesNodes)
.alpha(1)
.restart();
};
update(null, null);
</script>
</body>
https://d3js.org/d3.v4.min.js