Skip to content
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

Legend / Bar Band Description #605

Open
simonprovost opened this issue Apr 5, 2025 · 6 comments
Open

Legend / Bar Band Description #605

simonprovost opened this issue Apr 5, 2025 · 6 comments
Labels
enhancement New feature or request

Comments

@simonprovost
Copy link

Hi Folks!

When we programmatically set up a colour expression for a given layer based on an attribute value, we then visualise it on the map. Understanding the nuances becomes complex even for those who created the colour expression for this layer.

Would it be possible to show on the map, or via a tool that displays a pop-up when a layer is selected, or within the identify panel to display a description of the colours?

I believe this could be an understood and passed at the same time of the colour expression. Could that maybe be something along the line of colour expression description?

Cheers,

See here, we understand the extreme colours (light yellow to red, but not necessarily the numbers involved, nor and worst what orange means, etc.; allowing it to be programmatically available via the API would be awesome!)

Image
@mfisher87 mfisher87 added the enhancement New feature or request label Apr 5, 2025
@mfisher87
Copy link
Member

Related: #433

@simonprovost
Copy link
Author

That's exactly it thanks for that! Having it programmatically (API) available would make it so good too :)

@mfisher87
Copy link
Member

mfisher87 commented Apr 6, 2025

What would you be looking for from the API? Do you want to for example get an image of a legend when you do layer.legend?

@simonprovost
Copy link
Author

What would you be looking for from the API? Do you want to for example get an image of a legend when you do layer.legend?

AH nah more like specifying how the legend should be looking alike, for color x it should be labeled y when programmatically we pass the color expression we should be able to pass what they mean as well, is not it?

I may be missing something here!

@mfisher87
Copy link
Member

I see, you're looking for labels for the colors? Or units? Are you thinking of passing in, for example: "The colormap minimum is 25, the maximum is 75, and the units are elevation in meters"; or are you looking in to pass categorical colors and labels, e.g. "Values of 1 are land and green; values of 2 are ocean and blue; values of 3 indicate presence of an urban environment and are yellow"? If you could give an example of what the ideal API call might look like, that would be amazing!

@simonprovost
Copy link
Author

Taking back from : #598 (comment)

I was thinking an helper that provides color expr associated with their label explaining what they are in a "word-manner" (categorical / string ...).

For instance:

from enum import Enum
from typing import List, Tuple, Union, Optional, Dict

class InterpolationType(Enum):
    LINEAR = "linear"
    DISCRETE = "discrete"
    EXACT = "exact"

PROPERTY_VALUE_TYPES = {
    "circle-fill-color": "color",
    "fill-color": "color",
    "stroke-color": "color",
    "circle-radius": "number",
    "stroke-width": "number",
}

