mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(devtools): simplify vendored deps to make importing into google3 easier (#62567)
prefer using node_modules/webtreemap, and remove memo-decorator PR Close #62567
This commit is contained in:
parent
817d9df84b
commit
cfa44df503
20 changed files with 24 additions and 911 deletions
|
|
@ -8,7 +8,7 @@ adev/shared-docs/package.json=450629456
|
|||
adev/shared-docs/pipeline/api-gen/package.json=939673974
|
||||
integration/package.json=-239561259
|
||||
modules/package.json=-2111512175
|
||||
package.json=-1105986473
|
||||
package.json=2040412148
|
||||
packages/animations/package.json=-678724831
|
||||
packages/benchpress/package.json=-1908328724
|
||||
packages/common/package.json=1729763064
|
||||
|
|
@ -26,7 +26,7 @@ packages/platform-server/package.json=-737662753
|
|||
packages/router/package.json=860819913
|
||||
packages/upgrade/package.json=16347051
|
||||
packages/zone.js/package.json=-1005735564
|
||||
pnpm-lock.yaml=-525143433
|
||||
pnpm-lock.yaml=-373910383
|
||||
pnpm-workspace.yaml=1738525657
|
||||
tools/bazel/rules_angular_store/package.json=-239561259
|
||||
yarn.lock=-1269359948
|
||||
yarn.lock=-1535687935
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ ng_project(
|
|||
"//:node_modules/@angular/material",
|
||||
"//:node_modules/@types/d3",
|
||||
"//:node_modules/d3",
|
||||
"//:node_modules/memo-decorator",
|
||||
"//:node_modules/ngx-flamegraph",
|
||||
"//:node_modules/rxjs",
|
||||
"//:node_modules/webtreemap",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ ts_project(
|
|||
"//:node_modules/tslib",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-services:theme_rjs",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/record-formatter:record-formatter_rjs",
|
||||
"//devtools/projects/ng-devtools/src/lib/vendor/memo-decorator:memo-decorator_rjs",
|
||||
"//devtools/projects/protocol:protocol_rjs",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
type ProfilerFrame,
|
||||
} from '../../../../../../../../protocol';
|
||||
|
||||
import {memo} from '../../../../../vendor/memo-decorator';
|
||||
import {RecordFormatter} from '../record-formatter';
|
||||
|
||||
export interface BargraphNode {
|
||||
|
|
@ -25,8 +24,13 @@ export interface BargraphNode {
|
|||
}
|
||||
|
||||
export class BarGraphFormatter extends RecordFormatter<BargraphNode[]> {
|
||||
@memo({cache: new WeakMap()})
|
||||
cache = new WeakMap();
|
||||
|
||||
override formatFrame(frame: ProfilerFrame): BargraphNode[] {
|
||||
if (this.cache.has(frame)) {
|
||||
return this.cache.get(frame);
|
||||
}
|
||||
|
||||
const result: BargraphNode[] = [];
|
||||
this.addFrame(result, frame.directives);
|
||||
// Remove nodes with 0 value.
|
||||
|
|
@ -55,7 +59,9 @@ export class BarGraphFormatter extends RecordFormatter<BargraphNode[]> {
|
|||
});
|
||||
|
||||
// Sort nodes by value.
|
||||
return Object.values(uniqueBarGraphNodes).sort((a, b) => b.value - a.value);
|
||||
const out = Object.values(uniqueBarGraphNodes).sort((a, b) => b.value - a.value);
|
||||
this.cache.set(frame, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
override addFrame(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ ng_project(
|
|||
),
|
||||
deps = [
|
||||
"//:node_modules/@angular/core",
|
||||
"//:node_modules/memo-decorator",
|
||||
"//:node_modules/rxjs",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-services:theme_rjs",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/record-formatter:record-formatter_rjs",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ ts_project(
|
|||
deps = [
|
||||
"//:node_modules/tslib",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/record-formatter:record-formatter_rjs",
|
||||
"//devtools/projects/ng-devtools/src/lib/vendor/memo-decorator:memo-decorator_rjs",
|
||||
"//devtools/projects/protocol:protocol_rjs",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import {ElementProfile, type ProfilerFrame} from '../../../../../../../../protocol';
|
||||
|
||||
import {memo} from '../../../../../vendor/memo-decorator';
|
||||
import {RecordFormatter} from '../record-formatter';
|
||||
|
||||
export interface TreeMapNode {
|
||||
|
|
@ -20,20 +19,28 @@ export interface TreeMapNode {
|
|||
}
|
||||
|
||||
export class TreeMapFormatter extends RecordFormatter<TreeMapNode> {
|
||||
@memo({cache: new WeakMap()})
|
||||
cache = new WeakMap();
|
||||
|
||||
override formatFrame(record: ProfilerFrame): TreeMapNode {
|
||||
if (this.cache.has(record)) {
|
||||
return this.cache.get(record);
|
||||
}
|
||||
|
||||
const children: TreeMapNode[] = [];
|
||||
this.addFrame(children, record.directives);
|
||||
const size = children.reduce((accum, curr) => {
|
||||
return accum + curr.size;
|
||||
}, 0);
|
||||
return {
|
||||
|
||||
const out = {
|
||||
id: 'Application',
|
||||
size,
|
||||
value: size,
|
||||
children,
|
||||
original: null,
|
||||
};
|
||||
this.cache.set(record, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
override addFrame(
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ ng_project(
|
|||
deps = [
|
||||
"//:node_modules/@angular/core",
|
||||
"//:node_modules/rxjs",
|
||||
"//:node_modules/webtreemap",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording-timeline/record-formatter/tree-map-formatter:tree-map-formatter_rjs",
|
||||
"//devtools/projects/ng-devtools/src/lib/vendor/webtreemap:webtreemap_rjs",
|
||||
"//devtools/projects/protocol:protocol_rjs",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {ProfilerFrame} from '../../../../../../../../protocol';
|
|||
import {Subject, Subscription} from 'rxjs';
|
||||
import {debounceTime} from 'rxjs/operators';
|
||||
|
||||
import {render} from '../../../../../vendor/webtreemap/treemap';
|
||||
import {render} from 'webtreemap/build/treemap';
|
||||
import {TreeMapFormatter, TreeMapNode} from '../../record-formatter/tree-map-formatter';
|
||||
|
||||
@Component({
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
load("//devtools/tools:typescript.bzl", "ts_project")
|
||||
|
||||
package(default_visibility = ["//:__subpackages__"])
|
||||
|
||||
ts_project(
|
||||
name = "memo-decorator",
|
||||
srcs = ["index.ts"],
|
||||
)
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
[](https://travis-ci.org/mgechev/memo-decorator)
|
||||
|
||||
# Memo Decorator
|
||||
|
||||
This decorator applies memoization to a method of a class.
|
||||
|
||||
## Usage
|
||||
|
||||
Apply the decorator to a method of a class. The cache is local for the method but shared among all instances of the class. Strongly recommend you to **use this decorator only on pure methods.**
|
||||
|
||||
Installation:
|
||||
|
||||
```shell
|
||||
npm i memo-decorator --save
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```ts
|
||||
export interface Config {
|
||||
resolver?: Resolver;
|
||||
cache?: MapLike;
|
||||
}
|
||||
```
|
||||
|
||||
- `Resolver` is a function, which returns the key to be used for given set of arguments. By default, the resolver will use the first argument of the method as the key.
|
||||
- `MapLike` is a cache instance. By default, the library would use `Map`.
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
import memo from 'memo-decorator';
|
||||
|
||||
class Qux {
|
||||
@memo({
|
||||
resolver: (...args: any[]) => args[1],
|
||||
cache: new WeakMap(),
|
||||
})
|
||||
foo(a: number, b: number) {
|
||||
return a * b;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
```typescript
|
||||
import memo from 'memo-decorator';
|
||||
|
||||
class Qux {
|
||||
@memo()
|
||||
foo(a: number) {
|
||||
console.log('foo: called');
|
||||
return 42;
|
||||
}
|
||||
|
||||
@memo({
|
||||
resolver: (_) => 1,
|
||||
})
|
||||
bar(a: number) {
|
||||
console.log('bar: called');
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
|
||||
const a = new Qux();
|
||||
// Create a new cache entry and associate `1` with the result `42`.
|
||||
a.foo(1);
|
||||
// Do not invoke the original method `foo` because there's already a cache
|
||||
// entry for the key `1` associated with the result of the method.
|
||||
a.foo(1);
|
||||
// Invoke the original `foo` because the cache doesn't contain an entry
|
||||
// for the key `2`.
|
||||
a.foo(2);
|
||||
|
||||
// Invoke `bar` and return the result `42` gotten from the original `bar` implementation.
|
||||
a.bar(1);
|
||||
// Does not invoke the original `bar` implementation because of the specified `resolver`
|
||||
// which is passed to `memo`. For any arguments of the function, the resolver will return
|
||||
// result `1` which will be used as the key.
|
||||
a.bar(2);
|
||||
|
||||
const b = new Qux();
|
||||
// Does not invoke the method `foo` because there's already an entry
|
||||
// in the cache which associates the key `1` to the result `42` from the
|
||||
// invocation of the method `foo` by the instance `a`.
|
||||
b.foo(1);
|
||||
|
||||
// Outputs:
|
||||
// foo: called
|
||||
// foo: called
|
||||
// bar: called
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
export type Resolver = (...args: any[]) => any;
|
||||
|
||||
export interface MapLike<K = unknown, V = unknown> {
|
||||
set(key: K, v: V): MapLike<K, V>;
|
||||
get(key: K): V;
|
||||
has(key: K): boolean;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
resolver?: Resolver;
|
||||
cache?: MapLike;
|
||||
}
|
||||
|
||||
function memoize(func: Function, resolver: Resolver, cache: MapLike) {
|
||||
const memoized = function () {
|
||||
const args = arguments;
|
||||
// @ts-ignore: ignore implicit any type
|
||||
const key = resolver.apply(this, args);
|
||||
const cache = memoized.cache;
|
||||
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
// @ts-ignore: ignore implicit any type
|
||||
const result = func.apply(this, args);
|
||||
memoized.cache = cache.set(key, result) ?? cache;
|
||||
return result;
|
||||
};
|
||||
memoized.cache = cache;
|
||||
return memoized;
|
||||
}
|
||||
|
||||
const defaultResolver: Resolver = (...args: any[]) => args[0];
|
||||
|
||||
export const memo =
|
||||
(config: Config = {}) =>
|
||||
(_: any, __: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
|
||||
if (typeof descriptor.value !== 'function') {
|
||||
throw new Error('Memoization can be applied only to methods');
|
||||
}
|
||||
|
||||
const resolver = config.resolver ?? defaultResolver;
|
||||
const cache = config.cache ?? new Map();
|
||||
|
||||
descriptor.value = memoize(descriptor.value, resolver, cache);
|
||||
return descriptor;
|
||||
};
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
load("//devtools/tools:typescript.bzl", "ts_project")
|
||||
|
||||
package(default_visibility = ["//:__subpackages__"])
|
||||
|
||||
ts_project(
|
||||
name = "webtreemap",
|
||||
srcs = [
|
||||
"tree.ts",
|
||||
"treemap.ts",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# webtreemap
|
||||
|
||||
> **New 2017-Oct-16**: master is now webtreemap v2, a complete rewrite with
|
||||
> bug fixes, more features, and a different (simpler) API. If you're looking
|
||||
> for the old webtreemap, see the [v1] branch.
|
||||
|
||||
[v1]: https://github.com/evmar/webtreemap/tree/v1
|
||||
|
||||
A simple treemap implementation using web technologies (DOM nodes, CSS styling
|
||||
and transitions) rather than a big canvas/svg/plugin. It's usable as a library
|
||||
as part of a larger web app, but it also includes a command-line app that dumps
|
||||
a self-contained HTML file that displays a map.
|
||||
|
||||
Play with a [demo].
|
||||
|
||||
[demo]: http://evmar.github.io/webtreemap/demo.html
|
||||
|
||||
## Usage
|
||||
|
||||
### Web
|
||||
|
||||
The data format is a tree of `Node`, where each node is an object in the shape
|
||||
described at the top of [tree.ts].
|
||||
|
||||
[tree.ts]: https://github.com/evmar/webtreemap/blob/master/tree.ts
|
||||
|
||||
```html
|
||||
<script src='webtreemap.js'></script>
|
||||
<script>
|
||||
// Container must have its own width/height.
|
||||
const container = document.getElementById('myContainer');
|
||||
// See typings for full API definition.
|
||||
webtreemap.render(container, data, options);
|
||||
```
|
||||
|
||||
### Command line
|
||||
|
||||
```sh
|
||||
$ webtreemap -o output_file < my_data
|
||||
```
|
||||
|
||||
Command line data format is space-separated lines of "size path", where size is
|
||||
a number and path is a '/'-delimited path. This is exactly the output produced
|
||||
by du, so this works:
|
||||
|
||||
```sh
|
||||
$ du -ab some_path | webtreemap -o out.html
|
||||
```
|
||||
|
||||
But note that there's nothing file-system-specific about the data format -- it
|
||||
just uses slash as a nesting delimiter.
|
||||
|
||||
## Development
|
||||
|
||||
### Web piece
|
||||
|
||||
Use `npm run dev` to bring up file watchers that keep the demo JS bundle up
|
||||
to date. Then load `demo/demo.html` in a browser. The file generated by
|
||||
`npm run dev` is also used by the command line app.
|
||||
|
||||
### Command line app
|
||||
|
||||
Use `tsc -w` to keep the npm-compatible JS up to date, then run e.g.:
|
||||
|
||||
```
|
||||
$ du -ab node_modules/ | node build/cli.js --title 'node_modules usage' -o demo.html
|
||||
```
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
/**
|
||||
* Copyright 2019 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Node is the expected shape of input data.
|
||||
*/
|
||||
export interface Node {
|
||||
/**
|
||||
* id is optional but can be used to identify each node.
|
||||
* It should be unique among nodes at the same level.
|
||||
*/
|
||||
id?: string;
|
||||
/** size should be >= the sum of the children's size. */
|
||||
size: number;
|
||||
/** children should be sorted by size in descending order. */
|
||||
children?: Node[];
|
||||
/** dom node will be created and associated with the data. */
|
||||
dom?: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* treeify converts an array of [path, size] pairs into a tree.
|
||||
* Paths are /-delimited ids.
|
||||
*/
|
||||
export function treeify(data: Array<[string, number]>): Node {
|
||||
const tree: Node = {size: 0};
|
||||
for (const [path, size] of data) {
|
||||
const parts = path.replace(/\/$/, '').split('/');
|
||||
let t = tree;
|
||||
while (parts.length > 0) {
|
||||
const id = parts.shift();
|
||||
if (!t.children) t.children = [];
|
||||
let child = t.children.find((c) => c.id === id);
|
||||
if (!child) {
|
||||
child = {id, size: 0};
|
||||
t.children.push(child);
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
if (child.size !== 0) {
|
||||
throw new Error(`duplicate path ${path} ${child.size}`);
|
||||
}
|
||||
child.size = size;
|
||||
}
|
||||
t = child;
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* flatten flattens nodes that have only one child.
|
||||
* @param join If given, a function that joins the names of the parent and
|
||||
* child.
|
||||
*/
|
||||
export function flatten(n: Node, join = (parent: string, child: string) => `${parent}/${child}`) {
|
||||
if (n.children) {
|
||||
for (const c of n.children) {
|
||||
flatten(c, join);
|
||||
}
|
||||
if (n.children.length === 1) {
|
||||
const child = n.children[0];
|
||||
n.id += '/' + child.id;
|
||||
n.children = child.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rollup fills in the size attribute for nodes by summing their children.
|
||||
*
|
||||
* Note that it's legal for input data to have a node with a size larger
|
||||
* than the sum of its children, perhaps because some data was left out.
|
||||
*/
|
||||
export function rollup(n: Node) {
|
||||
if (!n.children) return;
|
||||
let total = 0;
|
||||
for (const c of n.children) {
|
||||
rollup(c);
|
||||
total += c.size;
|
||||
}
|
||||
|
||||
if (total > n.size) n.size = total;
|
||||
}
|
||||
|
||||
/**
|
||||
* sort sorts a tree by size, descending.
|
||||
*/
|
||||
export function sort(n: Node) {
|
||||
if (!n.children) return;
|
||||
for (const c of n.children) {
|
||||
sort(c);
|
||||
}
|
||||
n.children.sort((a, b) => b.size - a.size);
|
||||
}
|
||||
|
|
@ -1,343 +0,0 @@
|
|||
/**
|
||||
* Copyright 2019 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Node} from './tree';
|
||||
|
||||
const CSS_PREFIX = 'webtreemap-';
|
||||
const NODE_CSS_CLASS = CSS_PREFIX + 'node';
|
||||
|
||||
const DEFAULT_CSS = `
|
||||
.webtreemap-node {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
border: solid 1px #666;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
transition: left .15s, top .15s, width .15s, height .15s;
|
||||
}
|
||||
|
||||
.webtreemap-node:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.webtreemap-caption {
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
function addCSS(parent: HTMLElement) {
|
||||
const style = document.createElement('style');
|
||||
style.innerText = DEFAULT_CSS;
|
||||
parent.appendChild(style);
|
||||
}
|
||||
|
||||
export function isDOMNode(e: Element): boolean {
|
||||
return e.classList.contains(NODE_CSS_CLASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options is the set of user-provided webtreemap configuration.
|
||||
*/
|
||||
export interface Options {
|
||||
padding: [number, number, number, number];
|
||||
lowerBound: number;
|
||||
applyMutations(node: Node): void;
|
||||
caption(node: Node): string;
|
||||
showNode(node: Node, width: number, height: number): boolean;
|
||||
showChildren(node: Node, width: number, height: number): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the index of this node in its parent's children list.
|
||||
* O(n) but we expect n to be small.
|
||||
*/
|
||||
function getNodeIndex(target: Element): number {
|
||||
let index = 0;
|
||||
let node: Element | null = target;
|
||||
while ((node = node.previousElementSibling)) {
|
||||
if (isDOMNode(node)) index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a DOM node, compute its address: an array of indexes
|
||||
* into the Node tree. An address [a1,a2,...] refers to
|
||||
* tree.chldren[a1].children[a2].children[...].
|
||||
*/
|
||||
export function getAddress(el: Element): number[] {
|
||||
let address: number[] = [];
|
||||
let n: Element | null = el;
|
||||
while (n && isDOMNode(n)) {
|
||||
address.unshift(getNodeIndex(n));
|
||||
n = n.parentElement;
|
||||
}
|
||||
address.shift(); // The first element will be the root, index 0.
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number to a CSS pixel string.
|
||||
*/
|
||||
function px(x: number): string {
|
||||
// Rounding when computing pixel coordinates makes the box edges touch
|
||||
// better than letting the browser do it, because the browser has lots of
|
||||
// heuristics around handling non-integer pixel coordinates.
|
||||
return Math.round(x) + 'px';
|
||||
}
|
||||
|
||||
function defaultOptions(options: Partial<Options>): Options {
|
||||
const opts = {
|
||||
padding: options.padding || [14, 3, 3, 3],
|
||||
lowerBound: options.lowerBound === undefined ? 0.1 : options.lowerBound,
|
||||
applyMutations: options.applyMutations || (() => null),
|
||||
caption: options.caption || ((node: Node) => node.id || ''),
|
||||
showNode:
|
||||
options.showNode ||
|
||||
((node: Node, width: number, height: number): boolean => {
|
||||
return width > 20 && height >= opts.padding[0];
|
||||
}),
|
||||
showChildren:
|
||||
options.showChildren ||
|
||||
((node: Node, width: number, height: number): boolean => {
|
||||
return width > 40 && height > 40;
|
||||
}),
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
|
||||
export class TreeMap {
|
||||
readonly options: Options;
|
||||
constructor(
|
||||
public node: Node,
|
||||
options: Partial<Options>,
|
||||
) {
|
||||
this.options = defaultOptions(options);
|
||||
}
|
||||
|
||||
/** Creates the DOM for a single node if it doesn't have one already. */
|
||||
ensureDOM(node: Node): HTMLElement {
|
||||
if (node.dom) return node.dom;
|
||||
const dom = document.createElement('div');
|
||||
dom.className = NODE_CSS_CLASS;
|
||||
if (this.options.caption) {
|
||||
const caption = document.createElement('div');
|
||||
caption.className = CSS_PREFIX + 'caption';
|
||||
caption.innerText = this.options.caption(node);
|
||||
dom.appendChild(caption);
|
||||
}
|
||||
node.dom = dom;
|
||||
this.options.applyMutations(node);
|
||||
return dom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of sizes, the 1-d space available
|
||||
* |space|, and a starting rectangle index |start|, compute a span of
|
||||
* rectangles that optimizes a pleasant aspect ratio.
|
||||
*
|
||||
* Returns [end, sum], where end is one past the last rectangle and sum is the
|
||||
* 2-d sum of the rectangles' areas.
|
||||
*/
|
||||
private selectSpan(children: Node[], space: number, start: number): {end: number; sum: number} {
|
||||
// Add rectangles one by one, stopping when aspect ratios begin to go
|
||||
// bad. Result is [start,end) covering the best run for this span.
|
||||
// http://scholar.google.com/scholar?cluster=5972512107845615474
|
||||
let smin = children[start].size; // Smallest seen child so far.
|
||||
let smax = smin; // Largest child.
|
||||
let sum = 0; // Sum of children in this span.
|
||||
let lastScore = 0; // Best score yet found.
|
||||
let end = start;
|
||||
for (; end < children.length; end++) {
|
||||
const size = children[end].size;
|
||||
if (size < smin) smin = size;
|
||||
if (size > smax) smax = size;
|
||||
|
||||
// Compute the relative squariness of the rectangles with this
|
||||
// additional rectangle included.
|
||||
const nextSum = sum + size;
|
||||
|
||||
// Suppose you're laying out along the x axis, so "space"" is the
|
||||
// available width. Then the height of the span of rectangles is
|
||||
// height = sum/space
|
||||
//
|
||||
// The largest rectangle potentially will be too wide.
|
||||
// Its width and width/height ratio is:
|
||||
// width = smax / height
|
||||
// width/height = (smax / (sum/space)) / (sum/space)
|
||||
// = (smax * space * space) / (sum * sum)
|
||||
//
|
||||
// The smallest rectangle potentially will be too narrow.
|
||||
// Its width and height/width ratio is:
|
||||
// width = smin / height
|
||||
// height/width = (sum/space) / (smin / (sum/space))
|
||||
// = (sum * sum) / (smin * space * space)
|
||||
//
|
||||
// Take the larger of these two ratios as the measure of the
|
||||
// worst non-squarenesss.
|
||||
const score = Math.max(
|
||||
(smax * space * space) / (nextSum * nextSum),
|
||||
(nextSum * nextSum) / (smin * space * space),
|
||||
);
|
||||
if (lastScore && score > lastScore) {
|
||||
// Including this additional rectangle produces worse squareness than
|
||||
// without it. We're done.
|
||||
break;
|
||||
}
|
||||
lastScore = score;
|
||||
sum = nextSum;
|
||||
}
|
||||
return {end, sum};
|
||||
}
|
||||
|
||||
/** Creates and positions child DOM for a node. */
|
||||
private layoutChildren(node: Node, level: number, width: number, height: number) {
|
||||
const total: number = node.size;
|
||||
const children = node.children;
|
||||
if (!children) return;
|
||||
// We use box-sizing: border-box so CSS 'width' etc include the border.
|
||||
// With 0 padding we want children to perfectly overlap their parent,
|
||||
// so we start with offsets of -1 (to start at the same point as the
|
||||
// parent) and create each box 1px larger than necessary (to make
|
||||
// adjoining borders overlap).
|
||||
|
||||
let x1 = -1,
|
||||
y1 = -1,
|
||||
x2 = width - 1,
|
||||
y2 = height - 1;
|
||||
|
||||
const spacing = 0; // TODO: this.options.spacing;
|
||||
const padding = this.options.padding;
|
||||
y1 += padding[0];
|
||||
if (padding[1]) {
|
||||
// If there's any right-padding, subtract an extra pixel to allow for the
|
||||
// boxes being one pixel wider than necessary.
|
||||
x2 -= padding[1] + 1;
|
||||
}
|
||||
y2 -= padding[2];
|
||||
x1 += padding[3];
|
||||
|
||||
let i: number = 0;
|
||||
if (this.options.showChildren(node, x2 - x1, y2 - y1)) {
|
||||
const scale = Math.sqrt(total / ((x2 - x1) * (y2 - y1)));
|
||||
var x = x1,
|
||||
y = y1;
|
||||
children: for (let start = 0; start < children.length; ) {
|
||||
x = x1;
|
||||
const space = scale * (x2 - x1);
|
||||
const {end, sum} = this.selectSpan(children, space, start);
|
||||
if (sum / total < this.options.lowerBound) break;
|
||||
const height = sum / space;
|
||||
const heightPx = Math.round(height / scale) + 1;
|
||||
for (i = start; i < end; i++) {
|
||||
const child = children[i];
|
||||
const size = child.size;
|
||||
const width = size / height;
|
||||
const widthPx = Math.round(width / scale) + 1;
|
||||
if (!this.options.showNode(child, widthPx - spacing, heightPx - spacing)) {
|
||||
break children;
|
||||
}
|
||||
const needsAppend = child.dom == null;
|
||||
const dom = this.ensureDOM(child);
|
||||
const style = dom.style;
|
||||
style.left = px(x);
|
||||
style.width = px(widthPx - spacing);
|
||||
style.top = px(y);
|
||||
style.height = px(heightPx - spacing);
|
||||
if (needsAppend) {
|
||||
node.dom!.appendChild(dom);
|
||||
}
|
||||
|
||||
this.layoutChildren(child, level + 1, widthPx, heightPx);
|
||||
|
||||
// -1 so inner borders overlap.
|
||||
x += widthPx - 1;
|
||||
}
|
||||
// -1 so inner borders overlap.
|
||||
y += heightPx - 1;
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
// Remove the DOM for any children we didn't visit.
|
||||
// These can be created if we zoomed in then out.
|
||||
for (; i < children.length; i++) {
|
||||
if (!children[i].dom) break;
|
||||
children[i].dom!.parentNode!.removeChild(children[i].dom!);
|
||||
children[i].dom = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the full treemap in a container element.
|
||||
* The treemap is sized to the size of the container.
|
||||
*/
|
||||
render(container: HTMLElement) {
|
||||
addCSS(container);
|
||||
const dom = this.ensureDOM(this.node);
|
||||
const width = container.offsetWidth;
|
||||
const height = container.offsetHeight;
|
||||
dom.onclick = (e) => {
|
||||
let node: Element | null = e.target as Element;
|
||||
while (!isDOMNode(node)) {
|
||||
node = node.parentElement;
|
||||
if (!node) return;
|
||||
}
|
||||
let address = getAddress(node);
|
||||
this.zoom(address);
|
||||
};
|
||||
dom.style.width = width + 'px';
|
||||
dom.style.height = height + 'px';
|
||||
container.appendChild(dom);
|
||||
this.layoutChildren(this.node, 0, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms the treemap to display a specific node.
|
||||
* See getAddress() for a discussion of what address means.
|
||||
*/
|
||||
zoom(address: number[]) {
|
||||
let node = this.node;
|
||||
const [padTop, padRight, padBottom, padLeft] = this.options.padding;
|
||||
|
||||
let width = node.dom!.offsetWidth;
|
||||
let height = node.dom!.offsetHeight;
|
||||
for (const index of address) {
|
||||
width -= padLeft + padRight;
|
||||
height -= padTop + padBottom;
|
||||
|
||||
if (!node.children) throw new Error('bad address');
|
||||
for (const c of node.children) {
|
||||
if (c.dom) c.dom.style.zIndex = '0';
|
||||
}
|
||||
node = node.children[index];
|
||||
const style = node.dom!.style;
|
||||
style.zIndex = '1';
|
||||
// See discussion in layout() about positioning.
|
||||
style.left = px(padLeft - 1);
|
||||
style.width = px(width);
|
||||
style.top = px(padTop - 1);
|
||||
style.height = px(height);
|
||||
}
|
||||
this.layoutChildren(node, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/** Main entry point; renders a tree into an HTML container. */
|
||||
export function render(container: HTMLElement, node: Node, options: Partial<Options>) {
|
||||
new TreeMap(node, options).render(container);
|
||||
}
|
||||
|
|
@ -125,7 +125,6 @@
|
|||
"karma-requirejs": "^1.1.0",
|
||||
"karma-sourcemap-loader": "^0.4.0",
|
||||
"magic-string": "^0.30.8",
|
||||
"memo-decorator": "^2.0.1",
|
||||
"ngx-flamegraph": "0.1.1",
|
||||
"ngx-progressbar": "^14.0.0",
|
||||
"open-in-idx": "^0.1.1",
|
||||
|
|
|
|||
|
|
@ -263,9 +263,6 @@ importers:
|
|||
magic-string:
|
||||
specifier: ^0.30.8
|
||||
version: 0.30.17
|
||||
memo-decorator:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
ngx-flamegraph:
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1(@angular/common@packages+common)(@angular/core@packages+core)
|
||||
|
|
@ -16687,10 +16684,6 @@ packages:
|
|||
tslib: 2.8.1
|
||||
dev: false
|
||||
|
||||
/memo-decorator@2.0.1:
|
||||
resolution: {integrity: sha512-Cydoauo7y1Uad1UuznJqhuEQCt6adIl1w5ik3WmNl4FJeBmWAaMs64qyGRahaXWK/Dlmt/+QNesRTeFUcpJPkQ==, tarball: https://registry.npmjs.org/memo-decorator/-/memo-decorator-2.0.1.tgz}
|
||||
dev: false
|
||||
|
||||
/memoizeasync@1.1.0:
|
||||
resolution: {integrity: sha512-HMfzdLqClZo8HMyuM9B6TqnXCNhw82iVWRLqd2cAdXi063v2iJB4mQfWFeKVByN8VUwhmDZ8NMhryBwKrPRf8Q==, tarball: https://registry.npmjs.org/memoizeasync/-/memoizeasync-1.1.0.tgz}
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -12153,11 +12153,6 @@ memfs@^4.6.0:
|
|||
tree-dump "^1.0.1"
|
||||
tslib "^2.0.0"
|
||||
|
||||
memo-decorator@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/memo-decorator/-/memo-decorator-2.0.1.tgz#599db686337a53af3e2f59991f5f61b96e96c62b"
|
||||
integrity sha512-Cydoauo7y1Uad1UuznJqhuEQCt6adIl1w5ik3WmNl4FJeBmWAaMs64qyGRahaXWK/Dlmt/+QNesRTeFUcpJPkQ==
|
||||
|
||||
memoizeasync@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/memoizeasync/-/memoizeasync-1.1.0.tgz#9d7028a6f266deb733510bb7dbba5f51878c561e"
|
||||
|
|
|
|||
Loading…
Reference in a new issue