diff --git a/src/lib/components/Timeline.svelte b/src/lib/components/Timeline.svelte index e905f34..10e3487 100644 --- a/src/lib/components/Timeline.svelte +++ b/src/lib/components/Timeline.svelte @@ -15,6 +15,7 @@ export let cases; export let events; export let metrics; + export let gdelt; const margins = { top: 0, @@ -27,61 +28,96 @@ right: 24, bottom: 38, left: 120 - } + }; let width; let height = 200; + let dataToDisplay = 'gdelt'; + $: xScale = scaleTime($timeRangeFilter, [0, width - margins.right - margins.left]); $: opacityScale = scaleLinear() - .domain([0, max(cases.map(d => d.attribution_total_score))]) - .range([0.2, 1]) + .domain([0, max(cases.map((d) => d.attribution_total_score))]) + .range([0.2, 1]); $: ticks = xScale.ticks(5); - $: timeRangeDays = (xScale.domain()[1] - xScale.domain()[0])/86400000 - $: dateFormat = timeRangeDays > 100 - ? utcFormat('%b') - : utcFormat('%b %-d') + $: timeRangeDays = (xScale.domain()[1] - xScale.domain()[0]) / 86400000; + $: dateFormat = timeRangeDays > 100 ? utcFormat('%B') : utcFormat('%b %-d'); - const actorNations = ['Other', 'China', 'Iran', /*'North Korea', */'Russia']; - const colors = ['#555555', '#bf0a0a', '#0f8a0f', /*'#8a4d0f', */'#0f4c8a']; + const actorNations = ['Other', 'China', 'Iran', /*'North Korea', */ 'Russia']; + const colors = ['#555555', '#bf0a0a', '#0f8a0f', /*'#8a4d0f', */ '#0f4c8a']; let yScale = scalePoint(actorNations, [height - margins.bottom - margins.top, 0]).padding(0.5); let colorScale = scaleOrdinal(actorNations, colors); - const keyEventColor = "#555555" + const keyEventColor = '#555555'; + + const actorNationsGdelt = ['China', 'Iran', 'North Korea', 'Russia', 'Israel']; + const colorsGdelt = ['#bf0a0a', '#0f8a0f', '#8a4d0f', '#0f4c8a', '#8a0f38']; + let colorScaleGdelt = scaleOrdinal(actorNationsGdelt, colorsGdelt); let radiusScale = scaleOrdinal( - ['Category One', 'Category Two', 'Category Three', 'Category Four', 'Category Five', 'Category Six'], + [ + 'Category One', + 'Category Two', + 'Category Three', + 'Category Four', + 'Category Five', + 'Category Six' + ], [6, 8, 10, 11, 12, 13] - ) + ); //TODO: sort bubbles - $: if(cases && radiusScale){ - cases = cases.sort((a,b) => radiusScale(a.breakout_scale) < radiusScale(b.breakout_scale)) + $: if (cases && radiusScale) { + cases = cases.sort((a, b) => radiusScale(a.breakout_scale) < radiusScale(b.breakout_scale)); } - $: displayCountryMetrics = $actorNationFilter.filter(d => d.selected).map(d => d.name) - $: filteredMetrics = metrics.filter(d => displayCountryMetrics.includes(d.country)) + $: displayCountryMetrics = $actorNationFilter.filter((d) => d.selected).map((d) => d.name); + + // Metrics stacked areas + $: filteredMetrics = metrics.filter((d) => displayCountryMetrics.includes(d.country)); $: stackedMetrics = stack() .keys(union(filteredMetrics.map((d) => d.country))) - .value(([, D], key) => D.get(key).posts) - (index(filteredMetrics, d => d.date, d => d.country)); + .value(([, D], key) => D.get(key).posts)( + index( + filteredMetrics, + (d) => d.date, + (d) => d.country + ) + ); + + //GDELT stacked areas + $: filteredGdelt = gdelt.filter((d) => displayCountryMetrics.includes(d.country)); + + $: stackedGdelt = stack() + .keys(union(filteredGdelt.map((d) => d.country))) + .value(([, D], key) => D.get(key).value)( + index( + filteredGdelt, + (d) => d.date, + (d) => d.country + ) + ); let stackMax = 0; - $: if (stackedMetrics.length > 0) { + $: if (stackedMetrics.length > 0 && dataToDisplay == 'meltwater') { stackMax = max(stackedMetrics[stackedMetrics.length - 1].map((d) => d[1])); } - $: yScaleStack = scaleLinear([0, stackMax], [height - margins.bottom - margins.top, 0]); - let areaGenerator - $: if(xScale && yScaleStack) { - areaGenerator = area() - .x((d) => xScale(d.data[0])) - .y0((d) => yScaleStack(d[0])) - .y1((d) => yScaleStack(d[1])) - .curve(curveNatural) + $: if (stackedGdelt.length > 0 && dataToDisplay == 'gdelt') { + stackMax = max(stackedGdelt[stackedGdelt.length - 1].map((d) => d[1])); } - $: yScaleStackTicks = yScaleStack.ticks(2).filter(d => d != 0) + + $: yScaleStack = scaleLinear([0, stackMax], [height - margins.bottom - margins.top, 0]); + let areaGenerator; + $: if (xScale && yScaleStack) { + areaGenerator = area() + .x((d) => xScale(d.data[0])) + .y0((d) => yScaleStack(d[0])) + .y1((d) => yScaleStack(d[1])) + .curve(curveNatural); + } + $: yScaleStackTicks = yScaleStack.ticks(2).filter((d) => d != 0); // Tooltip let showTooltip = false; @@ -104,7 +140,7 @@ y1={yScale(nation)} y2={yScale(nation)} style:stroke={colorScale(nation)} - stroke-width={yScale.step()*0.9} + stroke-width={yScale.step() * 0.9} opacity={0.1} > - - {#if xScale} - - {#if stackedMetrics.length > 0 && areaGenerator} - {#each stackedMetrics as serie} - - - {/each} - {/if} - +
+ + +
+ + {#if xScale} + + {#if stackedMetrics.length > 0 && areaGenerator && dataToDisplay == 'meltwater'} + {#each stackedMetrics as serie} + + + {/each} + {/if} + {#if stackedGdelt.length > 0 && areaGenerator && dataToDisplay == 'gdelt'} + {#each stackedGdelt as serie} + + + {/each} + {/if} + - {#each yScaleStackTicks as tick} - - - {format("~s")(tick)} - {/each} - Social media posts - - {/if} - + {#each yScaleStackTicks as tick} + + {format('~s')(tick)} + {/each} + + {/if} + + - + {#if xScale} - - + + {#each ticks as tick} {dateFormat(tick)} {/each} @@ -252,10 +318,10 @@ {/if} {#if showTooltip} - + {/if} {#if showEventTooltip} - + {/if} @@ -271,7 +337,7 @@ font-size: 0.9rem; fill: #777777; } - .y-tick, .metrics-label { + .y-tick { font-size: 0.9rem; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2c80c4b..53ac47b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -40,6 +40,7 @@ let cases = []; let events = []; let metrics = []; + let gdelt = []; let maxAttribution = 0; onMount(async function () { @@ -113,6 +114,21 @@ return a.date - b.date; }); + const gdeltResponse = await csv( + 'https://fiat-2024-processed-data.s3.us-west-2.amazonaws.com/gdelt_volume_timeline.csv' + ); + + gdelt = gdeltResponse.map(d => { + let obj = {}; + obj.date = new Date(d.Date); + obj.value = +d.Value; + obj.country = d.Country + return obj + }).filter(d => !['North Korea', 'Israel'].includes(d.country)) + gdelt.sort((a, b) => { + return a.date - b.date; + }); + if ($page.url.searchParams.has('filters')) { const urlFilters = parseUrl($page.url.searchParams.get('filters')); actorNationFilter.applyBoolArray(urlFilters.actorNations); @@ -261,7 +277,7 @@ {#if isMobile} {:else} - + {/if}