Этот коммит содержится в:
Maarten 2024-10-14 23:52:23 +02:00
родитель ce3d8e98e7
Коммит 3ae27258ba
2 изменённых файлов: 180 добавлений и 98 удалений

Просмотреть файл

@ -15,6 +15,7 @@
export let cases; export let cases;
export let events; export let events;
export let metrics; export let metrics;
export let gdelt;
const margins = { const margins = {
top: 0, top: 0,
@ -27,61 +28,96 @@
right: 24, right: 24,
bottom: 38, bottom: 38,
left: 120 left: 120
} };
let width; let width;
let height = 200; let height = 200;
let dataToDisplay = 'gdelt';
$: xScale = scaleTime($timeRangeFilter, [0, width - margins.right - margins.left]); $: xScale = scaleTime($timeRangeFilter, [0, width - margins.right - margins.left]);
$: opacityScale = scaleLinear() $: opacityScale = scaleLinear()
.domain([0, max(cases.map(d => d.attribution_total_score))]) .domain([0, max(cases.map((d) => d.attribution_total_score))])
.range([0.2, 1]) .range([0.2, 1]);
$: ticks = xScale.ticks(5); $: ticks = xScale.ticks(5);
$: timeRangeDays = (xScale.domain()[1] - xScale.domain()[0])/86400000 $: timeRangeDays = (xScale.domain()[1] - xScale.domain()[0]) / 86400000;
$: dateFormat = timeRangeDays > 100 $: dateFormat = timeRangeDays > 100 ? utcFormat('%B') : utcFormat('%b %-d');
? utcFormat('%b')
: utcFormat('%b %-d')
const actorNations = ['Other', 'China', 'Iran', /*'North Korea', */'Russia']; const actorNations = ['Other', 'China', 'Iran', /*'North Korea', */ 'Russia'];
const colors = ['#555555', '#bf0a0a', '#0f8a0f', /*'#8a4d0f', */'#0f4c8a']; const colors = ['#555555', '#bf0a0a', '#0f8a0f', /*'#8a4d0f', */ '#0f4c8a'];
let yScale = scalePoint(actorNations, [height - margins.bottom - margins.top, 0]).padding(0.5); let yScale = scalePoint(actorNations, [height - margins.bottom - margins.top, 0]).padding(0.5);
let colorScale = scaleOrdinal(actorNations, colors); 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( 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] [6, 8, 10, 11, 12, 13]
) );
//TODO: sort bubbles //TODO: sort bubbles
$: if(cases && radiusScale){ $: if (cases && radiusScale) {
cases = cases.sort((a,b) => radiusScale(a.breakout_scale) < radiusScale(b.breakout_scale)) cases = cases.sort((a, b) => radiusScale(a.breakout_scale) < radiusScale(b.breakout_scale));
} }
$: displayCountryMetrics = $actorNationFilter.filter(d => d.selected).map(d => d.name) $: displayCountryMetrics = $actorNationFilter.filter((d) => d.selected).map((d) => d.name);
$: filteredMetrics = metrics.filter(d => displayCountryMetrics.includes(d.country))
// Metrics stacked areas
$: filteredMetrics = metrics.filter((d) => displayCountryMetrics.includes(d.country));
$: stackedMetrics = stack() $: stackedMetrics = stack()
.keys(union(filteredMetrics.map((d) => d.country))) .keys(union(filteredMetrics.map((d) => d.country)))
.value(([, D], key) => D.get(key).posts) .value(([, D], key) => D.get(key).posts)(
(index(filteredMetrics, d => d.date, d => d.country)); 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; let stackMax = 0;
$: if (stackedMetrics.length > 0) { $: if (stackedMetrics.length > 0 && dataToDisplay == 'meltwater') {
stackMax = max(stackedMetrics[stackedMetrics.length - 1].map((d) => d[1])); stackMax = max(stackedMetrics[stackedMetrics.length - 1].map((d) => d[1]));
} }
$: yScaleStack = scaleLinear([0, stackMax], [height - margins.bottom - margins.top, 0]); $: if (stackedGdelt.length > 0 && dataToDisplay == 'gdelt') {
let areaGenerator stackMax = max(stackedGdelt[stackedGdelt.length - 1].map((d) => d[1]));
$: 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)
$: 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 // Tooltip
let showTooltip = false; let showTooltip = false;
@ -104,7 +140,7 @@
y1={yScale(nation)} y1={yScale(nation)}
y2={yScale(nation)} y2={yScale(nation)}
style:stroke={colorScale(nation)} style:stroke={colorScale(nation)}
stroke-width={yScale.step()*0.9} stroke-width={yScale.step() * 0.9}
opacity={0.1} opacity={0.1}
></line> ></line>
<text <text
@ -122,7 +158,9 @@
{#if attrCase.offline_mobilization == '1'} {#if attrCase.offline_mobilization == '1'}
<circle <circle
cx={xScale(new Date(attrCase.attribution_date))} cx={xScale(new Date(attrCase.attribution_date))}
cy={actorNations.includes(attrCase.actor_nation[0]) ? yScale(attrCase.actor_nation[0]) : yScale('Other')} cy={actorNations.includes(attrCase.actor_nation[0])
? yScale(attrCase.actor_nation[0])
: yScale('Other')}
r={radiusScale(attrCase.breakout_scale) + 2} r={radiusScale(attrCase.breakout_scale) + 2}
fill={'none'} fill={'none'}
stroke={'#555555'} stroke={'#555555'}
@ -132,9 +170,13 @@
{/if} {/if}
<Bubble <Bubble
cx={xScale(new Date(attrCase.attribution_date))} cx={xScale(new Date(attrCase.attribution_date))}
cy={actorNations.includes(attrCase.actor_nation[0]) ? yScale(attrCase.actor_nation[0]) : yScale('Other')} cy={actorNations.includes(attrCase.actor_nation[0])
? yScale(attrCase.actor_nation[0])
: yScale('Other')}
r={radiusScale(attrCase.breakout_scale)} r={radiusScale(attrCase.breakout_scale)}
fill={actorNations.includes(attrCase.actor_nation[0]) ? colorScale(attrCase.actor_nation[0]) : colorScale('Other')} fill={actorNations.includes(attrCase.actor_nation[0])
? colorScale(attrCase.actor_nation[0])
: colorScale('Other')}
stroke={'#ffffff'} stroke={'#ffffff'}
strokeWidth={1.5} strokeWidth={1.5}
opacity={opacityScale(attrCase.attribution_score)} opacity={opacityScale(attrCase.attribution_score)}
@ -152,82 +194,106 @@
{/if} {/if}
</svg> </svg>
<svg {width} height={height}> <div>
{#if xScale} <div class="buttons has-addons is-right">
<g transform={`translate(${margins.left},${margins.top})`}> <button
{#if stackedMetrics.length > 0 && areaGenerator} class={dataToDisplay == 'meltwater'
{#each stackedMetrics as serie} ? 'button is-dark is-selected is-small'
<path d={areaGenerator(serie)} stroke={'white'} stroke-width={1} fill={colorScale(serie.key)}> : 'button is-small'}
</path> on:click={() => {
{/each} dataToDisplay = 'meltwater';
{/if} }}>Social media posts</button
<rect >
x={-margins.left} <button
y={-margins.top} class={dataToDisplay == 'gdelt'
width={margins.left} ? 'button is-dark is-selected is-small'
height={height - margins.top} : 'button is-small'}
fill={'#F9F8F8'} on:click={() => {
dataToDisplay = 'gdelt';
}}>Television news mentions</button
>
</div>
<svg {width} {height}>
{#if xScale}
<g transform={`translate(${margins.left},${margins.top})`}>
{#if stackedMetrics.length > 0 && areaGenerator && dataToDisplay == 'meltwater'}
{#each stackedMetrics as serie}
<path
d={areaGenerator(serie)}
stroke={'white'}
stroke-width={1}
fill={colorScale(serie.key)}
>
</path>
{/each}
{/if}
{#if stackedGdelt.length > 0 && areaGenerator && dataToDisplay == 'gdelt'}
{#each stackedGdelt as serie}
<path
d={areaGenerator(serie)}
stroke={'white'}
stroke-width={1}
fill={colorScaleGdelt(serie.key)}
>
</path>
{/each}
{/if}
<rect
x={-margins.left}
y={-margins.top}
width={margins.left}
height={height - margins.top}
fill={'#F9F8F8'}
></rect> ></rect>
{#each yScaleStackTicks as tick} {#each yScaleStackTicks as tick}
<line
x1={-10}
x2={-16}
y1={yScaleStack(tick)}
y2={yScaleStack(tick)}
stroke={'#777777'}
stroke-width={1}
></line>
<text
class={'y-tick'}
x={-18}
y={yScaleStack(tick) + 4}
text-anchor={'end'}
fill={'#777777'}>{format('~s')(tick)}</text
>
{/each}
</g>
{/if}
</svg>
</div>
<line <svg {width} height={height / 2}>
x1={-10}
x2={-16}
y1={yScaleStack(tick)}
y2={yScaleStack(tick)}
stroke={'#777777'}
stroke-width={1}
></line>
<text
class={'y-tick'}
x={-18}
y={yScaleStack(tick) + 4}
text-anchor={'end'}
fill={'#777777'}
>{format("~s")(tick)}</text>
{/each}
<text
class={'metrics-label'}
x={12}
y={20}
color={'#000000'}
>Social media posts</text>
</g>
{/if}
</svg>
<svg {width} height={height/2}>
{#if xScale} {#if xScale}
<g transform={`translate(${marginsKeyEvents.left},${marginsKeyEvents.top})`}> <g transform={`translate(${marginsKeyEvents.left},${marginsKeyEvents.top})`}>
<line <line
x1={0} x1={0}
x2={width} x2={width}
y1={32} y1={32}
y2={32} y2={32}
style:stroke={keyEventColor} style:stroke={keyEventColor}
stroke-width={32} stroke-width={32}
opacity={0.1} opacity={0.1}
></line> ></line>
<text <text class="country-label" x={-10} y={32 + 4} text-anchor={'end'} fill={keyEventColor}
class="country-label" ></text>
x={-10}
y={32 + 4}
text-anchor={'end'}
fill={keyEventColor}></text
>
{#each ticks as tick} {#each ticks as tick}
<line <line
x1={xScale(tick)} x1={xScale(tick)}
x2={xScale(tick)} x2={xScale(tick)}
y1={height/2 - marginsKeyEvents.bottom} y1={height / 2 - marginsKeyEvents.bottom}
y2={height/2 - marginsKeyEvents.bottom + 10} y2={height / 2 - marginsKeyEvents.bottom + 10}
stroke={'#777777'} stroke={'#777777'}
stroke-width={1} stroke-width={1}
></line> ></line>
<text <text
class="time-axis-tick-label" class="time-axis-tick-label"
x={xScale(tick)} x={xScale(tick)}
y={height/2 - marginsKeyEvents.bottom + 24} y={height / 2 - marginsKeyEvents.bottom + 24}
text-anchor={'middle'}>{dateFormat(tick)}</text text-anchor={'middle'}>{dateFormat(tick)}</text
> >
{/each} {/each}
@ -252,10 +318,10 @@
{/if} {/if}
</svg> </svg>
{#if showTooltip} {#if showTooltip}
<Tooltip {tooltipX} {tooltipY} {hoveredCaseData} {width} bind:showTooltip/> <Tooltip {tooltipX} {tooltipY} {hoveredCaseData} {width} bind:showTooltip />
{/if} {/if}
{#if showEventTooltip} {#if showEventTooltip}
<EventTooltip {tooltipX} {tooltipY} {hoveredEventData} {width}/> <EventTooltip {tooltipX} {tooltipY} {hoveredEventData} {width} />
{/if} {/if}
</div> </div>
@ -271,7 +337,7 @@
font-size: 0.9rem; font-size: 0.9rem;
fill: #777777; fill: #777777;
} }
.y-tick, .metrics-label { .y-tick {
font-size: 0.9rem; font-size: 0.9rem;
} }
</style> </style>

Просмотреть файл

@ -40,6 +40,7 @@
let cases = []; let cases = [];
let events = []; let events = [];
let metrics = []; let metrics = [];
let gdelt = [];
let maxAttribution = 0; let maxAttribution = 0;
onMount(async function () { onMount(async function () {
@ -113,6 +114,21 @@
return a.date - b.date; 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')) { if ($page.url.searchParams.has('filters')) {
const urlFilters = parseUrl($page.url.searchParams.get('filters')); const urlFilters = parseUrl($page.url.searchParams.get('filters'));
actorNationFilter.applyBoolArray(urlFilters.actorNations); actorNationFilter.applyBoolArray(urlFilters.actorNations);
@ -261,7 +277,7 @@
{#if isMobile} {#if isMobile}
<TimelineMobile {cases} bind:modalOpen bind:activeCaseData></TimelineMobile> <TimelineMobile {cases} bind:modalOpen bind:activeCaseData></TimelineMobile>
{:else} {:else}
<Timeline {cases} {events} {metrics}></Timeline> <Timeline {cases} {events} {metrics} {gdelt}></Timeline>
{/if} {/if}
</div> </div>
</section> </section>