locate-the-news / frontend / src / utils / clusteringUtils.js
clusteringUtils.js
Raw
import Supercluster from 'supercluster';
import { CLUSTER_CONFIG } from '../config/MapboxConfig';

/**
 * Initialize Supercluster with custom configuration
 * @param {Array} events - Array of news events with location data
 * @returns {Supercluster} Configured supercluster instance
 */
export const initializeCluster = (events) => {
    const cluster = new Supercluster({
        radius: CLUSTER_CONFIG.radius,
        maxZoom: CLUSTER_CONFIG.maxZoom,
        minZoom: CLUSTER_CONFIG.minZoom,
        minPoints: CLUSTER_CONFIG.minPoints,
        map: (props) => ({
            sentiment_sum: props.sentiment_score || 0,
            sentiment_count: props.sentiment_score !== null ? 1 : 0,
            negative_count: (props.sentiment_score || 0) < -0.3 ? 1 : 0
        }),
        reduce: (accumulated, props) => {
            accumulated.sentiment_sum += props.sentiment_sum;
            accumulated.sentiment_count += props.sentiment_count;
            accumulated.negative_count += props.negative_count;
        }
    });

    // Convert events to GeoJSON features
    const features = events.map(event => ({
        type: 'Feature',
        properties: {
            id: event.id,
            title: event.title,
            summary: event.summary,
            link: event.link,
            description: event.description,
            priority: event.priority,
            sentiment_score: event.sentiment_score || 0,
            sentiment_label: event.sentiment_label || 'neutral',
            source_name: event.source_name,
            published_date: event.published_date,
            entity_name: event.location?.entity_name || 'Unknown'
        },
        geometry: {
            type: 'Point',
            coordinates: [
                event.location?.longitude || 0,
                event.location?.latitude || 0
            ]
        }
    }));

    cluster.load(features);
    return cluster;
};

/**
 * Get clusters and points for current map bounds and zoom
 * @param {Supercluster} cluster - Supercluster instance
 * @param {Object} bounds - Map bounds {west, south, east, north}
 * @param {number} zoom - Current zoom level
 * @returns {Array} Array of clusters and individual points
 */
export const getClustersInBounds = (cluster, bounds, zoom) => {
    if (!cluster || !bounds) return [];

    return cluster.getClusters(
        [bounds.west, bounds.south, bounds.east, bounds.north],
        Math.floor(zoom)
    );
};

/**
 * Get individual points from a cluster
 * @param {Supercluster} cluster - Supercluster instance
 * @param {number} clusterId - Cluster ID
 * @returns {Array} Array of individual point features
 */
export const getClusterExpansion = (cluster, clusterId) => {
    if (!cluster) return [];
    return cluster.getLeaves(clusterId, Infinity);
};

/**
 * Calculate average sentiment for a cluster
 * @param {Object} clusterProperties - Cluster properties from supercluster
 * @returns {number} Average sentiment score
 */
export const getClusterSentiment = (clusterProperties) => {
    if (!clusterProperties.sentiment_count) return 0;
    return clusterProperties.sentiment_sum / clusterProperties.sentiment_count;
};

/**
 * Calculate negative news ratio for a cluster
 * @param {Object} clusterProperties - Cluster properties
 * @returns {number} Ratio of negative news (0 to 1)
 */
export const getClusterNegativeRatio = (clusterProperties) => {
    const total = clusterProperties.point_count || 1;
    return (clusterProperties.negative_count || 0) / total;
};

/**
 * Get zoom level to expand a cluster
 * @param {Supercluster} cluster - Supercluster instance
 * @param {number} clusterId - Cluster ID
 * @returns {number} Zoom level to expand cluster
 */
export const getClusterExpansionZoom = (cluster, clusterId) => {
    if (!cluster) return 10;
    return cluster.getClusterExpansionZoom(clusterId);
};