Perspective table header filter dropdowns - matching column widths

While we wait for table column filters (like Excel has) and before I try to code this myself, has anyone created something that will have the dropdown widths stay in alignment with the Perspective table columns?

A quick test shows that I can set the drop-box width to track the column width of fixed width columns. Usually we would leave some of the columns to resize with the browser. How did you handle the dropdown width for these columns?

Thanks.

1 Like

I am also needing this feature.

A filter dropdown on table's column, with unique values on the dropdown options.
To select which row to show.

Any workaround?

Nope.

I am trying to filter column of table, using dropdown, without using DB Query.
Thinking out loud.
I can extract data of one column as list. Get the unique values from list and feed this to dropdown.
Dropdown value will be bind to column's string filter.

That will work. You already have all the column data in the table's dataSource property. You can script the binding on each dropdown to extract the unique values in each column. The only problem is aligning the dropdowns with the columns. (They don't have to though.)

Tip: binding the props of one component to another can make your application "brittle". Move a component in or out of a container and the relative path will change and the binding will break.
A way to avoid that is to generate the data in a view.custom.<propName>. Then you can bind the table's dataSource prop and all the dropdowns' option binding (with transform) to the custom property. Now you can move stuff freely as the bindings are using the absolute path to view.custom.<propName>.

Figure 1. Filter dropdowns and labels. Dotted borders are enabled on the main flex row and embedded flex columns to give a better idea of the structure.

3 Likes

That is awesome.
My filter is on the popup though, send message on change,and updates the view.custom.filter property of table.

I agree its a challenge or no way to align.
Perhaps if individual columns are on individual flex. It could. Just tedious.

I noted this. As much as possible put source of data in view.custom.<propName>
If the data source comes from a component's property within the view. Writing that data to view.custom.<propName> will still break the binding when you move that component.

Therefore, when the data source is a component's property, just bind to it directly.. Unless of course if you want to use message handler.

Not if the binding is on the component, and is bidirectional.

With the few exceptions for input data and selection data, this is an anti-pattern. Data should "flow" from view parameters to view custom props to component custom props to component display props.

Does that mean, writing an "On property change" scritp->write to view.custom.<propName> is better practice than binding view.custom.<propName> via relative path to that component's property.

You should never be binding "inward". That is, view custom properties should only be bound to view parameters or other view custom properties or queries or indirection computed from view props. Any other "writes" to view custom properties should come from a bidirectional binding elsewhere, or a script assignment as you suggest. Use bindings before scripts, though, where applicable.

Aye. Noted, that is clear. I don't know if I inadvertently do that. I will observe this coding next time.

Recap:
I now have a dropdown that filters table to show only rows according to desired column data.
This is done without using query, but table's column filter feature.

Right now, I am working on visual for smaller screen (mobile).
I use breakpoint. And instead of table, I use custom made card and display them thru a flex repeater.
This time how do I apply filter to only show cards with desired values.
Is it possible to bind this card repeater to a hidden "Filtered" table.

Are there any other options?

I would use the where() expression function from my Integration Toolkit module.

Bind the data to a custom property, then bind the instances property of the flex repeater to that custom property AND the dropdown with a structure binding.
Add a transform script to filter the data based on the dropdown's value.

Example with the sample data the perspective table comes with, if you wanted to filter by country:

return filter(lambda row: row['Country'] == value.dropdown, value.data)

Adapt the lambda to your needs. If the filtering is too complicated for a lambda to be practical, define a function in your script library and pass it instead.

I can follow the use of filter and lambda, but I don't follow the arguments, and what is returned.

That gave me an idea to build on:

I have two dropdown, location and status. ddLocation and ddStatus that act as a filter.
And a dataset ds.

I join them via Structure Binding with below transform

If there's more elegant code to do this, I am all ears.

To make it perfectly clear:

Your filter needs to filter on BOTH status and location, with EXACT matches.
Which means that anything with a matching status, but that does not match the selected location, will be removed, and the other way around as well.
Is that right ?

In this case:

