diff --git a/seshat/apps/core/static/core/js/map_functions.js b/seshat/apps/core/static/core/js/map_functions.js index 0d33d000e..0db043ffc 100644 --- a/seshat/apps/core/static/core/js/map_functions.js +++ b/seshat/apps/core/static/core/js/map_functions.js @@ -410,6 +410,39 @@ function updateLegend() { legendDiv.appendChild(polityContainer); } + } else if (hierarchicalComplexityVariablesFull.includes(variable)) { + + var legendTitle = document.createElement('h3'); + legendTitle.textContent = variable; + legendDiv.appendChild(legendTitle); + + let variable_underscore = variable.toLowerCase().replace(' ', '_'); + // Get the maximum value of the hierarchical complexity variable + let hierarchicalVariableMaxValue = highestComplexityValues[variable_underscore]; + + for (var key in hierarchicalComplexityColourMapping) { + + var legendItem = document.createElement('p'); + + var colorBox = document.createElement('span'); + colorBox.style.display = 'inline-block'; + colorBox.style.width = '20px'; + colorBox.style.height = '20px'; + colorBox.style.backgroundColor = hierarchicalComplexityColourMapping[key]; + colorBox.style.marginRight = '10px'; + legendItem.appendChild(colorBox); + + if (key === 'min') { + legendItem.appendChild(document.createTextNode(0)); + } else if (key === 'max') { + legendItem.appendChild(document.createTextNode(hierarchicalVariableMaxValue)); + } else { + legendItem.appendChild(document.createTextNode(`${key}`)); + } + + legendDiv.appendChild(legendItem); + }; + } else if (variable in categorical_variables) { var legendTitle = document.createElement('h3'); @@ -601,6 +634,9 @@ function clearSelection() { }); document.getElementById('hideUnselected').checked = false; plotPolities(); + if (document.getElementById('chooseVariable').value != 'polity') { + legendDiv.style.display = 'block'; + }; } function selectAllCheckbox() { @@ -704,9 +740,14 @@ function populateVariableDropdown(variables) { Object.entries(vars).forEach(([variable, details]) => { const option = document.createElement('option'); option.value = details.formatted; - option.textContent = details.full_name; + if (hierarchicalComplexityVariables.includes(variable)) { + option.textContent = "Hierarchical Complexity: " + details.full_name; + } else { + option.textContent = details.full_name; + } optgroup.appendChild(option); }); + optgroup.innerHTML = [...optgroup.children].sort((a, b) => a.textContent.localeCompare(b.textContent)).map(e => e.outerHTML).join(''); chooseVariableDropdown.appendChild(optgroup); } }); @@ -827,4 +868,40 @@ function closeHelp() { if (popup !== null) { popup.style.display = 'block'; } +} + +function hierarchicalComplexityColour(maxValue, value) { + // If the value is null, return silver (Uncoded) + if (value == null) { + return 'silver'; + } + // If the value is 0, return the min color + if (value == 0) { + return hierarchicalComplexityColourMapping['min']; + } + // If the value is greater than the maximum value, return the max color + if (value > maxValue) { + return hierarchicalComplexityColourMapping['max']; + } + // Calculate the colour based on the value and the maximum value + let ratio = value / maxValue; + + // Convert hex to RGB + function hexToRgb(hex) { + let bigint = parseInt(hex.slice(1), 16); + return { + r: (bigint >> 16) & 255, + g: (bigint >> 8) & 255, + b: bigint & 255 + }; + } + + let startColor = hexToRgb(hierarchicalComplexityColourMapping['min']); + let endColor = hexToRgb(hierarchicalComplexityColourMapping['max']); + + let r = Math.round(startColor.r + ratio * (endColor.r - startColor.r)); + let g = Math.round(startColor.g + ratio * (endColor.g - startColor.g)); + let b = Math.round(startColor.b + ratio * (endColor.b - startColor.b)); + + return `rgb(${r}, ${g}, ${b})`; } \ No newline at end of file diff --git a/seshat/apps/core/templates/core/world_map.html b/seshat/apps/core/templates/core/world_map.html index e951d944a..52614d940 100644 --- a/seshat/apps/core/templates/core/world_map.html +++ b/seshat/apps/core/templates/core/world_map.html @@ -614,6 +614,9 @@

How to use the Seshat World Map

updateCategoricalVariableSelection(localStorage.getItem('variable')); }; } + + var hierarchicalComplexityVariablesFull = ['Military Level', 'Administrative Level', 'Religious Level', 'Settlement Hierarchy']; + var hierarchicalComplexityVariables = ['military_level', 'administrative_level', 'religious_level', 'settlement_hierarchy']; var displayedShapes = []; var polityBorderWeight = 0; @@ -667,6 +670,18 @@

