Closed dangotbanned closed 2 months ago
@binste FYI I spent a good chunk of yesterday trying (and failing) to make progress on this point. If you have any ideas I'm all ears - will loop back to this eventually but it really stumped me
Tasks
- [ ] Replace
MANUAL_DEFS
w/ a recursive/graph-based solution
- Need a better understanding of the existing code
- Working through changes like
3c9aab5
(#3536)
Think I need to account for ThemeConfig
being the type of a key "config"
in some outer schema.
Maybe adapting TopLevelUnitSpec
?
Planning on using the following subset of properties, which without any extra code would be:
class StillUnnamedTopLevel(TypedDict):
align: RowColLayoutAlign | LayoutAlign_T
autosize: AutoSizeParamsKwds | AutosizeType_T
background: ColorHex | ColorName_T
bounds: Literal['full', 'flush']
center: bool | RowColboolean
config: Config # <------------------------- `ThemeConfig`
description: str
height: Step | float | Literal['container']
name: str
padding: Map | float
params: Sequence[VariableParameterKwds | TopLevelSelectionParameterKwds]
projection: Projection
resolve: Resolve
spacing: float | RowColnumber
title: str | TitleParams | Sequence[str]
usermeta: Map
view: ViewBackground
width: Step | float | Literal['container']
RowCol
generic
RowColLayoutAlign
, RowColboolean
, RowColnumber
("Config", ("ThemeConfig",))
to remapping@binste pinging in case you had any thoughts here?
The first thing I tried was simply passing TopLevelUnitSpec
to find_theme_config_targets
.
However, that had a dramatic effect on the number of TypedDict
(s):
Config
: 59TopLevelUnitSpec
: 172Personally, I've only used these keys in a theme before - with almost all of the contents being "config"
:
{"autosize", "background", "config", "height", "width"}
So I'm trying to avoid creating loads of things that aren't used frequently
TopLevelUnitSpec
_typed_dict_(args|doc)
ThemeConfig
name for StillUnnamedTopLevel
ThemeConfig
-> ConfigKwds
This PR blows my mind! 🤯 Really appreciate all the work you put into it, this obviously required going very deep in the VL schema and the code generation and I think the outcome is super helpful for further advancing the UX of themes. Now I really want to write some new themes 😄
I only have some very minor comments. If you feel good with it, I'm ok if you merge it afterwards!
Thanks so much for the review @binste! (and helping me get unstuck midway through)
Hopefully this will make it easier to explore some of your points in https://github.com/vega/altair/issues/3519#issuecomment-2292010192 once we've merged
I'll leave this open for a few hours in case you had any thoughts on https://github.com/vega/altair/pull/3536#discussion_r1761463506 - otherwise I'll follow this up with another PR as mentioned
Thanks for all the work. A concise PR description with an explanation of the what/why/how would really help future readers coming to this PR. This description is also helpful for making the release notes better. Thanks again!
Thanks for all the work. A concise PR description with an explanation of the what/why/how would really help future readers coming to this PR. This description is also helpful for making the release notes better. Thanks again!
No worries, will work on that now thanks @mattijn
@mattijn just updated the description, hopefully you (and others) find it helpful 😄
Thanks! Can you add also how this feature is being used? Do we want users to import the following?
import altair.vegalite.v5.schema._config as theme
And
from altair.vegalite.v5.schema._config import ThemeConfig
And then? I'd love to have a brief description that explains how a new user can start using this feature. It would be great if you could provide a simple example that highlights just one feature controlled by the ThemeConfig
. Thanks again for all your hard work!
Thanks for expanding on the PR description, that was helpful for me to appreciate just how much this PR improves the experience of defining custom themes, wow! In line with what Mattijn suggested, it would be great to update the custom theme section of the docs to use this approach and mention the autocompletion behavior.
Thanks! Can you add also how this feature is being used? Do we want users to import the following?
import altair.vegalite.v5.schema._config as theme
from altair.typing import theme
from altair.typing import ThemeConfig, theme
custom_theme = ThemeConfig(
config=theme.ConfigKwds(
axis=theme.AxisConfigKwds(grid=True, labelFont="system-ui"),
circle=theme.MarkConfigKwds(fill="purple"),
)
)
custom_theme
And
from altair.vegalite.v5.schema._config import ThemeConfig
from altair.typing import ThemeConfig
And then? I'd love to have a brief description that explains how a new user can start using this feature. It would be great if you could provide a simple example that highlights just one feature controlled by the
ThemeConfig
. Thanks again for all your hard work!
Reading this made me spot a bug with the import behavior, just fixed in d4bd6db
(#3536)
For reference, @register_theme
has an example of the intended import path
@joelostblom @mattijn thanks for your comments!
I'll be updating the description with more examples tomorrow, but hopefully you get a rough idea with d4bd6db
(#3536)
it would be great to update the custom theme section of the docs to use this approach and mention the autocompletion behavior.
Absolutely agree, I added that to the list in https://github.com/vega/altair/issues/3519#issue-2447214476 an hour ago - I think it will make the remaining items much easier
Awesome! Looking forward to! Another question I have, why is this ThemeConfig
not imported to the root level of Altair? It sounds like a top level object to me. Can you elaborate on this? With feature comes questions😀. Thanks!
Awesome! Looking forward to! Another question I have, why is this
ThemeConfig
not imported to the root level of Altair? It sounds like a top level object to me. Can you elaborate on this? With feature comes questions😀. Thanks!
For v6
I would agree, but right now I think this is much easier to find. (https://github.com/vega/altair/issues/2918)
I can't seem to find the thread, but I suggested to @binste having a small number of ...Kwds
available in altair.typing
- with the rest in altair.typing.theme
.
With the current layout, I think this reinforces the relationship between ThemeConfig
-> ...Kwds
and that ThemeConfig
is the entry point.
I'm not sure if all that made sense, if not apologies, I was supposed to call it a day already 😅
@mattijn @joelostblom
FYI the conversation marked as unresolved is actually resolved.
I haven't updated the status of it yet, to block merging until you've both seen the updated description
Otherwise this should be ready to merge 👍
I don't have much to comment on the changes to the import, so will refer to @mattijn for that.
On first look, the class based syntax looks maybe a bit verbose:
So I would probably opt for the combination with dicts myself:
But maybe I will change my mind when I try them; it's great that both exist and support autocompletion!
@mattijn I'm planning to merge this tomorrow if you don't have any objections.
In #3600 I've been trying to work around not having the refactored tools/...
and have made a lot of progress; but I need to start making changes in tools.generate_schema_wrapper.py
and tools.schemapi.utils.py
soon for generating alt.expr.__init__.py
Thanks for asking! I still cannot understand why this ThemeConfig
feature is available within the altair.types
. I thought we declared that altair.types
is the interface of public types within altair. But now we are placing features such as ThemeConfig
in there. Is it a type? Therefore it does not help me with discoverability either.
Also, as a user, I really prefer a feature explanation that I can copy and paste. I don't see that in this PR. I see a few screenshots, but that is really not the advocated route for a Minimal, Reproducible Example. For me it is still not clear how I can use this from the PR description.
I found the following in the code:
import altair as alt
from altair.typing import ThemeConfig
from vega_datasets import data
@alt.register_theme("param_font_size", enable=True)
def custom_theme() -> ThemeConfig:
sizes = 12, 14, 16, 18, 20
return {
"autosize": {"contains": "content", "resize": True},
"background": "#F3F2F1",
"config": {
"axisX": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
"axisY": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
"font": "'Lato', 'Segoe UI', Tahoma, Verdana, sans-serif",
"headerColumn": {"labelFontSize": sizes[1]},
"headerFacet": {"labelFontSize": sizes[1]},
"headerRow": {"labelFontSize": sizes[1]},
"legend": {"labelFontSize": sizes[0], "titleFontSize": sizes[1]},
"text": {"fontSize": sizes[0]},
"title": {"fontSize": sizes[-1]},
},
"height": {"step": 28},
"width": 350,
}
source = data.stocks()
lines = (
alt.Chart(source, title=alt.Title("Stocks"))
.mark_line()
.encode(x="date:T", y="price:Q", color="symbol:N")
)
lines.interactive(bind_y=False)
That works! But I also see another approach. Here you define a custom_theme
from the ThemeConfig
from altair.typing import ThemeConfig, theme
custom_theme = ThemeConfig(
config=theme.ConfigKwds(
axis=theme.AxisConfigKwds(grid=True, labelFont="system-ui"),
circle=theme.MarkConfigKwds(fill="purple"),
)
)
custom_theme
How do I register this as theme?
I searched for register_theme
within the altair documentation page, but nothing was returned https://altair-viz.github.io/search.html?q=register_theme. A brief description to the documentation page would help the average user a lot here.
Last, after I enabled the custom theme, how can I de-register this custom theme and go back to the default settings?
Thank you for your patience!
Thanks for your response @mattijn!
I'm going to try my best to address your concerns in a few follow-up comments.
Just finished writing up the first (of 2 or 3), so please know there is more to come
altair.typing
@mattijn
I still cannot understand why this
ThemeConfig
feature is available within thealtair.types
. I thought we declared thataltair.types
is the interface of public types within altair. But now we are placing features such asThemeConfig
in there. Is it a type? Therefore it does not help me with discoverability either.
So in the description I provided a link that can clear this up:
This PR aims to improve the UX when authoring a theme. By utilising TypedDict(s) ...
I've picked out some key quotes, but I would really recommend reading the section Using TypedDict Types as this illustrates how TypedDict
(s) differ from regular classes:
- The created
TypedDict
type object is not a real class object.- When called, the
TypedDict
type object returns an ordinary dictionary object at runtime- In particular,
TypedDict
type objects cannot be used inisinstance()
tests such asisinstance(d, Movie)
.
Back to your question:
Is it a type?
I'm afraid this is complex question to answer, but I will do my best to summarize.
TypedDict
is defined in typing - not as a class - but as a function
TypedDict
is a TypeForm
type[C]
? also further illustrates the distinctionThe short (but seemingly a non)answer is that it is not a real class.
I believe that if we were to add these TypedDict
(s) to the top-level, that it could cause confusion for users that (understandably) expect them to work the same as SchemaBase
classes.
In https://github.com/vega/altair/pull/3536#discussion_r1759328893 I mentioned some related issues, but can now see I forgot to elaborate on what I would prefer for v6
.
I would like to have used altair.theme
as the namespace instead of altair.typing.theme
.
However, this would be very easy to mistake for altair.themes
- which is a variable storing a registry of themes.
For v6
I would propose the following api changes - and if there is any support I will open a new issue:
altair.typing.theme
-> altair.theme
altair.typing.ThemeConfig
, which is already included in the above@altair.register_theme
-> @altair.theme.register
altair.theme.enable
altair.themes
top-level exportThis would consolidate all theme related functionality behind a single namespace:
from altair import theme
@theme.register("custom 1", enable=False)
def custom_theme() -> theme.ThemeConfig:
return {
"autosize": {"contains": "content", "resize": True},
"config": theme.ConfigKwds(
circle=theme.MarkConfigKwds(fill="purple")
),
}
...
theme.enable("custom 1")
...
theme.enable("default")
...
@mattijn
Also, as a user, I really prefer a feature explanation that I can copy and paste. I don't see that in this PR. I see a few screenshots, but that is really not the advocated route for a Minimal, Reproducible Example. For me it is still not clear how I can use this from the PR description.
I have now updated the PR description with collapsible code blocks to accompany each screenshot.
I would absolutely agree that providing a Minimal, Reproducible Example is helpful, although I thought that related more to bugs/issues?
There were a few reasons leaned towards screenshots:
dict
keysUsing the classes themselves is entirely optional, as can be seen in
test_theme.py
.
@mattijn
How do I register this as theme?
I've updated the PR description, this is the example under the heading Options for imports & runtime typing
I searched for
register_theme
within the altair documentation page, but nothing was returned altair-viz.github.io/search.html?q=register_theme. A brief description to the documentation page would help the average user a lot here.
Well spotted! I wasn't aware this hadn't been included in the API reference yet, but will open a new issue/PR to address it - since that is orthogonal to this PR.
I was waiting until this PR was finished before introducing @register_theme
to the User Guide, since they complement eachother well.
There are a few other changes I've noted for the User Guide in https://github.com/vega/altair/issues/3519, including ThemeConfig
as mentioned in https://github.com/vega/altair/pull/3536#issuecomment-2353877227
Last, after I enabled the custom theme, how can I de-register this custom theme and go back to the default settings?
Exactly the same way as documented in https://altair-viz.github.io/user_guide/customization.html#defining-a-custom-theme
In https://github.com/vega/altair/pull/3526 I've described this as a convenience for the common case where you use a single theme. That isn't the only possible use-case, but it is the motivating one
I didn't mean to trigger a merge to main
with https://github.com/vega/altair/commit/712680b2965eede4c40d93f52d3f963c5d181587
I was trying to fix the conflicts but now I see the comment I'd mentioned was unresolved in https://github.com/vega/altair/pull/3536#issuecomment-2357877156 has been resolved
@binste can we revert this so that @mattijn has time to respond please?
See https://github.com/vega/altair/pull/3536#issuecomment-2367591832
Thanks @mattijn for https://github.com/vega/altair/commit/3d97faeeb53141cffb1a5bacaa90018982414090
I'm not sure what would be the best way to proceed here, should I open a new PR?
Thanks for the added explanations! Appreciated. I like the suggestion you provide in https://github.com/vega/altair/pull/3536#issuecomment-2366736230 to consolidate everything under a single namespace, including the proposed ThemeConfig
. I’m fine with doing that now, rather than releasing a version first and requiring deprecation immediately after.
However, instead of phasing out the plural altair.themes
in favour of the singleton altair.theme
can we consolidate this PR's work into the existing altair.themes
? For example, can we have altair.themes.ThemeConfig
(and altair.themes.AxisConfigKwds
, etc.) aligning with the existing altair.themes.register()
and altair.themes.enable()
.
Aesthetically I like the singleton altair.theme
, but I think sticking with altair.themes
is more practical, if that is also an option. Can you reflect on this?
Thank you for the added explanations regarding the type of ThemeConfig
! I currently don't think it needs to be registered as part of interface of altair its public types. The current _TypedDictMeta
type seems fine and even if it later becomes something like TypeForm
, it's essentially a dictionary (great!) at runtime if I understand correctly.
Also, much appreciated for including both code blocks and screenshots! Great combination as such!
I don't think it is possible to re-open a merged PR, even when the changes are reverted with the latest commit, probably the best option is to open a new PR😔
Thanks for the added explanations! Appreciated. I like the suggestion you provide in #3536 (comment) to consolidate everything under a single namespace, including the proposed
ThemeConfig
. I’m fine with doing that now, rather than releasing a version first and requiring deprecation immediately after.Thank you for the added explanations regarding the type of
ThemeConfig
! I currently don't think it needs to be registered as part of interface of altair its public types. The current_TypedDictMeta
type seems fine and even if it later becomes something likeTypeForm
, it's essentially a dictionary (great!) at runtime if I understand correctly.Also, much appreciated for including both code blocks and screenshots! Great combination as such!
Thanks @mattijn
However, instead of phasing out the plural
altair.themes
in favour of the singletonaltair.theme
can we consolidate this PR's work into the existingaltair.themes
? For example, can we havealtair.themes.ThemeConfig
(andaltair.themes.AxisConfigKwds
, etc.) aligning with the existingaltair.themes.register()
andaltair.themes.enable()
.Aesthetically I like the singleton
altair.theme
, but I think sticking withaltair.themes
is more practical, if that is also an option. Can you reflect on this?
Okay so rather than fully revert this PR, I think the changes we want are strictly concerning a small refactor of the public interface.
@register_theme
is all goodI propose:
refactor
PR to implement this interfaceHow does that sound to you?
The changes requested were only concerning a small refactor of the public interface. Proposal sounds good!
Resolves one item in #3519
Description
This PR aims to improve the UX when authoring a theme. By utilising
TypedDict
(s) we can provide a very similar experience to that of writing in Vega Editor, with rich autocompletion and static validation feedback.Main Additions
TypedDict
classes, mirroringSchemaBase
classes - for the subset of objects that are used within a theme._.config.py
where these classes reside.tools/
(w/ improved documentation) to support both existing schema generation and these newTypedDict
(s):Interactions with public API
These classes are entirely isolated from the main
alt.___
namespace, but can still be used anywhere adict|Map
annotation is present in existing code. Currently:ThemeConfig
is exported toaltair.typing
altair.typing.theme
.Using the classes themselves is entirely optional, as can be seen in
test_theme.py
. Simply annotatingThemeConfig
(and using a static type checker) allows the user to specify a theme using nativepython
types.Examples
Autocomplete w/ native
python
typesCode block
```py import altair as alt from altair.typing import ThemeConfig # noqa: TCH001 @alt.register_theme("custom theme", enable=True) def my_theme() -> ThemeConfig: return {""} ```
Type checking w/ mixed
python|altair
typesCode block
```py import altair as alt from altair.typing import ThemeConfig # noqa: TCH001 from altair.typing.theme import ConfigKwds @alt.register_theme("custom theme", enable=True) def my_theme() -> ThemeConfig: return { "autosize": {"contains": "padding"}, "config": ConfigKwds(font="Segoe UI", axis=9999999), "width": "container", "height": [500], } ```
Options for imports & runtime typing
Code block
```py import altair as alt from altair.typing import ThemeConfig, theme # noqa: F811 from altair.typing.theme import AxisConfigKwds from vega_datasets import data custom_theme = ThemeConfig( config=theme.ConfigKwds( axis=AxisConfigKwds(grid=True, labelFont="system-ui"), axisX=AxisConfigKwds(labelOpacity=0.5), circle=theme.MarkConfigKwds(fill="purple"), ) ) print( f"{custom_theme!r}\n\n" f"The above theme was defined using {ThemeConfig.__name__!r}, " f"but at runtime the type is {type(custom_theme).__name__!r}" ) # Manual registration alt.themes.register("demo", lambda: custom_theme) alt.themes.enable("demo") alt.Chart(data.cars()).mark_circle().encode(x="Horsepower", y="Miles_per_Gallon") ```
@register_theme
docDefault
Code block
```py import altair as alt from altair.typing import ThemeConfig from altair.typing.theme import ConfigKwds from vega_datasets import data source = data.stocks() lines = ( alt.Chart(source, title=alt.Title("Stocks")) .mark_line() .encode(x="date:T", y="price:Q", color="symbol:N") .interactive(bind_y=False) ) lines ```
With a registered theme
Code block
```py @alt.register_theme("param_font_size", enable=True) def custom_theme_2() -> ThemeConfig: sizes = 12, 14, 16, 18, 20 return { "autosize": {"contains": "content", "resize": True}, "background": "#F3F2F1", "config": ConfigKwds( axisX={"labelFontSize": sizes[1], "titleFontSize": sizes[1]}, axisY={"labelFontSize": sizes[1], "titleFontSize": sizes[1]}, font="'Lato', 'Segoe UI', Tahoma, Verdana, sans-serif", headerColumn={"labelFontSize": sizes[1]}, headerFacet={"labelFontSize": sizes[1]}, headerRow={"labelFontSize": sizes[1]}, legend={"labelFontSize": sizes[0], "titleFontSize": sizes[1]}, text={"fontSize": sizes[0]}, title={"fontSize": sizes[-1]}, ), "height": {"step": 28}, "width": 350, } lines ```
Early POC Demo
These screenshots were taken mid-development
Providing fully nested auto-complete
![image](https://github.com/user-attachments/assets/1ab8e3c1-2dc9-4aa3-aada-f402f556f3f2)Type checking feedback
![image](https://github.com/user-attachments/assets/84b70a50-8cc0-4246-9e72-ad472a12d77f)
Tasks
ThemeConfig
"parent`EncodeKwds
, where docs do not contain typing informationThemeConfig
TypedDict
,Literal
, or a composition that contains to 0SchemaBase
typesbb99389
(#3536)MANUAL_DEFS
w/ a recursive/graph-based solution3c9aab5
(#3536) to better understand the surrounding code