123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- import {Component, CSSProperties} from 'react';
- import FoamTree from '@carrotsearch/foamtree';
- export default class Treemap extends Component {
- constructor(props) {
- super(props);
- this.treemap = null;
- this.zoomOutDisabled = false;
- this.findChunkNamePartIndex();
- }
- componentDidMount() {
- this.treemap = this.createTreemap();
- window.addEventListener('resize', this.resize);
- }
- componentWillReceiveProps(nextProps) {
- if (nextProps.data !== this.props.data) {
- this.findChunkNamePartIndex();
- this.treemap.set({
- dataObject: this.getTreemapDataObject(nextProps.data),
- });
- } else if (nextProps.highlightGroups !== this.props.highlightGroups) {
- setTimeout(() => this.treemap.redraw());
- }
- }
- shouldComponentUpdate() {
- return false;
- }
- componentWillUnmount() {
- window.removeEventListener('resize', this.resize);
- this.treemap.dispose();
- }
- saveNodeRef = node => (this.node = node);
- getTreemapDataObject(data = this.props.data) {
- return {groups: data};
- }
- createTreemap() {
- const component = this;
- const {props} = this;
- return new FoamTree({
- element: this.node,
- layout: 'squarified',
- stacking: 'flattened',
- pixelRatio: window.devicePixelRatio || 1,
- maxGroups: Infinity,
- maxGroupLevelsDrawn: Infinity,
- maxGroupLabelLevelsDrawn: Infinity,
- maxGroupLevelsAttached: Infinity,
- wireframeLabelDrawing: 'always',
- groupMinDiameter: 0,
- groupLabelVerticalPadding: 0.2,
- rolloutDuration: 0,
- pullbackDuration: 0,
- fadeDuration: 0,
- groupExposureZoomMargin: 0.2,
- zoomMouseWheelDuration: 300,
- openCloseDuration: 200,
- dataObject: this.getTreemapDataObject(),
- titleBarDecorator(opts, props, vars) {
- vars.titleBarShown = false;
- },
- groupColorDecorator(options, properties, variables) {
- const root = component.getGroupRoot(properties.group);
- const chunkName = component.getChunkNamePart(root.label);
- const hash = /[^0-9]/u.test(chunkName)
- ? hashCode(chunkName)
- : (parseInt(chunkName) / 1000) * 360;
- variables.groupColor = {
- model: 'hsla',
- h: Math.round(Math.abs(hash) % 360),
- s: 60,
- l: 50,
- a: 0.9,
- };
- const {highlightGroups} = component.props;
- const module = properties.group;
- if (highlightGroups && highlightGroups.has(module)) {
- variables.groupColor = {
- model: 'rgba',
- r: 255,
- g: 0,
- b: 0,
- a: 0.8,
- };
- } else if (highlightGroups && highlightGroups.size > 0) {
- // this means a search (e.g.) is active, but this module
- // does not match; gray it out
- // https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/553
- variables.groupColor.s = 10;
- }
- },
- /**
- * Handle Foamtree's "group clicked" event
- * @param {FoamtreeEvent} event - Foamtree event object
- * (see https://get.carrotsearch.com/foamtree/demo/api/index.html#event-details)
- * @returns {void}
- */
- onGroupClick(event) {
- preventDefault(event);
- if ((event.ctrlKey || event.secondary) && props.onGroupSecondaryClick) {
- props.onGroupSecondaryClick.call(component, event);
- return;
- }
- component.zoomOutDisabled = false;
- this.zoom(event.group);
- },
- onGroupDoubleClick: preventDefault,
- onGroupHover(event) {
- // Ignoring hovering on `FoamTree` branding group and the root group
- if (
- event.group &&
- (event.group.attribution || event.group === this.get('dataObject'))
- ) {
- event.preventDefault();
- if (props.onMouseLeave) {
- props.onMouseLeave.call(component, event);
- }
- return;
- }
- if (props.onGroupHover) {
- props.onGroupHover.call(component, event);
- }
- },
- onGroupMouseWheel(event) {
- const {scale} = this.get('viewport');
- const isZoomOut = event.delta < 0;
- if (isZoomOut) {
- if (component.zoomOutDisabled) {
- return preventDefault(event);
- }
- if (scale < 1) {
- component.zoomOutDisabled = true;
- preventDefault(event);
- }
- } else {
- component.zoomOutDisabled = false;
- }
- },
- });
- }
- getGroupRoot(group) {
- let nextParent;
- while (!group.isAsset && (nextParent = this.treemap.get('hierarchy', group).parent)) {
- group = nextParent;
- }
- return group;
- }
- zoomToGroup(group) {
- this.zoomOutDisabled = false;
- while (group && !this.treemap.get('state', group).revealed) {
- group = this.treemap.get('hierarchy', group).parent;
- }
- if (group) {
- this.treemap.zoom(group);
- }
- }
- isGroupRendered(group) {
- const groupState = this.treemap.get('state', group);
- return !!groupState && groupState.revealed;
- }
- update() {
- this.treemap.update();
- }
- resize = () => {
- const {props} = this;
- this.treemap.resize();
- if (props.onResize) {
- props.onResize();
- }
- };
- /**
- * Finds patterns across all chunk names to identify the unique "name" part.
- */
- findChunkNamePartIndex() {
- const splitChunkNames = this.props.data.map(chunk =>
- chunk.label.split(/[^a-z0-9]/iu)
- );
- const longestSplitName = Math.max(...splitChunkNames.map(parts => parts.length));
- const namePart = {
- index: 0,
- votes: 0,
- };
- for (let i = longestSplitName - 1; i >= 0; i--) {
- const identifierVotes = {
- name: 0,
- hash: 0,
- ext: 0,
- };
- let lastChunkPart = '';
- for (const splitChunkName of splitChunkNames) {
- const part = splitChunkName[i];
- if (part === undefined || part === '') {
- continue;
- }
- if (part === lastChunkPart) {
- identifierVotes.ext++;
- } else if (
- /[a-z]/u.test(part) &&
- /[0-9]/u.test(part) &&
- part.length === lastChunkPart.length
- ) {
- identifierVotes.hash++;
- } else if (/^[a-z]+$/iu.test(part) || /^[0-9]+$/u.test(part)) {
- identifierVotes.name++;
- }
- lastChunkPart = part;
- }
- if (identifierVotes.name >= namePart.votes) {
- namePart.index = i;
- namePart.votes = identifierVotes.name;
- }
- }
- this.chunkNamePartIndex = namePart.index;
- }
- getChunkNamePart(chunkLabel) {
- return chunkLabel.split(/[^a-z0-9]/iu)[this.chunkNamePartIndex] || chunkLabel;
- }
- render() {
- return (
- <div
- style={{width: '100%', height: 'calc(100vh - 300px)'}}
- {...this.props}
- ref={this.saveNodeRef}
- />
- );
- }
- }
- function preventDefault(event) {
- event.preventDefault();
- }
- function hashCode(str) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- const code = str.charCodeAt(i);
- hash = (hash << 5) - hash + code;
- hash = hash & hash;
- }
- return hash;
- }
|