How to use the Seshat World Map

if (shape.alternate_religion) { shape.alternate_religion = JSON.parse(shape.alternate_religion.replace(/'/g, '"')); } + if (shape.settlement_hierarchy) { + shape.settlement_hierarchy = JSON.parse(shape.settlement_hierarchy.replace(/'/g, '"')); + } + if (shape.religious_level) { + shape.religious_level = JSON.parse(shape.religious_level.replace(/'/g, '"')); + } + if (shape.military_level) { + shape.military_level = JSON.parse(shape.military_level.replace(/'/g, '"')); + } + if (shape.administrative_level) { + shape.administrative_level = JSON.parse(shape.administrative_level.replace(/'/g, '"')); + } // Any fields that end _dict should be converted to a dictionary from a string for (const [key, value] of Object.entries(shape)) { if (key.endsWith('_dict')) { @@ -702,6 +717,11 @@

How to use the Seshat World Map

var tick_years = {{ tick_years }}; setSliderTicks(tick_years); + var highestSettlementHierarchy; + var highestReligiousLevel; + var highestMilitaryLevel; + var highestAdministrativeLevel; + // Load all polity shapes and modern province/country shapes in background window.addEventListener('load', function () { function fetchData(url, displayYear) { @@ -751,6 +771,9 @@

How to use the Seshat World Map

plotPolities(); document.getElementById('variablesLoadingIndicator').style.display = 'none'; + // Get the highest values of the hierarchical complexity variables + highestComplexityValues = data.highest_complexity_values; + return fetch('/core/provinces_and_countries'); }) .then(response => response.json()) @@ -814,6 +837,12 @@

How to use the Seshat World Map

'No Seshat page': 'silver' }; + let hierarchicalComplexityColourMapping = { + 'min': '#0000FF', // blue + 'max': '#FF0000', // red + 'Uncoded': 'silver' + }; + function switchBaseMapWorldMap() { switchBaseMap(); if (document.getElementById('chooseVariable').value != 'polity') { @@ -922,6 +951,18 @@

How to use the Seshat World Map

shapeWeight = shape.weight; shapeColour = shape.colour; + // Hierarchical complexity variables + } else if (hierarchicalComplexityVariablesFull.includes(variable)) { + shapeWeight = polityBorderWeightSelected; + let variable_underscore = variable.toLowerCase().replace(' ', '_'); + // Get the maximum value of the hierarchical complexity variable + let hierarchicalVariableMaxValue = highestComplexityValues[variable_underscore]; + // Calculate the colour based highest of the from and to values + if (shape[variable_underscore][1] != 'None' && shape[variable_underscore][1] != null) { // If the to value is not None, use it since it is the highest + shapeColour = hierarchicalComplexityColour(hierarchicalVariableMaxValue, shape[variable_underscore][1]); + } else { // If the to value is None, use the from value (even if it is None) + shapeColour = hierarchicalComplexityColour(hierarchicalVariableMaxValue, shape[variable_underscore][0]); + } } else if (variable in categorical_variables){ shapeWeight = polityBorderWeightSelected; // If the shape has a dictionary for the variable the key of the dict is the variable value and the value is a list of start and end years. @@ -1095,7 +1136,7 @@

How to use the Seshat World Map

`; // Absent/present variables - if (variable != 'polity' && !categorical_variables.hasOwnProperty(variable)) { + if (variable != 'polity' && !categorical_variables.hasOwnProperty(variable) && !hierarchicalComplexityVariablesFull.includes(variable)) { if (shape[variable + '_dict']) { // Iterate through key/values in the dictionary var counter = 0; @@ -1158,60 +1199,156 @@

How to use the Seshat World Map

// this will need to be updated to use shape[variable + '_dict'] as done above for absent/present variables // Note: the actual colouring of the shapes is done in the style function above and this is already set up to handle the dictionaries popupContent = popupContent + ` - - Languages - ${shape.language.join(', ')} - - - Language Genus - ${shape.language_genus.join(', ')} - - - Linguistic Family - ${shape.linguistic_family.join(', ')} - - `; - } - if (variable == 'religious_tradition' || variable == 'religion' || variable == 'religion_family' || variable == 'religion_genus' || variable == 'alternate_religion' || variable == 'alternate_religion_family' || variable == 'alternate_religion_genus') { - popupContent = popupContent + ` - - Religious Traditions - ${shape.religious_tradition} - - - Religions - ${shape.religion.join(', ')} - - - Religion Genus - ${shape.religion_genus.join(', ')} - - - Religion Family - ${shape.religion_family.join(', ')} - - - Alternate Religions - ${shape.alternate_religion.join(', ')} - - - Alternate Religion Genus - ${shape.alternate_religion_genus.join(', ')} - - - Alternate Religion Family - ${shape.alternate_religion_family.join(', ')} - - `; - } - if (allCapitalsInfo[shape.seshat_id]) { - if (allCapitalsInfo[shape.seshat_id]['year_from'] <= selectedYearInt && allCapitalsInfo[shape.seshat_id]['year_to'] >= selectedYearInt) { - popupContent = popupContent + ` - - Capital: - ${allCapitalsInfo[shape.seshat_id].map(capital => capital.capital).join(', ')} - - `; + + Languages + ${shape.language.join(', ')} + + + Language Genus + ${shape.language_genus.join(', ')} + + + Linguistic Family + ${shape.linguistic_family.join(', ')} + + `; + } + if (variable == 'religious_tradition' || variable == 'religion' || variable == 'religion_family' || variable == 'religion_genus' || variable == 'alternate_religion' || variable == 'alternate_religion_family' || variable == 'alternate_religion_genus') { + popupContent = popupContent + ` + + Religious Traditions + ${shape.religious_tradition} + + + Religions + ${shape.religion.join(', ')} + + + Religion Genus + ${shape.religion_genus.join(', ')} + + + Religion Family + ${shape.religion_family.join(', ')} + + + Alternate Religions + ${shape.alternate_religion.join(', ')} + + + Alternate Religion Genus + ${shape.alternate_religion_genus.join(', ')} + + + Alternate Religion Family + ${shape.alternate_religion_family.join(', ')} + + `; + } + if (variable == 'Settlement Hierarchy') { + if (shape.settlement_hierarchy.length == 0 || ((shape.settlement_hierarchy[0] == 'None' || shape.settlement_hierarchy[0] == null) && (shape.settlement_hierarchy[1] == 'None' || shape.settlement_hierarchy[1] == null))) { + popupContent = popupContent + ` + + Settlement Hierarchy + Uncoded + + `; + } else if (shape.settlement_hierarchy[0] == shape.settlement_hierarchy[1] || shape.settlement_hierarchy[1] == 'None' || shape.settlement_hierarchy[1] == null) { + popupContent = popupContent + ` + + Settlement Hierarchy + ${shape.settlement_hierarchy[0]} + + `; + } else { + popupContent = popupContent + ` + + Settlement Hierarchy + ${shape.settlement_hierarchy[0]} to ${shape.settlement_hierarchy[1]} + + `; + } + } + if (variable == 'Religious Level') { + if (shape.religious_level.length == 0 || ((shape.religious_level[0] == 'None' || shape.religious_level[0] == null) && (shape.religious_level[1] == 'None' || shape.religious_level[1] == null))) { + popupContent = popupContent + ` + + Religious Level + Uncoded + + `; + } else if (shape.religious_level[0] == shape.religious_level[1] || shape.religious_level[1] == 'None' || shape.religious_level[1] == null) { + popupContent = popupContent + ` + + Religious Level + ${shape.religious_level[0]} + + `; + } else { + popupContent = popupContent + ` + + Religious Level + ${shape.religious_level[0]} to ${shape.religious_level[1]} + + `; + } + } + if (variable == 'Military Level') { + if (shape.military_level.length == 0 || ((shape.military_level[0] == 'None' || shape.military_level[0] == null) && (shape.military_level[1] == 'None' || shape.military_level[1] == null))) { + popupContent = popupContent + ` + + Military Level + Uncoded + + `; + } else if (shape.military_level[0] == shape.military_level[1] || shape.military_level[1] == 'None' || shape.military_level[1] == null) { + popupContent = popupContent + ` + + Military Level + ${shape.military_level[0]} + + `; + } else { + popupContent = popupContent + ` + + Military Level + ${shape.military_level[0]} to ${shape.military_level[1]} + + `; + } + } + if (variable == 'Administrative Level') { + if (shape.administrative_level.length == 0 || ((shape.administrative_level[0] == 'None' || shape.administrative_level[0] == null) && (shape.administrative_level[1] == 'None' || shape.administrative_level[1] == null))) { + popupContent = popupContent + ` + + Administrative Level + Uncoded + + `; + } else if (shape.administrative_level[0] == shape.administrative_level[1] || shape.administrative_level[1] == 'None' || shape.administrative_level[1] == null) { + popupContent = popupContent + ` + + Administrative Level + ${shape.administrative_level[0]} + + `; + } else { + popupContent = popupContent + ` + + Administrative Level + ${shape.administrative_level[0]} to ${shape.administrative_level[1]} + + `; + } + } + if (allCapitalsInfo[shape.seshat_id]) { + if (allCapitalsInfo[shape.seshat_id]['year_from'] <= selectedYearInt && allCapitalsInfo[shape.seshat_id]['year_to'] >= selectedYearInt) { + popupContent = popupContent + ` + + Capital: + ${allCapitalsInfo[shape.seshat_id].map(capital => capital.capital).join(', ')} + + `; popupContent = popupContent + ` diff --git a/seshat/apps/core/tests/tests.py b/seshat/apps/core/tests/tests.py index deeece1bc..3e57586c4 100644 --- a/seshat/apps/core/tests/tests.py +++ b/seshat/apps/core/tests/tests.py @@ -4,7 +4,7 @@ from django.urls import reverse from ..models import Cliopatria, GADMShapefile, GADMCountries, GADMProvinces, Polity, Capital from ...general.models import Polity_capital, Polity_peak_years, Polity_language, Polity_religious_tradition -from ...sc.models import Judge +from ...sc.models import Judge, Settlement_hierarchy, Religious_level, Military_level, Administrative_level from ...wf.models import Copper from ...rt.models import Gov_res_pub_pros from ..views import get_provinces, get_polity_shape_content, get_all_polity_capitals, assign_variables_to_shapes, assign_categorical_variables_to_shapes @@ -176,6 +176,30 @@ def setUp(self): religious_tradition='Islam', polity_id=2 ) + Settlement_hierarchy.objects.create( + name='settlement_hierarchy', + settlement_hierarchy_from=6, + settlement_hierarchy_to=7, + polity_id=2 + ) + Religious_level.objects.create( + name='religious_level', + religious_level_from=9, + religious_level_to=10, + polity_id=2 + ) + Military_level.objects.create( + name='military_level', + military_level_from=13, + military_level_to=14, + polity_id=2 + ) + Administrative_level.objects.create( + name='administrative_level', + administrative_level_from=5, + administrative_level_to=6, + polity_id=2 + ) # Model tests @@ -586,4 +610,8 @@ def test_assign_categorical_variables_to_shapes(self): self.assertEqual(result_shapes[0]['language'], ['English', 'French']) self.assertEqual(result_shapes[0]['language_dict']['English'], [1998, 2000]) self.assertEqual(result_shapes[0]['language_dict']['French'], [1999, 2007]) - self.assertEqual(result_shapes[0]['religious_tradition'], ['Christianity', 'Islam']) \ No newline at end of file + self.assertEqual(result_shapes[0]['religious_tradition'], ['Christianity', 'Islam']) + self.assertEqual(result_shapes[0]['settlement_hierarchy'], [6, 7]) + self.assertEqual(result_shapes[0]['religious_level'], [9, 10]) + self.assertEqual(result_shapes[0]['military_level'], [13, 14]) + self.assertEqual(result_shapes[0]['administrative_level'], [5, 6]) \ No newline at end of file diff --git a/seshat/apps/core/views.py b/seshat/apps/core/views.py index 22d630b2c..3ee58ca5a 100644 --- a/seshat/apps/core/views.py +++ b/seshat/apps/core/views.py @@ -71,6 +71,7 @@ from django.contrib.messages.views import SuccessMessageMixin from ..general.models import Polity_research_assistant, Polity_duration, Polity_linguistic_family, Polity_language_genus, Polity_language, POLITY_LINGUISTIC_FAMILY_CHOICES, POLITY_LANGUAGE_GENUS_CHOICES, POLITY_LANGUAGE_CHOICES, Polity_religious_tradition, Polity_religion_genus, Polity_religion_family, Polity_religion, Polity_alternate_religion_genus, Polity_alternate_religion_family, Polity_alternate_religion, POLITY_RELIGION_GENUS_CHOICES, POLITY_RELIGION_FAMILY_CHOICES, POLITY_RELIGION_CHOICES +from ..sc.models import Settlement_hierarchy, Religious_level, Military_level, Administrative_level from ..crisisdb.models import Power_transition @@ -4259,7 +4260,7 @@ def assign_categorical_variables_to_shapes(shapes, variables): Assign the categorical variables to the shapes. Note: - Currently only language and religion variables are implemented. + Extend this function to add more of the variables. Args: shapes (list): The shapes to assign the variables to. @@ -4268,7 +4269,7 @@ def assign_categorical_variables_to_shapes(shapes, variables): Returns: tuple: A tuple containing the shapes and the variables. """ - # Add categorical variables to the variables dictionary + # Add categorical variables from General Variables to the variables dictionary variables['General Variables'] = { 'polity_linguistic_family': {'formatted': 'linguistic_family', 'full_name': 'Linguistic Family'}, 'polity_language_genus': {'formatted': 'language_genus', 'full_name': 'Language Genus'}, @@ -4282,10 +4283,18 @@ def assign_categorical_variables_to_shapes(shapes, variables): 'polity_alternate_religion': {'formatted': 'alternate_religion', 'full_name': 'Alternate Religion'}, } + # Add categorical variables from Social Complexity Variables to the variables dictionary + if 'Social Complexity Variables' not in variables: + variables['Social Complexity Variables'] = {} + variables['Social Complexity Variables']['settlement_hierarchy'] = {'formatted': 'Settlement Hierarchy', 'full_name': 'Settlement Hierarchy'} + variables['Social Complexity Variables']['religious_level'] = {'formatted': 'Religious Level', 'full_name': 'Religious Level'} + variables['Social Complexity Variables']['military_level'] = {'formatted': 'Military Level', 'full_name': 'Military Level'} + variables['Social Complexity Variables']['administrative_level'] = {'formatted': 'Administrative Level', 'full_name': 'Administrative Level'} + # Fetch all polities and store them in a dictionary for quick access polities = {polity.new_name: polity for polity in Polity.objects.all()} - # Fetch all linguistic families, language genuses, and languages and store them in dictionaries for quick access + # Fetch all categorical variables and store them in dictionaries for quick access linguistic_families = {} for lf in Polity_linguistic_family.objects.all(): if lf.polity_id not in linguistic_families: @@ -4346,7 +4355,7 @@ def assign_categorical_variables_to_shapes(shapes, variables): alternate_religions[ar.polity_id] = [] alternate_religions[ar.polity_id].append(ar) - # Add language variable info to polity shapes + # Add categorical variable info to polity shapes for shape in shapes: shape['linguistic_family'] = [] shape['linguistic_family_dict'] = {} @@ -4368,6 +4377,10 @@ def assign_categorical_variables_to_shapes(shapes, variables): shape['alternate_religion_family_dict'] = {} shape['alternate_religion'] = [] shape['alternate_religion_dict'] = {} + shape['settlement_hierarchy'] = [] + shape['religious_level'] = [] + shape['military_level'] = [] + shape['administrative_level'] = [] if shape['seshat_id'] != 'none': # Skip shapes with no seshat_id polity = polities.get(shape['seshat_id']) if polity: @@ -4382,8 +4395,24 @@ def assign_categorical_variables_to_shapes(shapes, variables): shape['alternate_religion_genus'].extend([arg.alternate_religion_genus for arg in alternate_religion_genuses.get(polity.id, [])]) shape['alternate_religion_family'].extend([arf.alternate_religion_family for arf in alternate_religion_families.get(polity.id, [])]) shape['alternate_religion'].extend([ar.alternate_religion for ar in alternate_religions.get(polity.id, [])]) - - # Get the years for the variables for the polity + settlement_hierarchy = Settlement_hierarchy.objects.filter(polity_id=polity.id) + if settlement_hierarchy: + shape['settlement_hierarchy'].append(settlement_hierarchy[0].settlement_hierarchy_from) + shape['settlement_hierarchy'].append(settlement_hierarchy[0].settlement_hierarchy_to) + religious_level = Religious_level.objects.filter(polity_id=polity.id) + if religious_level: + shape['religious_level'].append(religious_level[0].religious_level_from) + shape['religious_level'].append(religious_level[0].religious_level_to) + military_level = Military_level.objects.filter(polity_id=polity.id) + if military_level: + shape['military_level'].append(military_level[0].military_level_from) + shape['military_level'].append(military_level[0].military_level_to) + administrative_level = Administrative_level.objects.filter(polity_id=polity.id) + if administrative_level: + shape['administrative_level'].append(administrative_level[0].administrative_level_from) + shape['administrative_level'].append(administrative_level[0].administrative_level_to) + + # Get the years for the variables which have years for the polity shape['linguistic_family_dict'].update({lf.linguistic_family: [lf.year_from, lf.year_to] for lf in linguistic_families.get(polity.id, [])}) shape['language_genus_dict'].update({lg.language_genus: [lg.year_from, lg.year_to] for lg in language_genuses.get(polity.id, [])}) shape['language_dict'].update({l.language: [l.year_from, l.year_to] for l in languages.get(polity.id, [])}) @@ -4395,7 +4424,7 @@ def assign_categorical_variables_to_shapes(shapes, variables): shape['alternate_religion_family_dict'].update({arf.alternate_religion_family: [arf.year_from, arf.year_to] for arf in alternate_religion_families.get(polity.id, [])}) shape['alternate_religion_dict'].update({ar.alternate_religion: [ar.year_from, ar.year_to] for ar in alternate_religions.get(polity.id, [])}) - # If no linguistic family, language genus, or language was found, append 'Uncoded' + # If no variable was found, append 'Uncoded' polity = polities.get(shape['seshat_id']) if polity: if not shape['linguistic_family']: @@ -4617,6 +4646,13 @@ def map_view_all_with_vars(request): # Set the last year in history we ever want to display, which will be used to determine when we should say "present" content['last_history_year'] = content['latest_year'] # Set this to the latest year in the data or a value of choice + # Get the highest values of hierarchical complexity variables for the legend + content['highest_complexity_values'] = {} + content['highest_complexity_values']['settlement_hierarchy'] = max([max(filter(None, shape['settlement_hierarchy']), default=0) for shape in content['shapes'] if shape['settlement_hierarchy']], default=0) + content['highest_complexity_values']['religious_level'] = max([max(filter(None, shape['religious_level']), default=0) for shape in content['shapes'] if shape['religious_level']], default=0) + content['highest_complexity_values']['military_level'] = max([max(filter(None, shape['military_level']), default=0) for shape in content['shapes'] if shape['military_level']], default=0) + content['highest_complexity_values']['administrative_level'] = max([max(filter(None, shape['administrative_level']), default=0) for shape in content['shapes'] if shape['administrative_level']], default=0) + return JsonResponse(content) def provinces_and_countries_view(request):