class PieChart {
constructor() {
document
.getElementsByTagName("head")[0]
.insertAdjacentHTML(
"beforeend",
`<style>.d3-tip { line-height: 1.4; padding: 12px; pointer-events: none !important; color: #203d5d; box-shadow: 0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2); background-color: #fff; border-radius: 4px; } /* Creates a small triangle extender for the tooltip */ .d3-tip:after { box-sizing: border-box; display: inline; font-size: 10px; width: 100%; line-height: 1; color: #fff; position: absolute; pointer-events: none; } /* Northward tooltips */ .d3-tip.n:after { content: "▼"; margin: -1px 0 0 0; top: 100%; left: 0; text-align: center; }</style>`
);
const attrs = {
id: "ID" + Math.floor(Math.random() * 1000000),
svgWidth: 600,
svgHeight: 600,
marginTop: 75,
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),
centerText: ``,
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
} = 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">
<div style="display:inline-block;text-transform:uppercase;font-size:17px;font-weight:bold;color:${defaultTextFill}">${centerText}
<br/>
<span style="font-size:13px;font-weight:400">${groupingText}</span> <br/>
<span style="font-size:20px;font-weight:bold">${valueFormat(sum)}</span>
</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()() + 10);
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();
const tip = d3.tip().attr("class", "d3-tip").offset([-10, 0]).html(tooltip);
// Draw SVG
const svg = container
.patternify({
tag: "svg",
selector: "svg-chart-container"
})
.attr("width", svgWidth)
.attr("height", svgHeight)
.attr("font-family", defaultFont);
svg.call(tip);
//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, tip });
}
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) {
tip.show(event, d, this);
})
.on("mouseleave.tooltip", function (d) {
if (tip) {
tip.hide();
}
})
.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;
}
}