diff --git a/CHANGELOG.md b/CHANGELOG.md index c10e2838..5aa8f3c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ _Note: Gaps between patch versions are faulty, broken or test releases._ * **New Feature** * Support outputting different URL in server mode ([#520](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/520) by [@southorange1228](https://github.com/southorange1228)) + * Use deterministic chunk colors (#[501](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/501) by [@CreativeTechGuy](https://github.com/CreativeTechGuy)) ## 4.5.0 diff --git a/client/components/Treemap.jsx b/client/components/Treemap.jsx index 423a29d9..d9d46b72 100644 --- a/client/components/Treemap.jsx +++ b/client/components/Treemap.jsx @@ -7,6 +7,7 @@ export default class Treemap extends Component { super(props); this.treemap = null; this.zoomOutDisabled = false; + this.findChunkNamePartIndex(); } componentDidMount() { @@ -16,6 +17,7 @@ export default class Treemap extends Component { componentWillReceiveProps(nextProps) { if (nextProps.data !== this.props.data) { + this.findChunkNamePartIndex(); this.treemap.set({ dataObject: this.getTreemapDataObject(nextProps.data) }); @@ -76,6 +78,19 @@ export default class Treemap extends Component { 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; @@ -136,6 +151,14 @@ export default class Treemap extends Component { }); } + getGroupRoot(group) { + let nextParent; + while (!group.isAsset && (nextParent = this.treemap.get('hierarchy', group).parent)) { + group = nextParent; + } + return group; + } + zoomToGroup(group) { this.zoomOutDisabled = false; @@ -164,9 +187,62 @@ export default class Treemap extends Component { 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; } } 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; +}