def create_style_expression_with_legend(
    style_property: str,
    attribute: str,
    interpolation_type: InterpolationType,
    stops: List[Tuple[Union[float, str], Union[List[float], float], str]],
    default_value: Optional[Union[List[float], float]] = None,
    default_label: Optional[str] = None,
) -> Tuple[Dict[str, List], List[Tuple[str, Union[List[float], float]]]]:
    """
    Create a style expression and legend information for a given style property based on an attribute.

    :param style_property: The style property to apply the expression to (e.g., 'fill-color').
    :param attribute: The feature attribute to base the styling on (e.g., 'count').
    :param interpolation_type: The type of interpolation: LINEAR, DISCRETE, or EXACT.
    :param stops: A list of tuples (key, value, label), where key is the attribute value or threshold,
                  value is the style value (color as [r, g, b, a] or number), and label is the legend description.
    :param default_value: A fallback value if no conditions match (required for DISCRETE and EXACT).
    :param default_label: A legend label for the default_value (optional for DISCRETE and EXACT).

    :return: A tuple (style_expression, legend_info), where style_expression is a dict with the style property
             and legend_info is a list of (label, value) tuples for the legend.

    :example:
        >>> # Linear interpolation
        >>> stops = [(0.0, [0, 255, 255, 1.0], "Low"), (100.0, [255, 165, 0, 1.0], "High")]
        >>> expr, legend = create_style_expression_with_legend(
        ...     "fill-color", "count", InterpolationType.LINEAR, stops
        ... )
        >>> # expr: {'fill-color': ['interpolate', ['linear'], ['get', 'count'], 0.0, [0, 255, 255, 1.0], 100.0, [255, 165, 0, 1.0]]}
        >>> # legend: [("Low", [0, 255, 255, 1.0]), ("High", [255, 165, 0, 1.0])]

        >>> # Discrete interpolation
        >>> stops = [(50.0, [173, 216, 230, 1.0], "Low"), (200.0, [255, 255, 0, 1.0], "Medium")]
        >>> expr, legend = create_style_expression_with_legend(
        ...     "stroke-color", "value", InterpolationType.DISCRETE, stops,
        ...     default_value=[64, 64, 64, 1.0], default_label="High"
        ... )
        >>> # expr: {'stroke-color': ['case', ['<=', ['get', 'value'], 50.0], [173, 216, 230, 1.0], ['<=', ['get', 'value'], 200.0], [255, 255, 0, 1.0], [64, 64, 64, 1.0]]}
        >>> # legend: [("Low", [173, 216, 230, 1.0]), ("Medium", [255, 255, 0, 1.0]), ("High", [64, 64, 64, 1.0])]

        >>> # Exact interpolation
        >>> stops = [("A", [255, 0, 0, 1.0], "Category A"), ("B", [0, 0, 255, 1.0], "Category B")]
        >>> expr, legend = create_style_expression_with_legend(
        ...     "fill-color", "type", InterpolationType.EXACT, stops,
        ...     default_value=[0, 0, 0, 1.0], default_label="Other"
        ... )
        >>> # expr: {'fill-color': ['case', ['==', ['get', 'type'], "A"], [255, 0, 0, 1.0], ['==', ['get', 'type'], "B"], [0, 0, 255, 1.0], [0, 0, 0, 1.0]]}
        >>> # legend: [("Category A", [255, 0, 0, 1.0]), ("Category B", [0, 0, 255, 1.0]), ("Other", [0, 0, 0, 1.0])]
    """
    value_type = PROPERTY_VALUE_TYPES.get(style_property, "unknown")
    if value_type == "unknown":
        print(f"WARNING: Unknown style property '{style_property}'. Trusted properties: {list(PROPERTY_VALUE_TYPES.keys())}.")

    if not isinstance(style_property, str) or not style_property.strip():
        raise ValueError("style_property must be a non-empty string.")
    if not isinstance(attribute, str) or not attribute.strip():
        raise ValueError("attribute must be a non-empty string.")
    if not isinstance(interpolation_type, InterpolationType):
        raise ValueError("interpolation_type must be an InterpolationType enum value.")
    if not stops or not isinstance(stops, list):
        raise ValueError("stops must be a non-empty list of tuples.")

    for stop in stops:
        if not isinstance(stop, tuple) or len(stop) != 3:
            raise ValueError(f"Each stop must be a tuple (key, value, label); got {stop}")
        key, value, label = stop
        if not isinstance(label, str) or not label.strip():
            raise ValueError(f"Label must be a non-empty string; got {label}")

        if interpolation_type != InterpolationType.EXACT and not isinstance(key, (int, float)):
            raise ValueError(f"For {interpolation_type.value} interpolation, stop keys must be numeric; got {key}")
        elif interpolation_type == InterpolationType.EXACT and not isinstance(key, (int, float, str)):
            raise ValueError(f"For EXACT interpolation, stop keys must be numeric or strings; got {key}")

        if value_type == "color":
            if not isinstance(value, list) or len(value) != 4 or not all(isinstance(v, (int, float)) for v in value):
                raise ValueError(f"For '{style_property}', value must be [r, g, b, a]; got {value}")
            if not all(0 <= v <= 255 for v in value[:3]) or not 0 <= value[3] <= 1:
                raise ValueError(f"Color {value} must have RGB in [0, 255] and alpha in [0, 1]")
        elif value_type == "number":
            if not isinstance(value, (int, float)):
                raise ValueError(f"For '{style_property}', value must be a number; got {value}")

    if default_value is not None:
        if value_type == "color":
            if not isinstance(default_value, list) or len(default_value) != 4 or not all(isinstance(v, (int, float)) for v in default_value):
                raise ValueError(f"For '{style_property}', default_value must be [r, g, b, a]; got {default_value}")
            if not all(0 <= v <= 255 for v in default_value[:3]) or not 0 <= default_value[3] <= 1:
                raise ValueError(f"Default color {default_value} must have RGB in [0, 255] and alpha in [0, 1]")
        elif value_type == "number":
            if not isinstance(default_value, (int, float)):
                raise ValueError(f"For '{style_property}', default_value must be a number; got {default_value}")
        if default_label is not None and (not isinstance(default_label, str) or not default_label.strip()):
            raise ValueError(f"default_label must be a non-empty string; got {default_label}")

    expression = []
    legend = []

    if interpolation_type == InterpolationType.LINEAR:
        if len(stops) < 2:
            raise ValueError("Linear interpolation requires at least two stops.")
        expression = ["interpolate", ["linear"], ["get", attribute]]
        sorted_stops = sorted(stops, key=lambda x: float(x[0]))
        for key, value, _ in sorted_stops:
            expression.extend([float(key), value])
        legend = [(label, value) for _, value, label in sorted_stops]

    elif interpolation_type == InterpolationType.DISCRETE:
        if default_value is None:
            raise ValueError("default_value is required for DISCRETE interpolation.")
        expression = ["case"]
        sorted_stops = sorted(stops, key=lambda x: float(x[0]))
        for key, value, _ in sorted_stops:
            expression.extend([["<=", ["get", attribute], float(key)], value])
        expression.append(default_value)

        legend = [(label, value) for _, value, label in sorted_stops]
        if default_label and default_value is not None:
            legend.append((default_label, default_value))

    elif interpolation_type == InterpolationType.EXACT:
        if default_value is None:
            raise ValueError("default_value is required for EXACT interpolation.")
        expression = ["case"]
        for key, value, _ in stops:
            expression.extend([["==", ["get", attribute], key], value])
        expression.append(default_value)
       
 legend = [(label, value) for _, value, label in stops]
        if default_label and default_value is not None:
            legend.append((default_label, default_value))

    return {style_property: expression}, legend
style_expr, legend = create_style_expression_with_legend(
    style_property="fill-color",
    attribute="temperature",
    interpolation_type=InterpolationType.LINEAR,
    stops=[
        (0.0, [0, 255, 255, 1.0], "Cold"),
        (50.0, [255, 255, 0, 1.0], "Moderate"),
        (100.0, [255, 0, 0, 1.0], "Hot")
    ]
)
print("Style Expression:", style_expr)
print("Legend:", legend)

#### ANOTHE ONE:
style_expr, legend = create_style_expression_with_legend(
    style_property="stroke-color",
    attribute="population",
    interpolation_type=InterpolationType.DISCRETE,
    stops=[
        (1000.0, [173, 216, 230, 1.0], "Small"),
        (5000.0, [255, 165, 0, 1.0], "Medium")
    ],
    default_value=[128, 0, 128, 1.0],
    default_label="Large"
)
print("Style Expression:", style_expr)
print("Legend:", legend)

How then should JGIS handle that out I yet not clues not being enough expert with the codebase but as the color expr from #598 (comment) works and where @martinRenou seems to have also found the idea neat I believe there is space for debate but would be nice to have this that way.

Any better thoughts? 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants