The Abacus Panels component provides a way to layout content. We have two main goals for this component. First, we want to easily layout multiple components or elements with a minimum of fuss. For example, we may want to neatly place 3 components, one on the top and two below. Second, we may want to have a primary component (or two) that is always visible and then a side panel (or two, or three) that can be visible or invisble. Most UI frameworks that have a panels component allow only two panels, but each panel can of course be divided again into two panels. This is sort of the way splitters work in ⎕WC. We have taken a different approach.
We use CSS grid and one look at the grid-template-areas property should tell you how nice a fit it is for the new array notation in Dyalog v20.
For example:
...
l←[
0 0
1 2
3 3
]
p←A.Panels.New ('Content' c) ('GridTemplateRows' l)
There are four components or elements in the content c. This instructs Abacus to layout the first element across the entire parent component, the place the next two elements below that, in equal proportion, and finally the last component all the way across the bottom.
The values in l are simply indices into c, but we could use HTML ids as well. We can change the proportions of the components by changing l:
l←[
0 0 0 0
1 1 1 2
3 3 3 3
]
Consider a case of two components, a main component and a right-hand sidebar that should only be visible on demand. On startup the GridTemplateAreas is:
[
0
]
(or simply ⍪0) a 1 by 1 matrix. This indicates that the main component should take up all of the space. When the side bar is reqested we reset it to:
[
0 0 0 1
]
This makes the sidebar visible, and gives it 25% of the available space, shrinking the main component to 75% of the available space.
Panels can be resized, if desired and applicable, using keyboard shortcuts for now. At some point we will allow resizing via the mouse.
We just encountered the following real-life problem, which oddly, if memory serves, we have never encountered before: Given a list of items, find the location of each item in a list of lists of items. An example will make it clear:
a← 'abc' 'de' '' 'fgha'
w←'hafxd'
a {...} w
3 0 3 4 1
So, it's just like Index Of (⍳), only we are looking a level deeper on the left for the existence of an item on the right. We are not concerned where in the sublist on the left the item on the right is found. It is simply the location of the sublist (where the item is found) within the main list.
Our first inclination is to flatten out a, and then lookup items in w in the new flat array:
⊃,/a
abcdefgha
(⊃,/a)⍳w
7 0 5 9 3
Then we need to map these indices, which index into the flattened array, back to indices apropriate for the original nested array, which are given by:
⍳≢a
0 1 2 3
We can do this by counting the items in each sublist:
≢¨a
3 2 0 4
And then replicating:
(≢¨a)/⍳≢a
0 0 0 1 1 3 3 3 3
Now we can map the first set of indices into the second set of indices to get the desired indices:
(⊂(⊃,/a)⍳w)⌷(≢¨a)/⍳≢a
INDEX ERROR
Oops, we need one more index for things not found:
(1,⍨≢¨a)/⍳1+≢a
0 0 0 1 1 3 3 3 3 4
And now, with a little code golf to remove a set of parentheses:
(⊂w⍳⍨⊃,/a)⌷(1,⍨≢¨a)/⍳1+≢a
3 0 3 4 1
Bingo, we are done. Let's make it a dfn:
liota←{(⊂⍵⍳⍨⊃,/⍺)⌷(1,⍨≢¨⍺)/⍳1+≢⍺}
Now let's try a completely different approach. Consider the outer product of membership (∊):
w∘.∊a
0 0 0 1
1 0 0 1
0 0 0 1
0 0 0 0
0 1 0 0
If we look for the first occurance of a 1 in each row we get our answer:
(↓w∘.∊a)⍳¨1
3 0 3 4 1
And a little golf:
1⍳⍨¨↓w∘.∊a
3 0 3 4 1
For what it is worth, we can get rid of nesting the Boolean matrix and the each operator by using the rank operator , un-golfing in the process:
1⍳⍨(⍤1)w∘.∊a
3 0 3 4 1
Maybe we can go tacit. It looks like we have a function between a and w, and then a function on the result, that is:
g a f w
Which can be rewritten as an atop:
a (g f) w
Where:
f←∘.∊⍨
g←⍳∘1⍤1
Let's see if it works:
a (g f) w
3 0 3 4 1
Oh yeah! Now we can just combine into one function:
liota2←⍳∘1⍤1∘.∊⍨
a liota2 w
3 0 3 4 1
We can guess that while the tacit version is short and sweet, it's going to be a dog in terms of time and space due to the outer product when both arguments get large, and indeed the flat version is almost infinitely faster. That being said, in our use case, neither argument will ever be very large.
Update
Josh David provides a much nicer flat array solution using interval index that is both shorter and faster:
{1+(+\≢¨⍺)⍸⍵⍳⍨⊃,/⍺}
It's amazing the various uses of ⍸.
Back when we first were designing the DataGrid we thought we would leave filtering and sorting up to the application. The problem with that approach is that it requires an extra copy of the data. For example, if we want the data sorted by a particular column or set of columns, we need to sort the data outside in the application and reset the DataGrid properties. This creates a whole new copy of the data. Same goes for basic filtering: if we want to select certain rows to display, we need to select the rows in the application and reset the DataGrid properties. This is not good.
It turns out the way we have implemented the grid makes it fairly easy to build in sorting and filtering. Assume the data is a matrix, m. Then at any given moment, the rows that are to be displayed in the available window space are given by a vector of (up till now) consecutive integers i:
m[i;]
If the user is on the bottom visible row and scrolls down one, then i will be effectively set to i+1. But there is no reason that i needs to be consecutive integers. We can display certain rows in a certain order by simply messing around with i.
In the DataGrid we now track an internal property RowIndices. This defaults to ⍳n where n is the number of data rows. That is, the default is to show all rows in the order given. We can pick out a subset of indices and permute them, assign this to RowIndices and thus display a subset of data in a different order:
m[RowIndices[i];]
And that's all there is to built-in sorting and filtering. The entire dataset is never sorted, nor is it ever selected out or copied. We limit built-in filtering to selecting where a column is equal to a particular value, or set of values. We can of course filter on multiple columns. More sophisticated filtering, for example selecting rows where a column is greater than a certain value, is left up to the application. There is a new RowMask property, a Boolean of the same length as the data. This allows the application to provide the entire dataset once to the DataGrid once, and then reset the visible rows. Built-in filtering can work on top of application filtering.
We only allow sorting and filtering if the property InsertRows is set to 0, that is the total number of rows cannot be changed. We may relax this in the future.