d3-org-chart-jc
Version:
Highly customizable org chart, created with d3
536 lines (491 loc) • 15.7 kB
JavaScript
class PieChart {
// ================. BASE SETUP. ==============
constructor() {
const attrs = {
id: 'ID' + Math.floor(Math.random() * 1000000),
svgWidth: 400,
svgHeight: 400,
marginTop: 75,
image: '',
marginBottom: 75,
marginRight: 105,
marginLeft: 105,
container: 'body',
defaultFontSize: 12,
percentCircleRadius: 14,
labelMargin: 10,
defaultTextFill: '#6F68A7',
backCircleColor: '#EAF0FB',
defaultFont: 'Helvetica',
valueAccessor: d => d.value,
round: (d, sum) => Math.round((d.data.value / sum) * 100),
groupingText: `Count`,
valueFormat: d => d3.format('.3s')(d),
ctx: document.createElement('canvas').getContext('2d'),
dimension: null,
group: null,
data: null,
tooltip: (event, d) => {
let items = d.data.items;
if (!items) items = [{ key: d.data.key, value: d.data.value }];
return `
<table style="font-size:12px">
<tr><th>Name</th><th>Value</th></tr>
${items
.map(
t =>
`<tr><td style="padding-right:20px;">${t.key}</td><td>${
t.value
}</td></tr>`
)
.join('')}
</table>
`;
},
setData: state => {
if (!state.group) return state.data;
const dt = state.group.all();
dt.sort((a, b) => (a.value > b.value ? -1 : 1));
return dt;
}
};
// Inner state props
Object.assign(attrs, {
calc: null,
svg: null,
chart: null,
pie: null,
arc: null,
arcOuter: null
});
this.getState = () => attrs;
this.setState = d => Object.assign(attrs, d);
Object.keys(attrs).forEach(key => {
//@ts-ignore
this[key] = function(_) {
var string = `attrs['${key}'] = _`;
if (!arguments.length) {
return eval(`attrs['${key}'];`);
}
eval(string);
return this;
};
});
this.initializeEnterExitUpdatePattern();
}
initializeEnterExitUpdatePattern() {
d3.selection.prototype.patternify = function(params) {
var container = this;
var selector = params.selector;
var elementTag = params.tag;
var data = params.data || [selector];
// Pattern in action
var selection = container.selectAll('.' + selector).data(data, (d, i) => {
if (typeof d === 'object') {
if (d.id) {
return d.id;
}
}
return i;
});
selection.exit().remove();
selection = selection
.enter()
.append(elementTag)
.merge(selection);
selection.attr('class', selector);
return selection;
};
}
// ================== RENDERING ===================
render() {
this.setDataProp();
this.setDynamicContainer();
this.calculateProperties();
this.drawSvgAndWrappers();
this.setLayouts();
this.drawSlices();
this.drawCenterTexts();
this.attachEventHandlers();
return this;
}
setDataProp() {
const data = this.getData();
this.setState({ data });
}
drawCenterTexts() {
const {
data,
centerPoint,
calc,
defaultTextFill,
valueFormat,
centerText,
groupingText,
image,
backCircleColor
} = this.getState();
const sum = d3.sum(data, d => d.value);
const fo = centerPoint
.patternify({ tag: 'foreignObject', selector: 'center-for-text' })
.attr('pointer-events', 'none')
.attr('x', -calc.innerRadius)
.attr('y', -calc.innerRadius)
.attr('width', calc.innerRadius * 2)
.attr('height', calc.innerRadius * 2);
fo.patternify({ tag: 'xhtml:div', selector: 'for-center-div' })
.html(`<div style="height:${calc.innerRadius *
2}px;display:flex;justify-content:center;align-items:center;text-align:center">
<img height="${calc.innerRadius * 2 -
20}" style="border:2px solid ${backCircleColor};border-radius:40px" width="${calc.innerRadius *
2 -
20}" src="${image}" />
</div>
</div>`);
}
setLayouts() {
const { calc } = this.getState();
const pie = d3
.pie()
.value(d => d.value)
.sort(null);
const arc = d3
.arc()
.innerRadius(calc.innerRadius)
.outerRadius(calc.radius)
.padAngle(0.02)
.cornerRadius(1);
const arcOuter = d3
.arc()
.innerRadius(arc.outerRadius()() + 2)
.outerRadius(arc.outerRadius()() + 5);
const arcLabel = d3
.arc()
.innerRadius(arcOuter.outerRadius()())
.outerRadius(arcOuter.outerRadius()() + 30);
this.setState({ pie, arc, arcOuter, arcLabel });
}
attachEventHandlers() {}
// Calculate what size will text take when drew
getTextWidth(text, { fontSize = 14, fontWeight = 400 } = {}) {
const { defaultFont, ctx } = this.getState();
ctx.font = `${fontWeight || ''} ${fontSize}px ${defaultFont} `;
const measurement = ctx.measureText(text);
return measurement.width;
}
setDynamicContainer() {
const attrs = this.getState();
//Drawing containers
var container = d3.select(attrs.container);
var containerRect = container.node().getBoundingClientRect();
//if (containerRect.width > 0) attrs.svgWidth = containerRect.width;
this.setState({ container });
}
drawSvgAndWrappers() {
const {
tooltip,
container,
svgWidth,
svgHeight,
defaultFont,
calc
} = this.getState();
// Draw SVG
const svg = container
.patternify({
tag: 'svg',
selector: 'svg-chart-container'
})
.attr('width', svgWidth)
.attr('height', svgHeight)
.attr('font-family', defaultFont);
//Add container g element
var chart = svg
.patternify({
tag: 'g',
selector: 'chart'
})
.attr(
'transform',
'translate(' + calc.chartLeftMargin + ',' + calc.chartTopMargin + ')'
);
const centerPoint = chart
.patternify({ tag: 'g', selector: 'center-point' })
.attr(
'transform',
'translate(' + calc.chartWidth / 2 + ',' + calc.chartHeight / 2 + ')'
);
this.setState({ chart, svg, centerPoint });
}
calculateProperties() {
const attrs = this.getState();
//Calculated properties
var calc = {
id: 'ID' + Math.floor(Math.random() * 1000000), // id for event handlings,
chartTopMargin: attrs.marginTop,
chartLeftMargin: attrs.marginLeft,
chartWidth: null,
chartHeight: null
};
calc.chartWidth = attrs.svgWidth - attrs.marginRight - calc.chartLeftMargin;
calc.chartHeight =
attrs.svgHeight - attrs.marginBottom - calc.chartTopMargin;
calc.radius = Math.min(calc.chartWidth, calc.chartHeight) / 2;
calc.innerRadius = calc.radius * 0.7;
if (calc.innerRadius < 100) calc.innerRadius = calc.radius * 0.8;
this.setState({ calc });
}
// =================== API IMPLEMENTATION ===============
getData() {
const state = this.getState();
const { setData } = state;
return setData(state);
}
drawSlices() {
const {
round,
labelMargin,
defaultFontSize,
centerPoint,
pie,
arc,
arcOuter,
backCircleColor,
arcLabel,
defaultTextFill,
tip,
percentCircleRadius
} = this.getState();
const dataInitial = this.getData();
const sum = d3.sum(dataInitial, d => d.value);
const minAllowed = 0.04;
let data = dataInitial.filter(
dataItem => dataItem.value / sum >= minAllowed
);
const others = dataInitial.filter(
dataItem => dataItem.value / sum < minAllowed
);
if (others.length > 1) {
data.push({
key: 'Others',
items: others,
value: d3.sum(others, d => d.value)
});
} else {
data = dataInitial;
}
const pieData = pie(data);
const right = pieData.filter(d => this.isRightSide(d));
const left = pieData.filter(d => !this.isRightSide(d));
pieData.forEach((d, i, arr) => {
d.xOffset = 0;
if ((i != 0 && i != arr.length - 1) || arr.length < 20) {
d.yOffset = 0;
} else {
d.yOffset = -30;
}
});
const process = (d, i, arr) => {
if (i < 1) return;
const prev = arr[i - 1];
const curr = d;
const yPrev = arcLabel.centroid(prev)[1] + prev.yOffset;
const yCurr = arcLabel.centroid(curr)[1];
console.log(yPrev, yCurr);
if (this.isRightSide(curr) && yPrev + percentCircleRadius * 2 > yCurr) {
console.log('is Righ Side');
curr.yOffset = yPrev + percentCircleRadius * 2 - yCurr + 2;
} else if (
!this.isRightSide(curr) &&
yPrev + percentCircleRadius * 2 > yCurr
) {
curr.yOffset = yPrev + percentCircleRadius * 2 - yCurr + 2;
if (arr.length > 4) {
if (i < 4 + arr.length / 10) {
//curr.xOffset = -10-arr.length/2*1;
}
if (arr.length > 9) {
curr.xOffset = 0;
}
}
console.log('is not Righ Side', this.isRightSide(curr));
}
};
right.forEach(process);
left.reverse().forEach(process);
const that = this;
centerPoint
.patternify({
tag: 'path',
selector: 'pie-background',
data: pie([{ value: 1 }])
})
.attr('d', arcOuter)
.attr('fill', backCircleColor);
const pieG = centerPoint.patternify({
tag: 'g',
selector: 'pie-wrapper',
data: pieData
});
pieG
.patternify({ tag: 'path', selector: 'pie-paths', data: d => [d] })
.attr('d', arc)
.attr('cursor', 'pointer')
.attr('fill', d => that.getColor(d))
.on('mouseenter.tooltip', function(event, d) {})
.on('mouseleave.tooltip', function(d) {})
.on('mouseenter.color', function(event, d) {
d3.select(this).attr('fill', d3.rgb(that.getColor(d)).darker(0.5));
})
.on('mouseleave.color', function(event, d) {
d3.select(this).attr('fill', that.getColor(d));
});
pieG
.patternify({
tag: 'polyline',
selector: 'pie-label-line',
data: d => [d]
})
.attr('points', d => {
let textWidth =
this.getTextWidth(d.data.key || '', { fontSize: defaultFontSize }) +
labelMargin;
if (this.isRightSide(d)) {
textWidth = -textWidth;
}
return `
${arc.centroid(d)[0]},
${arc.centroid(d)[1]}
${arcLabel.centroid(this.correct(d))[0] + d.xOffset},
${arcLabel.centroid(this.correct(d))[1] + d.yOffset}
${arcLabel.centroid(this.correct(d))[0] - textWidth + d.xOffset},
${arcLabel.centroid(this.correct(d))[1] + d.yOffset}
`;
})
.attr('stroke', defaultTextFill)
.attr('fill', 'none')
.attr('pointer-events', 'none')
.style('opacity', this.getLabelOpacity);
pieG
.patternify({
tag: 'circle',
selector: 'pie-center-points',
data: d => [d]
})
.attr('cx', d => arc.centroid(d)[0])
.attr('cy', d => arc.centroid(d)[1])
.style('opacity', this.getLabelOpacity)
.attr('fill', '#FFFFFF')
.attr('r', 2)
.attr('pointer-events', 'none');
pieG
.patternify({ tag: 'text', selector: 'pie-texts', data: d => [d] })
.text(d => d.data.key)
.attr('x', d => {
let x = arcLabel.centroid(this.correct(d))[0];
if (this.isRightSide(d)) x += labelMargin - 5;
else x -= labelMargin - 5;
return x + d.xOffset;
})
.attr('text-anchor', d => {
if (this.isRightSide(d)) return 'start';
return 'end';
})
.attr('font-size', defaultFontSize)
.attr('y', d => arcLabel.centroid(this.correct(d))[1] - 4 + d.yOffset)
.attr('fill', defaultTextFill)
.style('opacity', this.getLabelOpacity);
pieG
.patternify({
tag: 'text',
selector: 'pie-percent-texts',
data: d => [d]
})
.text(d => round(d, sum) + '%')
.attr('alignment-baseline', 'middle')
.attr('x', d => {
let textWidth =
this.getTextWidth(d.data.key || '', { fontSize: defaultFontSize }) +
labelMargin +
percentCircleRadius;
if (this.isRightSide(d)) {
textWidth = -textWidth;
}
return arcLabel.centroid(this.correct(d))[0] - textWidth + d.xOffset;
})
.attr('y', d => arcLabel.centroid(this.correct(d))[1] + d.yOffset)
.attr('text-anchor', 'middle')
.attr('font-size', 11)
.attr('fill', defaultTextFill)
.style('opacity', this.getLabelOpacity);
pieG
.patternify({
tag: 'circle',
selector: 'pie-percent-circle',
data: d => [d]
})
.attr('stroke', defaultTextFill)
.attr('r', percentCircleRadius)
.style('opacity', this.getLabelOpacity)
.attr('fill', 'none')
.attr('cx', d => {
let textWidth =
this.getTextWidth(d.data.key || '', { fontSize: defaultFontSize }) +
labelMargin +
percentCircleRadius;
if (this.isRightSide(d)) {
textWidth = -textWidth;
}
return arcLabel.centroid(this.correct(d))[0] - textWidth + d.xOffset;
})
.attr('cy', d => arcLabel.centroid(this.correct(d))[1] - 1.1 + d.yOffset);
// centroid = arcGenerator.centroid(d);
}
getLabelOpacity(pieItem) {
if (Math.abs(pieItem.yOffset) > 130) {
return 0;
}
return 1;
}
getColor(d) {
if (!d.data) {
debugger;
}
return (
d.data.color ||
d3.schemeSet2[this.hashCode(d.data.key + '') % d3.schemeSet2.length]
);
}
isRightSide(n) {
const d = this.correct(n);
const midAngle = (d.startAngle + d.endAngle) / 2;
if (midAngle <= Math.PI) return true;
return false;
}
correct(d) {
return Object.assign({}, d, {
startAngle: d.startAngle,
endAngle: d.endAngle
});
}
// Get hashcode from string
hashCode(s) {
for (var i = 0, h; i < s.length; i++)
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
return Math.abs(h);
}
// ===================
// Method which renders initial html elements
_doRender() {
this._doRedraw();
return this;
}
// Method which redraws graph after data change
_doRedraw() {
if (this.hasFilter() && this._multiple) {
} else if (this.hasFilter()) {
} else {
}
return this;
}
}