evidentlyai / evidently

Evidently is ​​an open-source ML and LLM observability framework. Evaluate, test, and monitor any AI-powered system or data pipeline. From tabular data to Gen AI. 100+ metrics.
https://www.evidentlyai.com/evidently-oss
Apache License 2.0
5.4k stars 596 forks source link

Pleasant Default Colour Schema #211

Open strickvl opened 2 years ago

strickvl commented 2 years ago

I'd propose to include some smart / pleasant default colour schema as options that users could have at their fingertips, saving them looking up colour codes and assembling something that looks ok.

I'd be happy to implement this as a PR, if you'd be interested.

emeli-dral commented 2 years ago

That sounds awesome, we’d be very happy to have it!

However, there is a thing I want to highlight. Together with https://github.com/evidentlyai/evidently/tree/main/src/evidently module, where we implement most of the package logic, we have https://github.com/evidentlyai/evidently/tree/main/ui part as well. Significant share of the visuals is specified there and it might be a bit difficult to customize it. Honestly, that is a reason why we still did not support a Dark Scheme for the reports: the background colors is specified in UI and we do not have one particular place where we can specify it :(

Taking this into account I believe it will be much more convenient to implement default colour schemas on top of the available color constants: https://docs.evidentlyai.com/user-guide/customization/options-for-color-schema#available-options.

If you are willing to touch this UI part, you are very welcome too! Just wanted to mention that it might bring extra complexity.

strickvl commented 2 years ago

Probably the easiest way to implement some of these would be to do a series of ColorOptions definitions within the color_scheme.py file itself, perhaps like this:

# ... all the definitions of `ColorOptions` etc above...

def _set_color_options(_self, **kwargs):
    for k, v in kwargs.items():
        setattr(_self, k, v)

solarized_color_options = ColorOptions()

_set_color_options(
    solarized_color_options,
    primary_color="#268bd2",
    secondary_color="#073642",
    current_data_color="#268bd2",
    reference_data_color="#073642",
    color_sequence=[
        "#268bd2",
        "#2aa198",
        "#859900",
        "#b58900",
        "#cb4b16",
        "#dc322f",
    ],
)

I have maybe 5 or 10 different color schemes in mind that I could define this way. This way the user can just import them directly without having to define all the attributes.

I guess I'd update the docs, too, to reflect this change and show how people can use it.

Liraim commented 2 years ago

Hi @strickvl,

Yes it is good way to create predefined ColorOptions but you don't even need _set_color_options as you can write it as:

solarized_color_options  = ColorOptions(primary_color="#268bd2",
    secondary_color="#073642",
    current_data_color="#268bd2",
    reference_data_color="#073642",
    color_sequence=[
        "#268bd2",
        "#2aa198",
        "#859900",
        "#b58900",
        "#cb4b16",
        "#dc322f",
    ],
)

This would work the same, and if you skip some of fields it would use default one.

strickvl commented 2 years ago

@Liraim Thanks. Yeah I had actually thought I could just set it directly, but I was hitting some errors around that. I'll take another look later on and see if I can implement this one way or another.

Liraim commented 2 years ago

If you need help why something doesn't work as you thought, you can ask here or in Discord.

strickvl commented 2 years ago

I tried it the way you suggested, but I get the following error when trying to instantiate the object:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/Users/strickvl/coding/evidently/test.ipynb Cell 1' in <cell line: 1>()
----> 1 from evidently.options import ColorOptions
      2 from evidently.dashboard import Dashboard
      3 from evidently.dashboard.tabs import DataDriftTab

File ~/coding/evidently/src/evidently/options/__init__.py:3, in <module>
      1 from typing import TypeVar, Generic, Type, Dict, Any
----> 3 from .color_scheme import ColorOptions, solarized_color_options
      4 from .data_drift import DataDriftOptions
      5 from .quality_metrics import QualityMetricsOptions

File ~/coding/evidently/src/evidently/options/color_scheme.py:75, in <module>
     49         return self.reference_data_color or self.secondary_color
     52 # def _set_color_options(_self, **kwargs):
     53 #     for k, v in kwargs.items():
     54 #         setattr(_self, k, v)
   (...)
     72 #     ],
     73 # )
---> 75 solarized_color_options = ColorOptions(
     76     primary_color="#268bd2",
     77     secondary_color="#073642",
     78     current_data_color="#268bd2",
     79     reference_data_color="#073642",
     80     color_sequence=[
     81         "#268bd2",
     82         "#2aa198",
     83         "#859900",
     84         "#b58900",
     85         "#cb4b16",
     86         "#dc322f",
     87     ],
     88 )

TypeError: __init__() got an unexpected keyword argument 'primary_color'

This is the reason I decided to create a small helper method, @Liraim.

Liraim commented 2 years ago

Oh, I've missed that: there are no type hints for fields in ColorOptions class so dataclass do not create __init__ method with them. Probably, best solution would be add type hints for fields.

Something like this:

@dataclass
class ColorOptions:
    primary_color: str = RED
    secondary_color: str = GREY
    current_data_color: Optional[str] = None
    reference_data_color: Optional[str] = None
    color_sequence: List[str] = COLOR_DISCRETE_SEQUENCE
    fill_color: str = "LightGreen"
    zero_line_color: str = "green"
    non_visible_color: str = "white"
    underestimation_color: str = "#6574f7"
    overestimation_color: str = "#ee5540"
    majority_color: str = "#1acc98"
strickvl commented 2 years ago

Ah yes, you're right. The only part that remains a bit tricky is using a mutable default value. For color_sequence it seems I have to do something like this:

color_sequence: list = field(
        default_factory=lambda: COLOR_DISCRETE_SEQUENCE
    )

but on instantiating the object I get this error:

TypeError: 'list' object is not callable
Liraim commented 2 years ago

You can change COLOR_DISCRETE_SEQUENCE from list to tuple (immutable), so you will be able to use it directly as before.

COLOR_DISCRETE_SEQUENCE = ("#ed0400", "#0a5f38", "#6c3461", "#71aa34", "#d8dcd6", "#6b8ba4")