return filter(
    lambda row: row['Location'] == value.ddLocation and row['Status'] == value.ddStatus,
    system.dataset.toPyDataSet(value.ds)
)

Note that if your data comes from a query binding, you can set its return format to json (in the view.custom.ds property's binding), and then you don't need the conversion to a pyDataSet.

If you need to filter on EITHER status or location, instead of both, replace the and in the lambda with an or.
If you need partial matches instead of exact matches, you can check for value.status in row['status'] instead of using ==.
If you need more complex filtering, it can also be arranged.

The lambda will return True or False. Its arguments are whatever iterating over data will produce. In this case, rows of the dataset.
filter will keep only the rows where the lambda returned True.
So you check each row against the dropdowns' values, in a way that it returns True for the rows you want to keep.

1 Like

Thanks. This worked!
I was thinking hard, where would I define row dictionary..
and then, I was like How...??? Wait What.... No way..! Wow.
Seems that the row dictionary implicitly defined:

pyDs = system.dataset.toPyDataSet(ds)
return filter(lambda row: row['Location']==ddLocation 
and row['Status']==ddStatus, pyDs)

This filter worked. I will need to add logic if ddLocation is empty, it should not evaluate filter for ddLocation. Othewise, filter will not return anything as there is no empty string on pyDs.

I'm not sure what you mean by that, but it looks like you're under the impression there's an implicit row defined somewhere. There's not.
in lambda row: ..., row is just the parameter to the lambda. It's like function parameters. You can name it whatever you want.

Maybe using a named function would make things clearer:

def filter_func(row):
    return row['Location'] == value.ddLocation and row['Status'] == value.ddStatus

return filter(filter_func, value.data)
1 Like

This is what I meant:
image
before using row['Location'], we must define row={}?
else how will python now that there is "row" dictionary exist?

I was thinking since we pass pyDs, in reality python is doing the below in the background:

for row in pyDs:
    #do action

That's kind of what we do when we write lambda row: dothings(row). It says "here's an unnamed function, that takes a parameter. Call that parameter 'row' and pass it to dothings".
How/why is it a dict ? It's actually not a dict, but that's not relevant here. Let's consider it's a dict.
filter will iterate through the data we give it. Here's that data is your pyDataSet. Iterating through a data structure means taking its elements one by one. So, it takes the elements of your pyDataSet, which we'll consider as if they were dicts.
Then, filter passes those elements to the filtering key. The key parameter is the first one. In this case, we pass a lambda as argument. A lambda is an unnamed function, which means it can take parameters. The parameter it receives for each iteration, is the current element from data.
You can think of it this way:

results = filter(filter_func, data)

is equivalent to

results = [element for element in data if filter_func(element)]

which is another way of doing this:

results = []
for element in data:
    if filter_func(element):
        results.append(element)

Here, element is what we called row before. It doesn't appear in the filter version because the filter_func argument is not a lambda. Note that nowhere in those 3 examples was filter_func defined, so obviously you'd need to define it for the examples to work. You could create that function, or... do this instead:

results = [element for element in data if element > 5]

results = []
for element in data:
    if element > 5:
        results.append(element)

BUT filter expects the key to be callable. Which is why we use a lambda, to make element > 5 callable:

results = filter(lambda element: element > 5, data)

element will be whatever items in data are. Let's say data is a string instead, then the key will receive the characters from that string, one after the other. For example:

vowels = filter(lambda c: c in "aeiouy", "FooBarBazPoxWuz")
# vowels contains o, o, a, a, o, u

If you make data a list of integers, the key will receive those integers, one at a time:

odds = filter(lambda n: n % 2 != 0, [1, 2, 3, 4, 5, 6, 7, 8, 9])
# odds contains 1, 3, 5, 7, 9

Note that what we call the lambda's parameter doesn't matter. It's not because we call it row that it will be a row from data. You can call it "shrinked_head" or "sunshine", it doesn't matter.

I hope I made things clearer and not the opposite :X

TL;DR:

Yea, pretty much.

1 Like