IWAC Visualization Creator
Overview
This skill guides you through creating new visualizations for the IWAC Dashboard following the established patterns and best practices.
Workflow
Step 1: Understand Data Requirements
First, invoke the iwac-dataset skill to understand:
- •Which subset(s) you need (articles, publications, documents, audiovisual, index, references)
- •Available fields and their types
- •Common query patterns for filtering
Ask the user:
- •What data should this visualization display?
- •What filtering/interaction is needed?
- •Should it be bilingual (always yes for user-facing text)?
Step 2: Check Existing Python Scripts
Look in scripts/ for existing generators that might already produce the needed data:
ls scripts/generate_*.py
Common existing scripts:
- •
generate_overview_stats.py- Summary statistics - •
generate_index_entities.py- Entity data - •
generate_treemap.py- Country/hierarchical data - •
generate_timeline.py- Temporal data - •
generate_categories.py- Category distributions - •
generate_wordcloud.py- Word frequencies - •
generate_world_map.py- Geographic data - •
generate_cooccurrence.py- Term co-occurrence matrices - •
generate_network.py- Network graph data - •
generate_topics.py- Topic modeling results
Check static/data/ for existing JSON files that might already have what you need.
Step 3: Create/Update Python Generator
If new data generation is needed, create a script following this pattern:
"""
Generate [description] data for the IWAC Dashboard.
Output: static/data/[filename].json
"""
from datasets import load_dataset
import json
from pathlib import Path
def main():
# Load dataset using iwac-dataset skill patterns
ds = load_dataset("fmadore/islam-west-africa-collection", "articles")
df = ds['train'].to_pandas()
# Process data...
result = {
# Structure for frontend consumption
}
# Save to static/data/
output_dir = Path(__file__).parent.parent / "static" / "data"
output_dir.mkdir(parents=True, exist_ok=True)
with open(output_dir / "[filename].json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"Generated: {output_dir / '[filename].json'}")
if __name__ == "__main__":
main()
Key patterns:
- •Output to
static/data/(and optionallybuild/data/) - •Use UTF-8 encoding with
ensure_ascii=Falsefor French text - •Pre-compute aggregations - no heavy processing in frontend
- •Structure data for direct frontend consumption
Step 4: Check Existing Visualization Components
Before creating new components, check for reusable ones:
LayerChart components (src/lib/components/visualizations/charts/layerchart/):
- •
Bar.svelte- Bar charts - •
PieChart.svelte- Pie/donut charts - •
Treemap.svelte- Hierarchical treemaps - •
Duration.svelte- Duration/timeline bars - •
Tooltip.svelte- Reusable tooltip
D3 components (src/lib/components/visualizations/charts/d3/):
- •
TimelineChart.svelte- Timeline visualizations - •
StackedBarChart.svelte- Stacked bar charts - •
CooccurrenceMatrix.svelte- Matrix visualizations - •
BarChartRace.svelte- Animated bar chart race - •
WordAssociations.svelte- Word association graphs
Map components (src/lib/components/visualizations/world-map/):
- •
WorldMapVisualization.svelte- Main map container with controls - •
Map.svelte- Leaflet map wrapper - •
ChoroplethLayer.svelte- Choropleth coloring layer - •Uses
mapDataStorefor state management (viewMode, filters, selected location)
Network components (src/lib/components/visualizations/network/):
- •
NetworkGraph.svelte- Sigma.js graph renderer - •
NetworkControls.svelte- Graph interaction controls - •
NetworkNodePanel.svelte- Node detail panel - •Uses Graphology for graph data structure and ForceAtlas2 for layout
Other visualizations:
- •
Wordcloud.svelte- D3 word clouds
Step 5: Create New Visualization Component
If a new component is needed, follow these patterns:
File Location
src/lib/components/visualizations/charts/layerchart/NewChart.svelte # LayerChart-based src/lib/components/visualizations/charts/d3/NewChart.svelte # D3-based src/lib/components/visualizations/NewVisualization.svelte # Standalone
Component Template (LayerChart preferred)
<script lang="ts">
import { t, languageStore } from '$lib/stores/translationStore.svelte.js';
import { Chart, Svg, Bar, Axis, Tooltip } from 'layerchart';
import { scaleBand, scaleLinear } from 'd3-scale';
interface DataItem {
label: string;
value: number;
}
let { data = [] }: { data: DataItem[] } = $props();
// Reactive to language changes
const chartTitle = $derived(t('chart.new_chart_title'));
// Scales
const xScale = $derived(scaleBand().domain(data.map(d => d.label)).padding(0.1));
const yScale = $derived(scaleLinear().domain([0, Math.max(...data.map(d => d.value))]));
</script>
<div class="h-[400px] w-full">
<Chart {data} {xScale} {yScale} padding={{ left: 40, bottom: 40, top: 20, right: 20 }}>
<Svg>
<Axis placement="left" />
<Axis placement="bottom" />
<Bar
x="label"
y="value"
class="fill-[var(--chart-1)]"
/>
</Svg>
<Tooltip.Root let:data>
<Tooltip.Header>{data.label}</Tooltip.Header>
<Tooltip.Item label={t('chart.value')} value={data.value} />
</Tooltip.Root>
</Chart>
</div>
Component Template (D3)
<script lang="ts">
import { t, languageStore } from '$lib/stores/translationStore.svelte.js';
import * as d3 from 'd3-selection';
import { scaleLinear, scaleBand } from 'd3-scale';
let { data = [] }: { data: unknown[] } = $props();
let container: HTMLDivElement;
// Re-render on language change
const lang = $derived(languageStore.current);
$effect(() => {
if (container && data.length > 0) {
// Access lang to create dependency
const _ = lang;
renderChart();
}
});
function renderChart() {
// D3 rendering logic using CSS variables
// Use: var(--chart-1), var(--foreground), var(--muted-foreground), etc.
}
</script>
<div bind:this={container} class="h-[400px] w-full"></div>
Component Template (Leaflet Map)
For geographic visualizations, use Leaflet with the existing map components:
<script lang="ts">
import { t } from '$lib/stores/translationStore.svelte.js';
import { mapDataStore } from '$lib/stores/mapDataStore.svelte.js';
import { Map, ChoroplethLayer } from '$lib/components/visualizations/world-map/index.js';
let { locations = [] }: { locations: GeoLocation[] } = $props();
interface GeoLocation {
lat: number;
lng: number;
country: string;
value: number;
}
</script>
<div class="h-[500px] w-full">
<Map
center={[12, 0]}
zoom={4}
locations={locations}
>
<ChoroplethLayer
data={locations}
valueField="value"
colorScale="blues"
/>
</Map>
</div>
Map data store (mapDataStore.svelte.ts):
- •
viewMode: 'bubbles' | 'choropleth' - •
selectedLocation: Currently selected location - •
filters: sourceCountry, yearRange - •
filteredLocations: Derived filtered data
Component Template (Sigma.js Network)
For network/graph visualizations, use Sigma.js with Graphology:
<script lang="ts">
import { t } from '$lib/stores/translationStore.svelte.js';
import Graph from 'graphology';
import Sigma from 'sigma';
import forceAtlas2 from 'graphology-layout-forceatlas2';
let { nodes = [], edges = [] }: { nodes: NetworkNode[], edges: NetworkEdge[] } = $props();
let container: HTMLDivElement;
let sigma: Sigma | null = null;
interface NetworkNode {
id: string;
label: string;
size: number;
color?: string;
}
interface NetworkEdge {
source: string;
target: string;
weight: number;
}
$effect(() => {
if (container && nodes.length > 0) {
initGraph();
}
return () => {
sigma?.kill();
};
});
function initGraph() {
const graph = new Graph();
// Add nodes
nodes.forEach(node => {
graph.addNode(node.id, {
label: node.label,
size: node.size,
color: node.color || 'var(--chart-1)',
x: Math.random(),
y: Math.random()
});
});
// Add edges
edges.forEach(edge => {
graph.addEdge(edge.source, edge.target, { weight: edge.weight });
});
// Apply ForceAtlas2 layout
forceAtlas2.assign(graph, { iterations: 100 });
// Render with Sigma
sigma = new Sigma(graph, container, {
renderLabels: true,
labelColor: { color: 'var(--foreground)' }
});
}
</script>
<div bind:this={container} class="h-[600px] w-full"></div>
Network data format (from generate_network.py):
{
"nodes": [
{ "id": "node1", "label": "Entity Name", "size": 10, "type": "person" }
],
"edges": [
{ "source": "node1", "target": "node2", "weight": 5 }
]
}
Step 6: Styling Requirements
CRITICAL: Use CSS variables, never hardcode colors!
<!-- CORRECT --> <div class="bg-background text-foreground border-border"> <Bar class="fill-[var(--chart-1)]" /> <text fill="var(--muted-foreground)"> <!-- WRONG --> <div class="bg-white text-black border-gray-200"> <Bar class="fill-blue-500" /> <text fill="#666">
Available chart colors:
- •
--chart-1through--chart-5- Primary chart palette - •
--country-color-*- Country-specific colors (burkina-faso, benin, cote-divoire, niger, togo, nigeria)
Use shadcn-svelte for UI elements:
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
Step 7: Internationalization
All user-facing text must use the translation function:
<script>
import { t, languageStore } from '$lib/stores/translationStore.svelte.js';
// For reactive updates when language changes
const title = $derived(t('chart.my_chart_title'));
</script>
<h2>{t('chart.title')}</h2>
<p>{t('chart.description', [someValue])}</p> <!-- with parameters -->
Add new translation keys to src/lib/stores/translationStore.svelte.ts:
// In the translations object, add to both 'en' and 'fr' sections: 'chart.new_key': 'English text', 'chart.new_key': 'French text',
Step 8: Create Route Page
If the visualization needs its own page:
src/routes/[page-name]/+page.ts:
import type { PageLoad } from './$types';
import { base } from '$app/paths';
export const prerender = true;
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch(`${base}/data/[filename].json`);
if (!response.ok) {
throw new Error(`Failed to load data: ${response.status}`);
}
const data = await response.json();
return { data };
};
src/routes/[page-name]/+page.svelte:
<script lang="ts">
import { t } from '$lib/stores/translationStore.svelte.js';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
import { NewVisualization } from '$lib/components/visualizations/index.js';
let { data: pageData } = $props();
const chartData = $derived(pageData.data);
</script>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">{t('pages.new_page_title')}</h1>
<Card>
<CardHeader>
<CardTitle>{t('chart.new_chart_title')}</CardTitle>
</CardHeader>
<CardContent>
<NewVisualization data={chartData} />
</CardContent>
</Card>
</div>
Step 9: Update Barrel Exports
Add new components to the appropriate index.ts:
// src/lib/components/visualizations/charts/layerchart/index.ts
export { default as NewChart } from './NewChart.svelte';
Step 10: Add to Sidebar Navigation
Update src/lib/components/layout/AppSidebar.svelte to add navigation link.
Checklist
Before completing, verify:
- • Data: Python script generates JSON to
static/data/ - • Component: Uses Svelte 5 runes (
$props,$state,$derived,$effect) - • Styling: Uses CSS variables only (no hardcoded colors)
- • i18n: All text uses
t()function, keys added for EN and FR - • UI: Uses shadcn-svelte components where applicable
- • Imports: Uses barrel exports with
/index.jssuffix - • Route: Page has
export const prerender = true; - • Reactivity: Chart updates when language changes
- • Types: TypeScript interfaces defined for data structures
Common Patterns
Loading State
{#if loading}
<Skeleton class="h-[400px] w-full" />
{:else if error}
<div class="text-destructive">{error}</div>
{:else}
<MyChart {data} />
{/if}
Responsive Container
<div class="h-[300px] sm:h-[400px] lg:h-[500px] w-full">
<Chart ... />
</div>
Country Colors
const countryColors: Record<string, string> = {
'Burkina Faso': 'var(--country-color-burkina-faso)',
'Benin': 'var(--country-color-benin)',
'Cote d\'Ivoire': 'var(--country-color-cote-divoire)',
'Niger': 'var(--country-color-niger)',
'Togo': 'var(--country-color-togo)',
'Nigeria': 'var(--country-color-nigeria)',
};