proportions-chart
Version:
A one-dimensional proportional chart web component for visualizing categorical data
240 lines (225 loc) • 10.1 kB
JavaScript
import { select } from 'd3-selection';
import { scaleLinear } from 'd3-scale';
import { pie } from 'd3-shape';
import { transition } from 'd3-transition';
import Kapsule from 'kapsule';
import tinycolor from 'tinycolor2';
import accessorFn from 'accessor-fn';
import Tooltip from 'float-tooltip';
function styleInject(css, ref) {
if (ref === void 0) ref = {};
var insertAt = ref.insertAt;
if (typeof document === 'undefined') {
return;
}
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = ".proportions-viz rect {\n transition-property: transform, opacity;\n transition-duration: .5s;\n}\n\n.proportions-viz rect:hover {\n transform: scaleY(1.2); /* set HOVER_REL_MARGIN to 0.18 */\n opacity: 0.85;\n transition-duration: .25s;\n transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); /* EaseOutBack */\n}\n\n.proportions-viz .rect {\n stroke: white;\n stroke-width: 1px;\n /*transition: opacity .4s;*/\n}\n\n.proportions-viz .rect:hover {\n opacity: 0.9;\n transition: opacity .05s;\n}\n\n.proportions-viz text {\n font-family: sans-serif;\n font-size: 12px;\n dominant-baseline: middle;\n text-anchor: middle;\n pointer-events: none;\n fill: #404041;\n}\n\n.proportions-viz text.sub-label {\n font-size: 10px;\n opacity: 0.7;\n}\n\n.proportions-viz .light text {\n fill: #F7F7F7;\n}\n\n.proportions-viz {\n position: relative;\n}\n\n.proportions-viz .tooltip-title {\n font-weight: bold;\n text-align: center;\n margin-bottom: 5px;\n}\n";
styleInject(css_248z);
var TRANSITION_DURATION = 800;
var HOVER_REL_MARGIN = 0.18; // How much (relative) margin to save for hover interaction enlarging
var CHAR_WIDTH_PER_FONT_SIZE = 0.55;
var MAIN_LABEL_FONTSIZE = 12;
var SUB_LABEL_FONTSIZE = 10;
var MAIN_LABEL_Y_OFFSET = -0.47;
var SUB_LABEL_Y_OFFSET = 0.77;
var proportionsChart = Kapsule({
props: {
width: {
"default": window.innerWidth
},
height: {
"default": window.innerHeight
},
data: {
"default": [],
onChange: function onChange() {
this._parseData();
}
},
sort: {
onChange: function onChange() {
this._parseData();
}
},
label: {
"default": function _default(d) {
return d.name;
}
},
size: {
"default": 'value',
onChange: function onChange() {
this._parseData();
}
},
color: {
"default": function _default(d) {
return 'lightgrey';
}
},
showLabels: {
"default": true
},
tooltipContent: {
triggerUpdate: false
},
onClick: {
triggerUpdate: false
},
onRightClick: {
triggerUpdate: false
},
onHover: {
triggerUpdate: false
}
},
methods: {
_parseData: function _parseData(state) {
var chartData = pie().endAngle(1) // stack values up to unity
.sort(state.sort || null).value(function (d) {
return d.value;
})(state.data || []);
chartData.forEach(function (d, i) {
d.id = i; // Mark each node with a unique ID
d.data.__dataNode = d; // Dual-link data nodes
});
state.layoutData = chartData;
}
},
stateInit: function stateInit() {
return {
chartId: Math.round(Math.random() * 1e12),
// Unique ID for DOM elems
widthScale: scaleLinear(),
heightScale: scaleLinear()
};
},
init: function init(domNode, state) {
var el = select(domNode).append('div').attr('class', 'proportions-viz');
state.svg = el.append('svg');
state.canvas = state.svg.append('g');
// tooltips
state.tooltip = new Tooltip(el);
// detect hover out events
state.svg.on('mouseover', function (ev) {
return state.onHover && state.onHover(null, ev);
});
},
update: function update(state) {
state.widthScale.range([0, state.width]);
state.heightScale.range([0, state.height * (1 - HOVER_REL_MARGIN)]);
state.svg.style('width', state.width + 'px').style('height', state.height + 'px').attr('viewBox', "0 ".concat(-state.height / 2, " ").concat(state.width, " ").concat(state.height));
var valSum = state.layoutData.reduce(function (agg, d) {
return agg + d.value;
}, 0);
var getPerc = function getPerc(d) {
return d.value / valSum * 100;
};
var segment = state.canvas.selectAll('.segment').data(state.layoutData, function (d) {
return d.id;
});
var nameOf = accessorFn(state.label);
var colorOf = accessorFn(state.color);
var transition$1 = transition().duration(TRANSITION_DURATION);
// Exiting
segment.exit().transition(transition$1).style('opacity', 0).remove();
// Entering
var newSegment = segment.enter().append('g').attr('class', 'segment').style('opacity', 0).on('click', function (ev, d) {
ev.stopPropagation();
state.onClick && state.onClick(d.data, ev);
}).on('contextmenu', function (ev, d) {
ev.stopPropagation();
if (state.onRightClick) {
state.onRightClick(d.data, ev);
ev.preventDefault();
}
}).on('mouseover', function (ev, d) {
ev.stopPropagation();
state.onHover && state.onHover(d.data, ev);
state.tooltip.content("\n <div class=\"tooltip-title\">".concat(nameOf(d.data), "</div>\n ").concat(state.tooltipContent ? state.tooltipContent(d.data, d) : "<div>\n <b>".concat(d.value, "</b> (<i>").concat(roundPercentage(getPerc(d), 2), "%</i>)\n </div"), "\n "));
}).on('mouseout', function () {
state.tooltip.content(null);
});
newSegment.append('rect').attr('id', function (d) {
return "segment-".concat(d.id);
}).attr('x', function (d) {
return state.widthScale((d.startAngle + d.endAngle) / 2);
}).attr('y', 0).attr('width', 0).attr('height', 0).style('fill', function (d) {
return colorOf(d.data, d.parent);
});
newSegment.append('clipPath').attr('id', function (d) {
return "clip-".concat(d.id);
}).append('use').attr('xlink:href', function (d) {
return "#segment-".concat(d.id);
});
var labels = newSegment.append('g').attr('clip-path', function (d) {
return "url(#clip-".concat(d.id, ")");
}).append('g').attr('class', 'label-container').attr('transform', function (d) {
return "translate(".concat(state.widthScale((d.startAngle + d.endAngle) / 2), ", 0) scale(0)");
});
labels.append('text').attr('class', 'main-label').attr('transform', "translate(0, ".concat(MAIN_LABEL_FONTSIZE * MAIN_LABEL_Y_OFFSET, ")"));
labels.append('text').attr('class', 'sub-label').attr('transform', "translate(0, ".concat(SUB_LABEL_FONTSIZE * SUB_LABEL_Y_OFFSET, ")"));
// Entering + Updating
var allSegments = segment.merge(newSegment);
allSegments.style('cursor', state.onClick ? 'pointer' : null).transition(transition$1).style('opacity', 1);
allSegments.select('rect').transition(transition$1).attr('id', function (d) {
return "segment-".concat(d.id);
}).attr('x', function (d) {
return state.widthScale(d.startAngle);
}).attr('y', -state.heightScale(1) / 2).attr('width', function (d) {
return state.widthScale(d.endAngle - d.startAngle);
}).attr('height', state.heightScale(1)).style('fill', function (d) {
return colorOf(d.data);
});
allSegments.select('.label-container').classed('light', function (d) {
return !tinycolor(colorOf(d.data, d.parent)).isLight();
}).transition(transition$1).attr('transform', function (d) {
return "translate(".concat(state.widthScale((d.startAngle + d.endAngle) / 2), ", 0) scale(1)");
});
allSegments.select('.main-label').text(function (d) {
return nameOf(d.data);
}).attr('transform', function (d) {
return "translate(0, ".concat(textFits(d, 'foo', SUB_LABEL_FONTSIZE * SUB_LABEL_Y_OFFSET, SUB_LABEL_FONTSIZE) ? MAIN_LABEL_FONTSIZE * MAIN_LABEL_Y_OFFSET : 0, ")");
}).style('display', function (d) {
return state.showLabels && textFits(d, nameOf(d.data), textFits(d, 'foo', SUB_LABEL_FONTSIZE * SUB_LABEL_Y_OFFSET, SUB_LABEL_FONTSIZE) ? MAIN_LABEL_FONTSIZE * MAIN_LABEL_Y_OFFSET : 0, MAIN_LABEL_FONTSIZE) ? null : 'none';
});
allSegments.select('.sub-label').text(function (d) {
return "".concat(roundPercentage(getPerc(d)), "%");
}).attr('transform', "translate(0, ".concat(SUB_LABEL_FONTSIZE * SUB_LABEL_Y_OFFSET, ")")).style('display', function (d) {
return state.showLabels && textFits(d, nameOf(d.data), MAIN_LABEL_FONTSIZE * MAIN_LABEL_Y_OFFSET, MAIN_LABEL_FONTSIZE) && textFits(d, "".concat(roundPercentage(getPerc(d)), "%"), SUB_LABEL_FONTSIZE * SUB_LABEL_Y_OFFSET, SUB_LABEL_FONTSIZE) ? null : 'none';
});
//
function textFits(d, text) {
var yOffset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
var fontSize = arguments.length > 3 ? arguments[3] : undefined;
var minRectHeight = fontSize + Math.abs(yOffset) * 2;
var minRectWidth = text.length * fontSize * CHAR_WIDTH_PER_FONT_SIZE;
return state.heightScale(1) >= minRectHeight && state.widthScale(d.endAngle - d.startAngle) >= minRectWidth;
}
}
});
//
function roundPercentage(val) {
var roundDecimalCases = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var roundDiv = Math.pow(10, roundDecimalCases);
// Treat ~0% and ~100% values specially so the precision is surfaced
return val > 99 ? 100 - +(100 - val).toPrecision(1) : val < 1 ? +val.toPrecision(1) : Math.round(val * roundDiv) / roundDiv;
}
export { proportionsChart as default };