UNPKG

proportions-chart

Version:

A one-dimensional proportional chart web component for visualizing categorical data

240 lines (225 loc) 10.1 kB
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 };