-
-
Notifications
You must be signed in to change notification settings - Fork 22
Make lambdas able to change other trace attributes #130
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
Comments
What I envision: in https://github.com/dbuezas/lovelace-plotly-graph-card/blob/master/src/plotly-graph-card.ts#L585 check for an The user would thus specify the lambda
It would not be very compatible with the solitary points patching because of the index shifting, but maybe in those cases it should be the users responsibility to cope (by ensuring there are no such points to patch maybe). Another possible API would be to just accept whatever member of the returned object and merge that directly instead of a sub-member. In that case the lambda would be:
|
Good idea! I'm also thinking about passing more stuff as parameter (like the visible range) |
I suppose that having lambdas everywhere is a good way to go too. I looked at your branch and it is clever, but a) you test twice for strings starting with |
I've been moving to show the errors in the screen to improve the yaml editing experience. I think in the experimental branch I used a prefix to indicate "this is a function", kind of like |
You used the prefix |
One thing that I haven't quite figured out yet, are the parameters to pass to the lambdas. |
It would be really great and add so much more flexibility if attributes/parameters could be specified dynamically bases on the trace data or other HA entity state/attributes. That would be very useful for x/y range limits, positions of annotations / shapes / images ... to name a view. |
I agree! I actually never moved forward for that because I assumed almost nobody would go this deep. I'm pleased to see how much I underestimated this card's users!
Correct
You can do that already with:
|
Btw, I welcome yaml examples of how you envision the API. entities:
- entity: sensor.my_other_sensor
fn: ({x, y, attributes, ...a_lot_more, vars, ... }) => {
vars.maxOther = Math.max(...y) // not returning anything has no effect
}
- entity: sensor.mysensor
y: $fn ({x, y, attributes, ...a_lot_more, vars, ... }) =>
parseFloat(y)*2 + vars.maxOther
- entity: sensor.mysensor_2
fn: ({x, y, attributes, ...a_lot_more, vars, ... }) => ({
y: parseFloat(y)*2 + vars.maxOther // returns an object, so this is merged with the entity
})
title: $fn ({hass}) => hass.states["sensor.any_sensor"].state # that's the current state
layout:
yaxis:
range: $fn ({states}) => [Math.min(...states["sensor.mysensor"], 100] |
The one thing I need to figure out is an elegant way to handle merging for "root lambdas", where the key is entities:
- entity: sensor.my_other_sensor
name: name1 # lambda overrides yaml
fn: |-
({x, y, attributes, ...a_lot_more, vars, ... }) => ({
name: name2,
}) entities:
- entity: sensor.my_other_sensor
fn: |-
({x, y, attributes, ...a_lot_more, vars, ... }) => ({
name: name,
})
name: name2 # <-- yaml overrides lambda Probably I just need to construct a new object, deep merging each key at time |
That looks really cool! |
Regarding the lambdas: Thinking more about my question if Jinja2 templates could be used, I realise that in most cases lambdas would be the best way to do it. This is because in most cases the data series contain historical data which is not that easy to access via Jinja2 templates. One would have to issue SQL queries to do that. So yes, I think lambdas are the way to go. From your examples above, it looks like your plan is to provide 1 lambda function which would output all the different attributes. With this approach you have the problem to merge the attributes specified in the lambda with attributes specified in YAML.
What about leaving the attributes in YAML exclusively and use multiple lambdas to modify each?
Then you don't have to merge. You just take the static value or evaluate the lambda. It gets a bit trickier in the other sections such as layout or shapes. In one of your examples from above you had
What is states? An array which contains the y data of all traces which are using the first y axis?
Maybe that could also be useful for plotting 1 sensor versus another one, e.g.
|
The plan is to do both:
|
That's the plan, one of the params will have all fetched data. There are a couple of issues, because multiple series can ve rxtracted from the entity name may (statistics, different periods, states and attributes) |
You nailed it. That's exactly why I originally started playing with the idea, driven by this request #21 |
How do you plan to solve the chicken-and-egg problem, that is if some lambda function sets something which changes the visible range thus the fetched data and may thus change what the lambda would return... Also, I think that having separate attributes being able to be computed with the If the only mechanism we have is And the solution to say "just make the parent object a And it is easier to just merge the lambda result's attributes (if the result is not iterable, as is currently checked). This is a low-risk and small change. |
Isn't it better, easier, and cleaner to only stick with the first? |
Yeah, you're probably right. One can still return an object in the parent element if that's needed |
With the recent filters work, I think that it would be better to merge into the trace a A bit of a chicken-and-egg problem: if one returns |
Ohhh, config is a very good name! Laugh about it, but I really struggled with naming that object! Particularly because it is just the attributes of the last state, so it is actually very related to state.attrubutes.
This way, if an inbuilt filter changes it, the user can still override it easily. But why do you think it should ve merged inside the trace instead of having it being an "ephemeral" value inside the data going in and out of filters? |
Btw, i intend to make data.vars available to the future lambda style functions that will be available outside of entities (like in layout, etc). That way one will be able to do things like setting an axis range based on entity data, but without having to expose the raw fetched data or any other internal stuff. Wdyt? Another thing I am not sure yet is what to do if one needs the history of one entity but doesn't want to plot it. One could hide it via plotlyjs parameters, but that would disable fetching and whatnot. WDYT about another trace option like |
Maybe |
I'm starting to spend some thoughts on the implementation.
Finally the data, layout and config objects to pass to plotlyjs can be generated. Alternatively, these are generated while evaluating the tree. Docs wise:
You are quite right that it is non trivial, but I think it will be a fun algorithmic challenge :) I think the corner cases of function evaluation order that require explanations will be very seldom encountered by users. @FrnchFrgg what do you think? Do you have a better idea? Any caveats or gotchas you can see? |
Alternatively, throw an exception if the evaluation order would be different than the written order. This may be a better way to avoid confusing behavior |
Nice, this is an absolute brainfuck, I like it a lot |
First prototype shows some promise: |
I think I'll have to rearchitect the fetch and plot functions, but they'll likely end up better than they were. What I have in mind is that the fetched and transformed data (i.e through filters), will end up inside the parsed_config as x and y. Then to plot it is just a matter of passing the whole thing to plotly as-is. |
It should be general enough that one could generate the list of entities via code, and generate them with code inside too. There will be some complexity with the auto generation of axes (each different unit_of_measurement gets its own yaxis) but i think that will just be a second pass over the evaluated "yaml tree". |
In the master branch the processing of the data, and merging different defaults in different places has become quite complex and the code hard to follow (very spaghetti like). |
The approach I'm following to avoid forward dependencies in function evaluation (*) is that no functions are allowed on parameters that appear later-than-needed in the yaml. An error will tell the user "move this up in the yaml". This should result in less user confusion as the execution order is always known (top to bottom), without getting in the way of plots that don't use functions to set some parameters. @FrnchFrgg you were right this is quite a big undertaking, I hope I finish it within the next couple of weeks, before my schedule has to catch up with adult life again. Code wise I'll be exchanging unnecessary complexity (bad code) with useful complexity. (*) e.g the offset to be defined before a filter is evaluated, because the offset affects what gets fetched |
I'm getting closer to the first beta. In some sense it feels similar to implementing an interpreter. It's quite a rewrite, probably around 50% of all the code different, and kind of dense. Once it starts running again I'll make another pass to try to make it a bit more intuitive. |
One negative effect of my approach is that fetching needs to be done sequentially, in case somebody uses $fn to compute an offset, entity name, statistic or period. One could proceed with the next trace until a $fn or filter is found. |
The new possibilities are awesome. Thank you so much for refactoring the code to make this possible. |
type: custom:plotly-graph-dev
entities:
- entity: sensor.openweathermap_temperature
period: day
statistic: max
x: $fn ({ ys,vars }) => ys
type: histogram
name: max
opacity: 0.8
marker:
color: rgb(206,43,30)
- entity: sensor.openweathermap_temperature
period: day
statistic: mean
x: $fn ({ ys,vars }) => ys
type: histogram
name: mean
opacity: 0.7
marker:
color: yellow
- entity: sensor.openweathermap_temperature
period: day
statistic: min
x: $fn ({ ys,vars }) => ys
type: histogram
name: min
opacity: 0.6
marker:
color: blue
title: Temperature Histogram last 100 days
hours_to_show: 2400
raw_plotly_config: true
layout:
barmode: overlay
margin:
t: 20
l: 50
b: 40
height: 285
xaxis:
autorange: true |
My free time ends next week, I hope I get some feedback during the weekend :) https://github.com/dbuezas/lovelace-plotly-graph-card/releases/tag/v3.0.0-beta @FrnchFrgg You were very right, this was a titanic effort, but I think it will be worth it. |
Closed by the release of v3.0.0. |
@FrnchFrgg, I haven't heard your feedback on the feature, what do you think? |
I have ported my previous setup to the new system and it works great. It feels really powerful. At first I thought that the dependency order would cause problems: I need to set several settings in the entity from a single loop, but cannot use a But the Playing a bit more with it, I feel a lot of the card features could be in fact implemented as In particular, I always wanted a way to align the right of the graph to the last statistic entry instead of "now". I now can by using a All in all it feels like a solid and powerful feature. I didn't have time to look at the implementation yet. |
Thanks for sharing! The implementation is kind of dense, I'll welcome readability suggestions if you have them. Regarding the vars, your approach sounds quite appropriate. On aligning to last timestamp, that's a clever trick! Let me know if you reach its limits and need some inbuilt support. And finally, about |
Hello, First of all, thanks a lot for your hard work. |
3d scatter: #298 |
Some plotly trace attributes can be arrays to be per-point. Extracting those out of the object returned by the lambda could enable using different attributes (say,
text
ormarker.size
) for specific points (local extrema come to mind).The text was updated successfully, but these errors were encountered: