Skip to content

Commit

Permalink
enable shading on data sets with multiple data types
Browse files Browse the repository at this point in the history
e.g flood risk

For #333
  • Loading branch information
struan committed Nov 29, 2023
1 parent 92b7b4f commit f0c72f6
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 141 deletions.
5 changes: 4 additions & 1 deletion hub/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,7 @@ def shader(self):
try:
return DataSet.objects.get(name=name)
except DataSet.DoesNotExist:
return None
try:
return DataType.objects.get(name=name)
except DataType.DoesNotExist:
return None
294 changes: 156 additions & 138 deletions hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,153 @@ def value_col(self):
return "data"


class DataSet(TypeMixin, models.Model):
class ShaderMixin:
shades = [
"#ffffd9",
"#edf8b1",
"#c7e9b4",
"#7fcdbb",
"#41b6c4",
"#1d91c0",
"#225ea8",
"#253494",
"#081d58",
]

COLOUR_NAMES = {
"red-500": "#CC3517",
"orange-500": "#ED6832",
"yellow-500": "#FEC835",
"teal-600": "#068670",
"blue-500": "#21A8E0",
"purple-500": "#6F42C1",
"gray-500": "#ADB5BD",
"gray-300": "#DEE2E6",
}

@property
def shader_table(self):
return self.table

Check warning on line 115 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L115

Added line #L115 was not covered by tests

@property
def shader_filter(self):
return {"data_type__data_set": self}

Check warning on line 119 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L119

Added line #L119 was not covered by tests

def shade(self, val, cmin, cmax):
if val == "":
return None
try:
x = float(val - cmin) / (cmax - cmin)
except ZeroDivisionError:
x = 0.5 # cmax == cmin

Check warning on line 127 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L123-L127

Added lines #L123 - L127 were not covered by tests

shade = int(x * 9) - 1

Check warning on line 129 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L129

Added line #L129 was not covered by tests
if shade < 0:
shade = 0
return self.shades[shade]

Check warning on line 132 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L131-L132

Added lines #L131 - L132 were not covered by tests

def colours_for_areas(self, areas):
if len(areas) == 0:
return {"properties": {"no_areas": True}}

Check warning on line 136 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L136

Added line #L136 was not covered by tests

values, mininimum, maximum = self.shader_value(areas)
legend = {}

Check warning on line 139 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L138-L139

Added lines #L138 - L139 were not covered by tests
if hasattr(self, "options"):
for option in self.options:
if option.get("shader", None) is not None:
legend[option["title"]] = self.COLOUR_NAMES.get(

Check warning on line 143 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L143

Added line #L143 was not covered by tests
option["shader"], option["shader"]
)

if len(legend) > 0:
props = {"properties": {"legend": legend}}

Check warning on line 148 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L148

Added line #L148 was not covered by tests
else:
d_max = maximum
d_min = mininimum

Check warning on line 151 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L150-L151

Added lines #L150 - L151 were not covered by tests
if self.is_float:
d_max = round(maximum, 1)
d_min = round(mininimum, 1)

Check warning on line 154 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L153-L154

Added lines #L153 - L154 were not covered by tests
if self.is_percentage:
d_max = f"{d_max}%"
d_min = f"{d_min}%"

Check warning on line 157 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L156-L157

Added lines #L156 - L157 were not covered by tests

props = {

Check warning on line 159 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L159

Added line #L159 was not covered by tests
"properties": {
"maximum": d_max,
"minimum": d_min,
"shades": self.shades,
}
}
colours = {}

Check warning on line 166 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L166

Added line #L166 was not covered by tests
for value in values:
data = value.value()

Check warning on line 168 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L168

Added line #L168 was not covered by tests
if hasattr(self, "options"):
for option in self.options:
if option["title"] == data:
colours[value.gss] = {

Check warning on line 172 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L172

Added line #L172 was not covered by tests
"colour": self.COLOUR_NAMES.get(
option["shader"], option["shader"]
),
"opacity": value.opacity(mininimum, maximum) or 0.7,
"value": data,
"label": self.label,
}

if colours.get(value.gss, None) is None:
shade = self.shade(data, mininimum, maximum)

Check warning on line 182 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L182

Added line #L182 was not covered by tests
if shade is not None:
colours[value.gss] = {

Check warning on line 184 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L184

Added line #L184 was not covered by tests
"colour": shade,
"opacity": 0.7,
"label": self.label,
"value": data,
}

# if there is no data for an area then need to set the shader to opacity 0 otherwise
# they will end up as the default
missing = {}

Check warning on line 193 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L193

Added line #L193 was not covered by tests
for area in areas:
if colours.get(area.gss, None) is None:
missing[area.gss] = {"colour": "#ed6832", "opacity": 0}

Check warning on line 196 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L196

Added line #L196 was not covered by tests

return {**colours, **missing, **props}

Check warning on line 198 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L198

Added line #L198 was not covered by tests

def shader_value(self, area):
if self.shader_table == "areadata":
min_max = AreaData.objects.filter(

Check warning on line 202 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L202

Added line #L202 was not covered by tests
area__in=area, **self.shader_filter
).aggregate(
max=models.Max(self.value_col),
min=models.Min(self.value_col),
)

data = (

Check warning on line 209 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L209

Added line #L209 was not covered by tests
AreaData.objects.filter(area__in=area, **self.shader_filter)
.select_related("area", "data_type")
.annotate(
gss=models.F("area__gss"),
)
)
return data, min_max["min"], min_max["max"]

Check warning on line 216 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L216

Added line #L216 was not covered by tests
else:
min_max = PersonData.objects.filter(

Check warning on line 218 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L218

Added line #L218 was not covered by tests
person__area__in=area, **self.shader_filter
).aggregate(
max=models.Max(self.value_col),
min=models.Min(self.value_col),
)

data = (

Check warning on line 225 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L225

Added line #L225 was not covered by tests
PersonData.objects.filter(person__area__in=area, **self.shader_filter)
.select_related("person__area", "data_type")
.annotate(gss=models.F("person__area__gss"))
)
return data, min_max["min"], min_max["max"]

Check warning on line 230 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L230

Added line #L230 was not covered by tests

return None, None, None

Check warning on line 232 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L232

Added line #L232 was not covered by tests


class DataSet(TypeMixin, ShaderMixin, models.Model):
SOURCE_CHOICES = [
("csv", "CSV File"),
("xlxs", "Excel File"),
Expand Down Expand Up @@ -251,144 +397,8 @@ class Meta:
def filter(self, query, **kwargs):
return Filter(self, query).run(**kwargs)

shades = [
"#ffffd9",
"#edf8b1",
"#c7e9b4",
"#7fcdbb",
"#41b6c4",
"#1d91c0",
"#225ea8",
"#253494",
"#081d58",
]

COLOUR_NAMES = {
"red-500": "#CC3517",
"orange-500": "#ED6832",
"yellow-500": "#FEC835",
"teal-600": "#068670",
"blue-500": "#21A8E0",
"purple-500": "#6F42C1",
"gray-500": "#ADB5BD",
"gray-300": "#DEE2E6",
}

def shade(self, val, cmin, cmax):
if val == "":
return None
try:
x = float(val - cmin) / (cmax - cmin)
except ZeroDivisionError:
x = 0.5 # cmax == cmin

shade = int(x * 9) - 1
if shade < 0:
shade = 0
return self.shades[shade]

def colours_for_areas(self, areas):
if len(areas) == 0:
return {"properties": {"no_areas": True}}

values, mininimum, maximum = self.shader_value(areas)
legend = {}
for option in self.options:
if option.get("shader", None) is not None:
legend[option["title"]] = self.COLOUR_NAMES.get(
option["shader"], option["shader"]
)

if len(legend) > 0:
props = {"properties": {"legend": legend}}
else:
d_max = maximum
d_min = mininimum
if self.is_float:
d_max = round(maximum, 1)
d_min = round(mininimum, 1)
if self.is_percentage:
d_max = f"{d_max}%"
d_min = f"{d_min}%"

props = {
"properties": {
"maximum": d_max,
"minimum": d_min,
"shades": self.shades,
}
}
colours = {}
for value in values:
data = value.value()
for option in self.options:
if option["title"] == data:
colours[value.gss] = {
"colour": self.COLOUR_NAMES.get(
option["shader"], option["shader"]
),
"opacity": value.opacity(mininimum, maximum) or 0.7,
"value": data,
"label": self.label,
}

if colours.get(value.gss, None) is None:
shade = self.shade(data, mininimum, maximum)
if shade is not None:
colours[value.gss] = {
"colour": shade,
"opacity": 0.7,
"label": self.label,
"value": data,
}

# if there is no data for an area then need to set the shader to opacity 0 otherwise
# they will end up as the default
missing = {}
for area in areas:
if colours.get(area.gss, None) is None:
missing[area.gss] = {"colour": "#ed6832", "opacity": 0}

return {**colours, **missing, **props}

def shader_value(self, area):
if self.table == "areadata":
min_max = AreaData.objects.filter(
area__in=area, data_type__data_set=self
).aggregate(
max=models.Max(self.value_col),
min=models.Min(self.value_col),
)

data = (
AreaData.objects.filter(area__in=area, data_type__data_set=self)
.select_related("area", "data_type")
.annotate(
gss=models.F("area__gss"),
)
)
return data, min_max["min"], min_max["max"]
else:
min_max = PersonData.objects.filter(
person__area__in=area, data_type__data_set=self
).aggregate(
max=models.Max(self.value_col),
min=models.Min(self.value_col),
)

data = (
PersonData.objects.filter(
person__area__in=area, data_type__data_set=self
)
.select_related("person__area", "data_type")
.annotate(gss=models.F("person__area__gss"))
)
return data, min_max["min"], min_max["max"]

return None, None, None


class DataType(TypeMixin, models.Model):
class DataType(TypeMixin, ShaderMixin, models.Model):
data_set = models.ForeignKey(DataSet, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
data_type = models.CharField(max_length=20, choices=TypeMixin.TYPE_CHOICES)
Expand All @@ -406,6 +416,14 @@ def __str__(self):

return self.name

@property
def shader_table(self):
return self.data_set.table

Check warning on line 421 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L421

Added line #L421 was not covered by tests

@property
def shader_filter(self):
return {"data_type": self}

Check warning on line 425 in hub/models.py

View check run for this annotation

Codecov / codecov/patch

hub/models.py#L425

Added line #L425 was not covered by tests


class UserDataSets(models.Model):
data_set = models.ForeignKey(DataSet, on_delete=models.CASCADE)
Expand Down
14 changes: 12 additions & 2 deletions hub/static/js/explore.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ const app = createApp({
addShader(datasetName) {
this.shader = this.getDataset(datasetName)

if (!this.shader.selectedType && this.shader.types) {
this.shader.selectedType = this.shader.types[0].name
}

trackEvent('explore_shader_added', {
'dataset': datasetName
});
Expand Down Expand Up @@ -220,7 +224,13 @@ const app = createApp({
}
})

if (this.shader) { state['shader'] = this.shader.name }
if (this.shader) {
if (this.shader.selectedType) {
state['shader'] = this.shader.selectedType
} else {
state['shader'] = this.shader.name
}
}

// don’t bother saving view unless it’s been changed from default
if ( this.view != 'map' ) { state['view'] = this.view }
Expand Down Expand Up @@ -493,7 +503,7 @@ const app = createApp({
case 'filter':
return dataset.is_filterable
case 'shader':
return dataset.is_shadable
return ["party", "constituency_ruc"].includes(dataset.name) || !["text", "json", "date", "profile_id"].includes(dataset.data_type)
default:
return true
}
Expand Down
5 changes: 5 additions & 0 deletions hub/templates/hub/explore.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ <h3 class="h6 mb-0 me-auto">${ shader.title }</h3>
<span class="flex-grow-1">${ title }</span>
</li>
</ul>
<div :class="{ 'filter__body': true, 'filter__body--expanded': true }">
<select v-if="shader.is_range" v-model="shader.selectedType" class="form-select form-select-sm flex-grow-0 flex-shrink-1">
<option v-for="ds_type in shader.types" :value="ds_type.name">${ ds_type.title }</option>
</select>
</div>
<div v-if="key" class="mt-3">
<div class="d-flex">
<span v-for="shade in key.shades" class="pt-3 flex-grow-1" :style="`background-color: ${ shade }`"></span>
Expand Down

0 comments on commit f0c72f6

Please sign in to comment.