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

How to test for nulls with new expression syntax? #5761

Closed
stevage opened this issue Nov 29, 2017 · 10 comments
Closed

How to test for nulls with new expression syntax? #5761

stevage opened this issue Nov 29, 2017 · 10 comments

Comments

@stevage
Copy link
Contributor

stevage commented Nov 29, 2017

I'm not sure if this is a support request, a comment on the new API or a feature request.

I'm loading a GeoJSON layer which contains some nulls. I want to create a color property which displays the null values differently from other values. (Yes, I could create two layers and a filter, as a workaround). My code looks like this:

        paint: {
            "circle-color": 
                ["case",
                    ["all", 
                        ["!=", ["get", "Enrolments_2017_Total"], null],
                        ["!=", ["get", "Capacity_Total"], null]],
                    [
                        "rgb",
                        ["max",
                            0,                    
                            ["min", 
                                255, 
                                ["+", 
                                    -200, 
                                    ["*", 
                                        400, 
                                        ["/", 
                                            ["get", "Enrolments_2017_Total"], 
                                            ["get", "Capacity_Total"]]]]]],
                        0,
                        200
                    ],
                    "yellow"
                ],

Problem:

If I use ["!=", ["get", "Enrolments_2017_Total"], null],, I get an error.

Error: layers.school-points.paint.circle-color[1][1]: Expected arguments of type (number, number) | (string, string) | (boolean, boolean) | (null, null), but found (value, null) instead.

Can this be right? Is it literally saying you can only compare a null literal to another null literal (["!=", null, null])? Um, why would you want to do that? :)

If I use ["has", "Enrolments_2017_Total"] then that test always passes, because every feature "has" that property, it's just sometimes null.

If I use [">", ["get", "Enrolments_2017_Total"], 0],

I get:

Error: layers.school-points.paint.circle-color[1][1]: Expected arguments of type (number, number) | (string, string), but found (value, number) instead.`

Is there something I'm missing here?

@stevage
Copy link
Contributor Author

stevage commented Nov 29, 2017

Hmm, this method almost worked:

[">", ["to-number", ["get", "Enrolments_2017_Total"]], 0],

but didn't seem to always get the right result.

This method worked:

["==", ["typeof", ["get", "Enrolments_2017_Total"]], "number"],

It feels quite cumbersome and not very intuitive, considering the equivalent in the old syntax was:

["!=", "Enrolments_2017_Total", null]

@anandthakker
Copy link
Contributor

I think you're right that having a (null, null) overload for == doesn't really make sense.

There are three ways in the current API designed for this sort of null-handling:

  1. The typeof-based approach you mentioned above, ["==", ["typeof", ["get", "Enrolments_2017_Total"]], "number"]. The most universal, but also the most cumbersome.
  2. Using a fallback value in the "number" type assertion: [">", ["number", ["get", "Enrolments_2017_Total"], -1], 0]. Only works if there's a value like -1 that's unused in the actual feature data, so that it can stand for 'null'.
  3. Convert to boolean: ["to-boolean", ["get", "Enrolments_2017_Total"]]. The most concise, but would also be false if the value of Enrolments_2017_Total was 0 (since 0 gets converted to false).

Other, less cumbersome options we could consider:

  • Add "==-null", "!=-null" operators: ["!=-null", ["get", "Enrolments_2017_Total"]]
  • Add type-aware versions of has (has-number, has-string, etc.): ["has-number", "Enrolments_2017_Total"]

@jfirebaugh
Copy link
Contributor

Another option would be to replace the (null, null) overload for == with (null, value) and (value, null) overloads.

@stevage
Copy link
Contributor Author

stevage commented Dec 7, 2017

Yeah, @jfirebaugh's suggestion is intuitively how I expected it to work. It was confusing and unexpected to me that "value" is a distinct type, and that you have to explicitly use casts everywhere. It was also very unintuitive that null is considered a unique type, rather than just a special value of every other type.

@anandthakker
Copy link
Contributor

Another option would be to replace the (null, null) overload for == with (null, value) and (value, null) overloads

A sort of similar problem to the one in this issue is doing something like ["==", ["get", "thing-one"], ["get", "thing-two"]]. Currently, this wouldn't compile without type assertions around the gets. I'm wondering (again) whether it's really valuable to force the per-atomic-type overloads here, rather than just having it be (value, value).

The only benefit I can think of is that, if the arguments to == were a bit complicated, it would be possible to accidentally author an expression where the two arguments are (always) incompatible types (e.g. string, number) and then be confused why it always evaluates to false. The current approach protects against that, but if we went with (value, value), we could have a bit of extra validation for == that fails if the arguments were (statically) typed as two incompatible types.

Meanwhile, the most common case is likely just ["==", ["get", "x"], some_literal] -- and in that case, the (value, value) approach is basically equivalent to the current approach with enhanced type inference.

anandthakker pushed a commit that referenced this issue Dec 11, 2017
Closes #5761
Closes #5835

The overloads of == and != are now effectively:
 - `(T1: Comparable, T2: Comparable) => boolean { T1 == T2 }`
 - `(Comparable, value) => boolean`
 - `(value, Comparable) => boolean`

Where `Comparable = string | number | boolean | null`.
anandthakker added a commit that referenced this issue Dec 12, 2017
* Improve typing for ==, != expressions

Closes #5761
Closes #5835

The overloads of == and != are now effectively:
 - `(T1: Comparable, T2: Comparable) => boolean { T1 == T2 }`
 - `(Comparable, value) => boolean`
 - `(value, Comparable) => boolean`

Where `Comparable = string | number | boolean | null`.

* Simplify comparability check

* Add internal docs, fix possibleValues
@stevage
Copy link
Contributor Author

stevage commented Dec 12, 2017

Awesome, glad I was useful.

@dominijk
Copy link

Is this documented somewhere? I have data with null values and want to filter these out, the data can be string or numeric.

@kuanb
Copy link

kuanb commented Feb 6, 2019

+1 to @dominijk question

@palamago
Copy link

@kuanb use ['has', field]

@mvl22
Copy link
Contributor

mvl22 commented Nov 28, 2024

After encountering the same problem, I thought it would be useful to provide examples:

Use in an expression, to switch styling between normal and null value:

This will keep the feature present (e.g. for hover purposes), but show it is as transparent:

const expression = [
	'case',
		['has', fieldname],    // check that the property for the named field is not null
			'green',       // value for the normal value colour
		'transparent'          // or whatever you want your NULL value to be shown as
];

The value for the normal state can itself be an expression, e.g. if you have a step/case expression for a range of values, you can wrap that with the above:

const expression = [
	'case',
		['has', fieldname],
			['step',
				['get', fieldname],
				'green'
				5, 'orange'
				10, 'red'
			],
		'transparent'
];

Use as a filter when defining addLayer, to exclude features entirely:

This will filter out null features entirely, i.e. not even shown/selectable at all.

map.addLayer ({
	...
	filter: ['has', fieldname],
});

See example at:
https://docs.mapbox.com/mapbox-gl-js/example/cluster/

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

No branches or pull requests

7 participants