Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Suggestion] Collapse chart node by default if children > 5 #233

Open
kevinlaw91 opened this issue Oct 20, 2016 · 7 comments
Open

[Suggestion] Collapse chart node by default if children > 5 #233

kevinlaw91 opened this issue Oct 20, 2016 · 7 comments

Comments

@kevinlaw91
Copy link

This will make the chart easier to navigate and read. Better if we can set the threshold in devtools options too.

@zalmoxisus
Copy link
Owner

zalmoxisus commented Oct 20, 2016

@kevinlaw91, thanks for the suggestion. I see why you want this.

map2tree gives us an array of children, so it wouldn't be difficult to implement it in d3-state-visualizer. The problem is to persist nodes uncollapsed by the user when rendered the previous state as per reduxjs/redux-devtools-chart-monitor#5.

@zalmoxisus
Copy link
Owner

Now it's possible to navigate only a subtree (see the release notes). However, setting a threshold would be still useful. If you want to fix that issue in redux-devtools-chart-monitor let me know, I'd help with it and would add the option here.

@G-Rath
Copy link

G-Rath commented Feb 26, 2018

Having this option should would be great: We're working with large arrays in our store, that perform fine, but cause the dev tools to lag a lot.

This is made worse by the fact that we're also dealing with audio data; the dev tools chart just becomes unusable as it tries to chart a UInt8 array with 20960 entries, when we don't ever actually need to see that bit of information.

Having the dev tools collapse everything by default would be a nice immediate fix, but the ability to customise the number of nodes for triggering auto collapsing would be a fantastic addition.

I can also think of a couple of extra additions that would help with this problem, such as having objects in arrays collapsed by default (as it's very common on our project to be interested in a single user out of a 100, that are stored in a map that has the users id as the key).

Another possible cool feature would a shouldCollapse function, that gets passed the nodes "property chain" (for lack of a better term?) as a string, so you could do things like shouldCollapse: chain => chain === 'state.services.data.users'.

I've just implemented a custom stateSanitizer that uses an array of "property chain" strings (e.g. "state.service.data") which is expanded dynamically (handling arrays and maps just fine), for targeting properties to null in the store.

For example, 'services.data.users.{}.creation' results in the sanitizer setting the value of 'creation' in every user object to 'CLEARED'.

You could utilize this code in dev tools to allow targeted collapsing: By providing an array of "property chain" strings to composeWithDevTools, the dev tools could then use the code to test if nodes should be collapsed by default.

Let me know if you're interested @zalmoxisus and I'll chuck the stateSanitizer code into a new issue for discussion and implementation. Otherwise, just having a basic collapse by default setting would be a fantastic immediate relief :)

@scriby
Copy link

scriby commented May 11, 2018

I ran into this as well... it's really hard to navigate large state trees. Even just having a command to collapse all the nodes would be really helpful.

@G-Rath
Copy link

G-Rath commented May 12, 2018

Our application had a map of users ({ [userId]: user}), with each user having their last transaction, which in turn has the message of that transaction, that in turn contains a speech property, which is an array/buffer containing synthaised speech audio from aws-polly.

Here's a simplified slice of how our state looked:

{
    services: {
        data: {
            users: {
                1: {
                    lastTransaction: {
                        /* ... other properties ... */
                        message: {
                            speech: [
                                /* ~1000 elements */
                            ]
                        }
                        /* ... other properties ... */
                    }
                },
                2: {
                    lastTransaction: {
                        /* ... other properties ... */
                        message: {
                            speech: [
                                /* ~1000 elements */
                            ]
                        }
                        /* ... other properties ... */
                    }
                },
                3: {
                    lastTransaction: {
                        /* ... other properties ... */
                        message: {
                            speech: [
                                /* ~1000 elements */
                            ]
                        }
                        /* ... other properties ... */
                    }
                }
                /* ... and so on ... */
            }
        }
    }
}

This made the devtools pretty unusable, since the average speech property would have something like 1000 elements to it, that the devtools would try to render.

Since we didn't actually need to the speech the property in that part of the state (doubly so when working in development), I built a custom stateSanitizer that takes an array of 'property path strings' to determine what properties on the store to touch, with support for 'dynamic' properties (objects & arrays).

Currently it exploits the fact that objects are passed by reference to clear properties on the state, but I feel it should be pretty straightforward to utilize this into devtools as a means of calculating which state properties to collapse by default in a manner that makes it super easy for us developers to configure.

Here is what the property path string looks like to target the annoying speech property I mentioned above, which I'll explain more in a moment: 'services.data.transactions.{}.lastTransaction.message.speech'

(You can easily get the path property of a property via the Chrome console - More on this at the end).

Here is the stateSanitizer itself:

const composeDevTools = process.env.NODE_ENV === 'production'
    ? args => args
    : composeWithDevTools({
        stateSanitizer: state => {
            // todo: optimize, comment & document
            const statePropsToDelete = [
                'services.data.users.{}.lastTransaction.message.speech'
            ];

            statePropsToDelete.forEach(propertyChain => {
                const properties = propertyChain.split('.');

                const chains = [[]];

                for (let i = 0; i < properties.length; i++) {
                    const propName = properties[i];

                    chains.forEach(chain => {
                        if (propName === '{}' || propName === '[]') {
                            const keys = Object.keys(chain.reduce((obj, name) => obj[name], state));

                            keys.forEach(key => chains.push([].concat(chain).concat(key)));

                            chain.length = 0;

                            return;
                        }

                        if (chain.reduce((obj, name) => obj[name], state)[propName]) {
                            chain.push(propName);

                            return;
                        }

                        chain.length = 0;
                    });
                }

                chains.forEach(chain => {
                    if (chain.length) {
                        const lastProperty = chain[chain.length - 1];

                        const reduced = chain.slice(0, -1).reduce((obj, name) => obj[name], state);

                        let clearValue = '';

                        if (Array.isArray(reduced[lastProperty])) {
                            clearValue = [];
                        }

                        reduced[lastProperty] = clearValue;
                    }
                });
            });

            return state;
        }
    });

Note that I'm going to focus mainly on the function inside of the statePropsToDelete.forEach, since the fact that statePropsToDelete is an array doesn't really factor into the rest of the sanitisers logic; there no conditions based on the array itself - it's all about the elements of the array.

The stateSanitizer is made up of two parts, with both parts taking a value that acts as a descriptor of the of the property accessors that must be used in order to reach the desired property on the state that should be actions (which I refer to as a 'property accessor chain').

The first part takes a string (in classic dot notation format), while the second part takes an array.

The second part (which I refer to as the Actioner) doesn't really matter much: It accepts an array of strings, with the elements of the array being the names of the properties that should be traversed in order to reach the desired target property on the state. We use a .reduce with the state as the initial reduce value to traverse down the state towards the parent object that has the property we want to action.

Once we've got the reference to that parent object, we can simply assign an empty string to it, and all is well.

The actual code here is boring; The main important thing about the Actioner is that it takes an array of strings looking like this:
["services", "data", "transactions", "1", "lastTransaction", "message", "speech"]

that it uses to get to the property it needs to action. This is the part in devtools that would handle doing the actual collapsing; the cooler part is how those string arrays are built, since we want to support targeting properties that are stored inside arrays and maps without having to explicitly define the property accessors.

And that is exactly what we do in the first part of the sanitizer (which I refer to as the Expander):

The Expander takes a string that describes (using dot notation) the names of the properties that should be traversed in order to reach the desired target property on the state. The Expander 'expands' a given string in an array of arrays (with the elements of the array being the names of the properties that should be traversed in order to reach the desired target property on the state - sound familiar?).

It does this by splitting the given string into an array of properties based on the position of the dot character (const properties = propertyChain.split('.')), and building an array of chains by iterating over the splits resulting substrings.

As it iterates, the Expander tests the current property with the chain being built against the state: if it can't access a property at any point, it throws out the chain (meaning that you don't have to worry about errors arising by non-existing properties on the state).

The other thing the Expander does is handle maps & arrays. If the Expander encounters a substring with the value {} or [], it'll iterator over all the keys of the currently accessible property, creating a new chain for each value.

This works because as the Expander iterators over the properties, it adds those property names to each existing chain in the chains array.

Once the Expander has finished, each array in the chains array is passed to the Actioner, which actions each property on the state.

Heres an example:

Given the state I give at the start, we could target the speech property with the given string:
services.data.users.{}.lastTransaction.message.speech

This string is split:
["services", "data", "users", "{}", "lastTransaction", "message", "speech"]

It's then passed to the expander, which generates the following:

[
    [],
    ['services', 'data', 'users', '1', 'lastTransaction', 'message', 'speech'],
    ['services', 'data', 'users', '2', 'lastTransaction', 'message', 'speech'],
    ['services', 'data', 'users', '3', 'lastTransaction', 'message', 'speech']
];

(Note and ignore the empty array - it's there as a result because the Expander can't pop out a 'broken' chain while it's iterating over chains, as that would be a concurrent modification. There's really no point in bothering to filter them out, when instead the Actioner can just check that a chain has a length before bothering to try and follow the chain).

Each one of these are passed to the Actioner, which then sets all the speech properties to an empty string.

Last-minute notes:

  • It doesn't matter if you use [] or {} - they'll both work, since Object.keys works regardless of if it's given an array or object.
  • You can target specific elements in arrays and objects, since it's all about properties: foo.1.bar will target the bar property of the object stored in index 1 of the array foo.

Hopefully the above makes sense - I spent a bit of time on that explanation, so I welcome feedback :)
(I've also likely made a mistake somewhere in my explanation, given how much I rewrote it)

I'd be very keen to see this implemented into the devtools, as I think it would be a really powerful feature.

I'm happy to take a crack at it myself - I suspect it'll either be very easy or very hard to implement the above code, as it depends a lot on when, where and how the devtools handles collapsing of elements.

Either Way, hopefully the above is useful for someone out there.

Finally: The Chrome Console can provide you with property paths that you can use with this. All you do is just right click on a property of an object outputted by the console - simple.

You can try it out by just copying the state example I gave at the start of the comment, paste it into the chrome console, expand the output and then right click on any property and click 'copy property path'.

The only thing you have to do is to replace any array and object property accessors with [] & {}.

For example, with the state object from the start, Chrome will give .services.data.users[1].lastTransaction.message.speech. This has to be changed to be .services.data.users.{}.lastTransaction.message.speech.

Let know what you think of all this @zalmoxisus - I'm interested in hearing your thoughts :)

@djohns2021
Copy link

Commenting to +1 this suggestion. Would be wildly helpful for viewing a more normalized state.

@finnmerlett
Copy link

+1 here! This would still be really useful to me, and make navigating a state involving long arrays significantly easier and less frustrating!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants