GDELT chart
Этот коммит содержится в:
		
							родитель
							
								
									ce3d8e98e7
								
							
						
					
					
						Коммит
						3ae27258ba
					
				| @ -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} | ||||
| 					></line> | ||||
| 					<text | ||||
| @ -122,7 +158,9 @@ | ||||
| 							{#if attrCase.offline_mobilization == '1'} | ||||
| 								<circle | ||||
| 									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} | ||||
| 									fill={'none'} | ||||
| 									stroke={'#555555'} | ||||
| @ -132,9 +170,13 @@ | ||||
| 							{/if} | ||||
| 							<Bubble | ||||
| 								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)} | ||||
| 								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'} | ||||
| 								strokeWidth={1.5} | ||||
| 								opacity={opacityScale(attrCase.attribution_score)} | ||||
| @ -152,82 +194,106 @@ | ||||
| 		{/if} | ||||
| 	</svg> | ||||
| 
 | ||||
| 	<svg {width} height={height}> | ||||
| 		{#if xScale} | ||||
| 			<g transform={`translate(${margins.left},${margins.top})`}> | ||||
| 				{#if stackedMetrics.length > 0 && areaGenerator} | ||||
| 					{#each stackedMetrics as serie} | ||||
| 						<path d={areaGenerator(serie)} stroke={'white'} stroke-width={1} fill={colorScale(serie.key)}> | ||||
| 						</path> | ||||
| 					{/each} | ||||
| 				{/if} | ||||
| 				<rect | ||||
| 					x={-margins.left} | ||||
| 					y={-margins.top} | ||||
| 					width={margins.left} | ||||
| 					height={height - margins.top} | ||||
| 					fill={'#F9F8F8'} | ||||
| 	<div> | ||||
| 		<div class="buttons has-addons is-right"> | ||||
|             <button | ||||
|                 class={dataToDisplay == 'meltwater' | ||||
|                     ? 'button is-dark is-selected is-small' | ||||
|                     : 'button is-small'} | ||||
|                 on:click={() => { | ||||
|                     dataToDisplay = 'meltwater'; | ||||
|                 }}>Social media posts</button | ||||
|             > | ||||
|             <button | ||||
|                 class={dataToDisplay == 'gdelt' | ||||
|                     ? 'button is-dark is-selected is-small' | ||||
|                     : 'button is-small'} | ||||
|                 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> | ||||
| 				{#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 | ||||
| 						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}> | ||||
| 	<svg {width} height={height / 2}> | ||||
| 		{#if xScale} | ||||
| 			<g transform={`translate(${marginsKeyEvents.left},${marginsKeyEvents.top})`}> | ||||
| 					<line | ||||
| 						x1={0} | ||||
| 						x2={width} | ||||
| 						y1={32} | ||||
| 						y2={32} | ||||
| 						style:stroke={keyEventColor} | ||||
| 						stroke-width={32} | ||||
| 						opacity={0.1} | ||||
| 					></line> | ||||
| 					<text | ||||
| 						class="country-label" | ||||
| 						x={-10} | ||||
| 						y={32 + 4} | ||||
| 						text-anchor={'end'} | ||||
| 						fill={keyEventColor}></text | ||||
| 					> | ||||
| 				<line | ||||
| 					x1={0} | ||||
| 					x2={width} | ||||
| 					y1={32} | ||||
| 					y2={32} | ||||
| 					style:stroke={keyEventColor} | ||||
| 					stroke-width={32} | ||||
| 					opacity={0.1} | ||||
| 				></line> | ||||
| 				<text class="country-label" x={-10} y={32 + 4} text-anchor={'end'} fill={keyEventColor} | ||||
| 				></text> | ||||
| 				{#each ticks as tick} | ||||
| 					<line | ||||
| 						x1={xScale(tick)} | ||||
| 						x2={xScale(tick)} | ||||
| 						y1={height/2 - marginsKeyEvents.bottom} | ||||
| 						y2={height/2 - marginsKeyEvents.bottom + 10} | ||||
| 						y1={height / 2 - marginsKeyEvents.bottom} | ||||
| 						y2={height / 2 - marginsKeyEvents.bottom + 10} | ||||
| 						stroke={'#777777'} | ||||
| 						stroke-width={1} | ||||
| 					></line> | ||||
| 					<text | ||||
| 						class="time-axis-tick-label" | ||||
| 						x={xScale(tick)} | ||||
| 						y={height/2 - marginsKeyEvents.bottom + 24} | ||||
| 						y={height / 2 - marginsKeyEvents.bottom + 24} | ||||
| 						text-anchor={'middle'}>{dateFormat(tick)}</text | ||||
| 					> | ||||
| 				{/each} | ||||
| @ -252,10 +318,10 @@ | ||||
| 		{/if} | ||||
| 	</svg> | ||||
| 	{#if showTooltip} | ||||
| 		<Tooltip {tooltipX} {tooltipY} {hoveredCaseData} {width} bind:showTooltip/> | ||||
| 		<Tooltip {tooltipX} {tooltipY} {hoveredCaseData} {width} bind:showTooltip /> | ||||
| 	{/if} | ||||
| 	{#if showEventTooltip} | ||||
| 		<EventTooltip {tooltipX} {tooltipY} {hoveredEventData} {width}/> | ||||
| 		<EventTooltip {tooltipX} {tooltipY} {hoveredEventData} {width} /> | ||||
| 	{/if} | ||||
| </div> | ||||
| 
 | ||||
| @ -271,7 +337,7 @@ | ||||
| 		font-size: 0.9rem; | ||||
| 		fill: #777777; | ||||
| 	} | ||||
| 	.y-tick, .metrics-label { | ||||
| 	.y-tick { | ||||
| 		font-size: 0.9rem; | ||||
| 	} | ||||
| </style> | ||||
|  | ||||
| @ -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} | ||||
| 			<TimelineMobile {cases} bind:modalOpen bind:activeCaseData></TimelineMobile> | ||||
| 		{:else} | ||||
| 			<Timeline {cases} {events} {metrics}></Timeline> | ||||
| 			<Timeline {cases} {events} {metrics} {gdelt}></Timeline> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </section> | ||||
|  | ||||
		Загрузка…
	
	
			
			x
			
			
		
	
		Ссылка в новой задаче
	
	Block a user
	 Maarten
						Maarten