mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Add tag cloud
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user