Add tag cloud

This commit is contained in:
2025-03-02 10:15:59 +11:00
parent 82297c08e7
commit 5be33cd718
3 changed files with 338 additions and 10 deletions
+70
View File
@@ -5,6 +5,66 @@ from django.shortcuts import get_object_or_404
from .models import Tag
from .forms import TagForm
from django.db.models import Count
from django.core.cache import cache
from django.utils import timezone
import json
import random
def get_tag_cloud_data():
"""
Get tag cloud data with caching.
Returns a list of tags with name, count, slug, and size attributes.
The size is a value from 1-10 based on the tag's usage count.
"""
# Try to get from cache first
tag_cloud = cache.get('tag_cloud_data')
if tag_cloud is not None:
return tag_cloud
# If not in cache, generate the data
tags = Tag.objects.annotate(
links_count=Count('links', distinct=True),
pages_count=Count('pages', distinct=True),
posts_count=Count('posts', distinct=True),
image_collections_count=Count('image_collections', distinct=True)
)
# Calculate total count for each tag
tag_data = []
for tag in tags:
count = (
tag.links_count +
tag.pages_count +
tag.posts_count +
tag.image_collections_count
)
if count > 0: # Only include tags that are used
tag_data.append({
'name': tag.name,
'slug': tag.slug,
'count': count
})
# Sort by count in descending order
tag_data.sort(key=lambda x: x['count'], reverse=True)
# Calculate size based on count (for font sizing in the cloud)
if tag_data:
max_count = max(item['count'] for item in tag_data)
min_count = min(item['count'] for item in tag_data)
range_count = max(1, max_count - min_count) # Avoid division by zero
for item in tag_data:
# Calculate a size value between 1 and 10
if range_count == 1:
item['size'] = 5 # If all tags have the same count
else:
item['size'] = 1 + int(9 * (item['count'] - min_count) / range_count)
# Store in cache for 1 minute
cache.set('tag_cloud_data', tag_data, 60)
return tag_data
class TagListView(ListView):
model = Tag
@@ -29,6 +89,16 @@ class TagListView(ListView):
tag.image_collections_count
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['tag_cloud'] = get_tag_cloud_data()
# Pass the tag colors to the template
context['tag_colors'] = [
'#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6',
'#1abc9c', '#d35400', '#34495e', '#16a085', '#27ae60'
]
return context
class TagDetailView(DetailView):
model = Tag
+90 -10
View File
@@ -10,6 +10,12 @@
</a>
</div>
<!-- Tag Cloud Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">{% trans "Tag Cloud" %}</h2>
<div id="tag-cloud-container" style="width: 100%; height: 400px;"></div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{% for tag in tags %}
<a href="{% url 'tag-detail' tag.slug %}" class="block group">
@@ -65,34 +71,108 @@
</div>
{% endfor %}
</div>
</div>
<!-- Include ECharts and wordcloud extension -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts-wordcloud@2.1.0/dist/echarts-wordcloud.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Debug message
console.log('Script is running');
// Define colors for tags
// Define colors for tags - using colors from the image
const tagColors = [
'#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6',
'#1abc9c', '#d35400', '#34495e', '#16a085', '#27ae60'
'#3498db', // blue
'#2ecc71', // green
'#e74c3c', // red
'#f39c12', // orange
'#9b59b6', // purple
'#1abc9c', // teal
'#d35400', // dark orange
'#34495e', // navy
'#16a085', // dark green
'#27ae60', // medium green
'#8e44ad', // violet
'#f1c40f' // yellow
];
// Get all tag cards
// Apply colors to tag cards
const tagCards = document.querySelectorAll('.tag-card');
console.log('Found tag cards:', tagCards.length);
// Apply random colors to each tag card
tagCards.forEach((card, index) => {
// Add a random color
const randomColor = tagColors[Math.floor(Math.random() * tagColors.length)];
card.style.backgroundColor = randomColor;
card.style.color = 'white';
// Log for debugging
console.log('Applied color:', randomColor, 'to card index:', index);
});
// Initialize ECharts instance
const chartDom = document.getElementById('tag-cloud-container');
const myChart = echarts.init(chartDom);
// Prepare data for word cloud
const tagCloudData = [];
{% for tag in tag_cloud %}
tagCloudData.push({
name: '{{ tag.name }}',
value: {{ tag.count }},
slug: '{{ tag.slug }}'
});
{% endfor %}
// Configure word cloud options
const option = {
series: [{
type: 'wordCloud',
shape: 'circle',
left: 'center',
top: 'center',
width: '100%',
height: '100%',
right: null,
bottom: null,
sizeRange: [14, 60], // Increased max size for better visibility
rotationRange: [0, 0], // No rotation for better readability like in the image
rotationStep: 0,
gridSize: 15, // Increased grid size for better spacing
drawOutOfBound: false,
layoutAnimation: true, // Enable animation when rendering
textStyle: {
fontFamily: 'sans-serif',
fontWeight: 'bold',
color: function () {
return tagColors[Math.floor(Math.random() * tagColors.length)];
}
},
emphasis: {
textStyle: {
fontWeight: 'bold',
shadowBlur: 10,
shadowColor: '#333'
}
},
data: tagCloudData
}]
};
// Set configuration and render chart
myChart.setOption(option);
// Add click event to navigate to tag detail page
myChart.on('click', function(params) {
const clickedTag = tagCloudData.find(tag => tag.name === params.name);
if (clickedTag) {
window.location.href = `/ui/tags/${clickedTag.slug}/`;
}
});
// Make chart responsive
window.addEventListener('resize', function() {
myChart.resize();
});
});
</script>
{% endblock %}
{% endblock %}
+178
View File
@@ -0,0 +1,178 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">{% trans "Tags" %}</h1>
<a href="{% url 'tag-create' %}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
{% trans "New Tag" %}
</a>
</div>
<!-- Tag Cloud Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">{% trans "Tag Cloud" %}</h2>
<div id="tag-cloud-container" style="width: 100%; height: 400px;"></div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{% for tag in tags %}
<a href="{% url 'tag-detail' tag.slug %}" class="block group">
<div
class="tag-card rounded-lg p-4 h-full flex flex-col items-center justify-center text-center shadow-md hover:shadow-lg transition-all duration-200">
<div class="mb-2">
{% with icon_classes="w-8 h-8 mx-auto" %}
{% if forloop.counter|divisibleby:5 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
{% elif forloop.counter|divisibleby:4 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{% elif forloop.counter|divisibleby:3 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{% elif forloop.counter|divisibleby:2 %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
</svg>
{% else %}
<svg class="{{ icon_classes }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
{% endif %}
{% endwith %}
</div>
<h2 class="text-lg font-semibold mb-1">{{ tag.name }}</h2>
<div class="mt-1">
<span
class="inline-flex items-center justify-center bg-white bg-opacity-30 text-xs font-medium px-2 py-1 rounded-full">
{{ tag.total_count }} {% trans "uses" %}
</span>
</div>
</div>
</a>
{% empty %}
<div class="col-span-full text-center py-8 text-gray-500">
{% trans "No tags found." %}
</div>
{% endfor %}
</div>
</div>
<!-- Include ECharts and wordcloud extension -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts-wordcloud@2.1.0/dist/echarts-wordcloud.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Debug message
console.log('Script is running');
// Define colors for tags - using colors from the image
const tagColors = [
'#3498db', // blue
'#2ecc71', // green
'#e74c3c', // red
'#f39c12', // orange
'#9b59b6', // purple
'#1abc9c', // teal
'#d35400', // dark orange
'#34495e', // navy
'#16a085', // dark green
'#27ae60', // medium green
'#8e44ad', // violet
'#f1c40f' // yellow
];
// Apply colors to tag cards
const tagCards = document.querySelectorAll('.tag-card');
console.log('Found tag cards:', tagCards.length);
tagCards.forEach((card, index) => {
const randomColor = tagColors[Math.floor(Math.random() * tagColors.length)];
card.style.backgroundColor = randomColor;
card.style.color = 'white';
console.log('Applied color:', randomColor, 'to card index:', index);
});
// Initialize ECharts instance
const chartDom = document.getElementById('tag-cloud-container');
const myChart = echarts.init(chartDom);
// Prepare data for word cloud
const tagCloudData = [];
{% for tag in tag_cloud %}
tagCloudData.push({
name: '{{ tag.name }}',
value: {{ tag.count }},
slug: '{{ tag.slug }}'
});
{% endfor %}
// Configure word cloud options
const option = {
series: [{
type: 'wordCloud',
shape: 'circle',
left: 'center',
top: 'center',
width: '100%',
height: '100%',
right: null,
bottom: null,
sizeRange: [14, 60], // Increased max size for better visibility
rotationRange: [0, 0], // No rotation for better readability like in the image
rotationStep: 0,
gridSize: 15, // Increased grid size for better spacing
drawOutOfBound: false,
layoutAnimation: true, // Enable animation when rendering
textStyle: {
fontFamily: 'sans-serif',
fontWeight: 'bold',
color: function () {
return tagColors[Math.floor(Math.random() * tagColors.length)];
}
},
emphasis: {
textStyle: {
fontWeight: 'bold',
shadowBlur: 10,
shadowColor: '#333'
}
},
data: tagCloudData
}]
};
// Set configuration and render chart
myChart.setOption(option);
// Add click event to navigate to tag detail page
myChart.on('click', function(params) {
const clickedTag = tagCloudData.find(tag => tag.name === params.name);
if (clickedTag) {
window.location.href = `/tags/${clickedTag.slug}/`;
}
});
// Make chart responsive
window.addEventListener('resize', function() {
myChart.resize();
});
});
</script>
{% endblock %}