GDELT chart
Этот коммит содержится в:
родитель
ce3d8e98e7
Коммит
3ae27258ba
@ -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>
|
||||||
|
|||||||
Загрузка…
x
Ссылка в новой задаче
Block a user