Merge pull request #1 from DFRLab/dev

Integration of new features
Этот коммит содержится в:
Matthias Stahl 2020-12-18 23:29:50 +01:00 коммит произвёл GitHub
родитель 81926cb28d 1043b71653
Коммит 129f7e5273
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
32 изменённых файлов: 822 добавлений и 114 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -141,6 +141,36 @@ a:hover, span.pseudolink:hover {
background-color: #e24545a1;
}
/* polarization */
.pol-l {
background-color: #2e64a0;
opacity: 0.8;
}
.pol-ll {
background-color: #61a3de;
opacity: 0.8;
}
.pol-c {
background-color: #96659e;
opacity: 0.8;
}
.pol-lr {
background-color: #a15552;
opacity: 0.8;
}
.pol-r {
background-color: #ca0800;
opacity: 0.8;
}
.pol-undef {
background-color: #e0d3e2;
opacity: 0.8;
}
/* the landing page */
.page-wrapper {

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

@ -118,6 +118,9 @@
<p>
The fixed timeframe, standardized Google dork formula, and opacity of the tools themselves means that relevant social media engagement data may be excluded. Therefore, the Attribution Impact should be treated as a rough estimate, most useful for comparing between cases.
</p>
<p>
The political leanings of 600+ media entities – using political data from the <a href="https://www.allsides.com/media-bias/media-bias-ratings" target="_blank">AllSides Media Bias Ratings</a> – was applied to the web links found in Attribution Impact. Since not all web links had matching AllSides data, the polarization data filter shows only cases where there were ten or more AllSides-matching articles and/or cases where 25 percent or more of articles matched with AllSides data. The stem lines on the polarization data filter are colored based on the mean political polarization of that case.
</p>
<p>
It is the intention of the FIAT team to introduce additional dimensions of Attribution Impact as the tool evolves.
</p>
@ -210,6 +213,7 @@
<li><strong>Short title</strong> (free text).</li>
<li><strong>Short description</strong> (free text). One to three sentence description of the allegation, alleged activity, and attribution.</li>
<li><strong>Link to attribution</strong> (link).</li>
<li>If the attribution is to Facebook and was removed by Facebook for coordinated inauthentic behavior, additional data is available. These variables are an updated version of Sima Basel and Matt Suiches <a href="https://si.ma/fb-cib/" target="_blank">OSINT analysis</a> of Facebook removals for coordinated inauthentic behavior. See Basels data <a href="https://github.com/simabasel/cib-data" target="_blank">compilation</a> for variables and definitions.</li>
</ul>
<h5 id="activity-attribution-date">When did the interference and attribution occur?</h5>
<ul>
@ -284,6 +288,7 @@
<li><strong>Twitter engagement</strong> (quantitative). Aggregate shares of a web link.</li>
<li><strong>Reddit engagement</strong> (quantitative). Aggregate shares and interactions of a web link.</li>
<li><strong>Total engagement</strong> (quantitative). Aggregate Facebook, Twitter, and Reddit engagement.</li>
<li><strong>Polarization</strong> (quantitative). Measures the political leaning of case-related articles using polarization data from the AllSides Media Bias Ratings. AllSides is a media technology company which provides multiple perspectives on news outlets. Articles from outlets with AllSides data were divided into five political categories: left, lean left, center, lean right, and right.</li>
</ul>
</li>
</ul>

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

@ -1,6 +1,6 @@
import CopyTooltip from '../components/CopyTooltip.svelte';
export function copytooltipable(node, content) {
export function copytooltipable(node, { content, showClickMessage = true }) {
let component;
function handleMouseleave(e) {
@ -19,7 +19,8 @@ export function copytooltipable(node, content) {
title: node.innerHTML,
content,
x,
y
y,
showClickMessage
},
intro: true
});

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

@ -22,7 +22,7 @@
}
input[type="checkbox"] {
display:none;
display: none;
pointer-events: all;
}
@ -30,6 +30,7 @@
display: block;
width: 15px;
height: 15px;
margin-top: 3px;
margin-right: 0.4rem;
border: 2px solid var(--usa-blue);
border-radius: 3px;

85
src/components/CheckboxPanel.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,85 @@
<script>
import {
highlightPolarization,
highlightCib,
polarizationFilter,
polarizationDef } from '../stores/filters';
import { polarizationScale } from '../stores/scales';
import { copytooltipable } from '../actions/copytooltipable';
import Checkbox from './Checkbox.svelte';
import Slider from './Slider.svelte';
function handleClick(type) {
switch (type) {
case 'polarization':
$highlightPolarization = !$highlightPolarization;
break;
case 'cib':
$highlightCib = !$highlightCib;
break;
}
}
</script>
<ul class="checkboxpanel-wrapper">
<li>
<Checkbox id="checkboxpanel-checkbox-polarization"
checked={$highlightPolarization}
on:click={() => handleClick('polarization')}>
<span use:copytooltipable={{content: 'The political leanings of 600+ media entities – using political data from the AllSides Media Bias Ratings – was applied to the web links found in Attribution Impact. Since not all web links had matching AllSides data, the polarization data filter shows only cases where there were ten or more AllSides-matching articles and/or cases where 25 percent or more of articles matched with AllSides data. The stem lines on the polarization data filter are colored based on the mean political polarization of that case.', showClickMessage: false}}>
Partisan Leaning Data Filter
</span>
</Checkbox>
</li>
<li class="polarization-slider" class:hide={!$highlightPolarization}>
<Slider value={$polarizationFilter}
lockInMode={false}
showLabel={false}
min={polarizationDef[0]}
max={polarizationDef[1]}
showHandleLabels={false}
barOpacity={0.7}
startColor={$polarizationScale(polarizationDef[0])}
stopColor={$polarizationScale(polarizationDef[1])}
showBorder={false}
on:changed={(e) => $polarizationFilter = e.detail} />
</li>
<!-- <li>
<Checkbox id="checkboxpanel-checkbox-cib"
checked={$highlightCib}
on:click={() => handleClick('cib')}>
<span use:copytooltipable={{content: 'Some content', showClickMessage: false}}>CIB data filter</span>
</Checkbox>
</li> -->
</ul>
<style>
ul {
display: flex;
align-items: center;
width: 100%;
margin: 0.1rem 0 0.1rem -0.2rem;
list-style-type: none;
}
li {
position: relative;
}
li.polarization-slider {
margin-bottom: 0.05rem;
}
span {
display: inline-block;
margin: 2% 0 0 0.5rem;
font-family: var(--font-02);
font-size: 0.8rem;
color: var(--usa-blue);
}
.hide {
visibility: hidden;
}
</style>

95
src/components/CibTable.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,95 @@
<script>
import {
cibTableFields,
cibColumnHeaders } from '../inputs/cib';
import { format } from 'd3';
import Icon from 'svelte-awesome';
import { facebook, instagram } from 'svelte-awesome/icons';
export let data;
const commaFormat = format(',');
const iconData = {
facebook,
instagram
};
</script>
<div class="cib-table-wrapper">
<table>
<thead>
<tr>
{#each cibColumnHeaders as { id, name } (id)}
<td>{name}</td>
{/each}
</tr>
</thead>
<tbody>
{#each cibTableFields as field (field.id)}
<tr>
{#each cibColumnHeaders as { id, type } (id)}
<td>
{#if (type === 'platformName')}
<Icon data={iconData[field[type].toLowerCase()]}
scale="0.9"
label={field[type]} />
{:else if (type === 'fieldName')}
<span>{field[type]}</span>
{:else if (data[field[type]] !== undefined)}
<span class="right-align">{commaFormat(data[field[type]])}</span>
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<div class="budget-and-events">
{#if (data.budgetTotalUsd && data.budgetTotalUsd > 0)}
<p>Facebook advertising expenditures: <strong>$ {commaFormat(data.budgetTotalUsd)}</strong>.</p>
{/if}
</div>
</div>
<style>
.cib-table-wrapper {
display: flex;
width: 100%;
}
table {
flex: 1;
font-size: 0.8rem;
color: var(--text-black);
}
thead {
font-size: 0.7rem;
}
td {
min-width: 30px;
padding: 0 0.2rem;
vertical-align: middle;
}
td span {
display: inline-block;
width: 100%;
margin-bottom: 0.2rem;
}
td span.right-align {
text-align: right;
}
.budget-and-events {
padding: 1.1rem 0 0 1rem;
}
.budget-and-events p {
font-size: 0.7rem;
}
</style>

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

@ -11,12 +11,15 @@
textSearchFilter,
selectAllFilters,
contextData,
originalTimeDomain } from '../stores/filters';
originalTimeDomain,
highlightPolarization,
highlightCib } from '../stores/filters';
import { timeScale, attributionScoreScale } from '../stores/scales';
import Dropdown from './Dropdown.svelte';
import Slider from './Slider.svelte';
import SearchText from './SearchText.svelte';
import CheckboxPanel from './CheckboxPanel.svelte';
import Share from './Share.svelte';
export let timePoints;
@ -32,6 +35,8 @@
function handleButtonClick() {
selectAllFilters();
contextData.unselectAll();
$highlightPolarization = false;
$highlightCib = false;
if ($originalTimeDomain) {
$timeScale.domain($originalTimeDomain);
$timeScale = $timeScale;
@ -89,13 +94,17 @@
Reset
</button>
</div>
<Share />
<div class="checkbox-panel">
<CheckboxPanel />
<Share />
</div>
<!-- <Share /> -->
</div>
{/if}
<style>
.controls-inner-wrapper {
padding: 0.2rem;
padding: 0 0.2rem;
border: none;
border-radius: 3px;
background-color: var(--transparentbg);
@ -156,4 +165,9 @@
background-color: var(--usa-blue);
cursor: pointer;
}
.checkbox-panel {
display: flex;
align-items: center;
}
</style>

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

@ -6,6 +6,7 @@
export let content = '';
export let x = 0;
export let y = 0;
export let showClickMessage = true;
const maxWidth = 300;
const margin = 10;
@ -33,7 +34,9 @@
<div class="content">
<h2>{title}</h2>
<p>{content}</p>
<p class="footer">Click to read more.</p>
{#if (showClickMessage)}
<p class="footer">Click to read more.</p>
{/if}
</div>
</div>

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

@ -4,7 +4,7 @@
import { width, panelHeight, controlsHeight } from '../stores/dimensions';
import { tooltip } from '../stores/eventSelections';
import { fade, slide } from 'svelte/transition';
import { timeFormat, format } from 'd3';
import { timeFormat } from 'd3';
import { extractHostname } from '../utils/misc';
import {
platformFilter,
@ -13,6 +13,7 @@
sourceCategoryFilter,
tagFilter,
textSearchFilter,
highlightPolarization,
selectAllFilters} from '../stores/filters';
import { maxScores } from '../inputs/scores';
import { images } from '../inputs/dataPaths';
@ -20,6 +21,9 @@
import EventTooltipCross from './EventTooltipCross.svelte';
import ScoreBar from './ScoreBar.svelte';
import ScoreQuestions from './ScoreQuestions.svelte';
import ImpactStrip from './ImpactStrip.svelte';
import PolarizationLegend from './PolarizationLegend.svelte';
import CibTable from './CibTable.svelte';
import Share from './Share.svelte';
const offset = {
@ -32,7 +36,6 @@
const attributionTf = timeFormat('%B %d, %Y');
const activityTf = timeFormat('%B %Y');
const commaFormat = format(',');
let elem;
let tWidth, tHeight;
@ -176,24 +179,24 @@
{/if}
</div>
<div class="smi">
<h3>Attribution impact</h3>
<h3>Attribution Impact</h3>
{#if ($tooltip.tp.smiPending)}
<p>pending</p>
{:else}
<ul>
<li>
<span class="smi-score facebook">{commaFormat($tooltip.tp.smiFacebook)}</span>
<span class="smi-label">Facebook</span>
</li>
<li>
<span class="smi-score twitter">{commaFormat($tooltip.tp.smiTwitter)}</span>
<span class="smi-label">Twitter</span>
</li>
<li>
<span class="smi-score reddit">{commaFormat($tooltip.tp.smiReddit)}</span>
<span class="smi-label">Reddit</span>
</li>
<ImpactStrip value={$tooltip.tp.smiFacebook}
polarization={$highlightPolarization ? $tooltip.tp.polarization : null}
label="Facebook" />
<ImpactStrip value={$tooltip.tp.smiTwitter}
polarization={$highlightPolarization ? $tooltip.tp.polarization : null}
label="Twitter" />
<ImpactStrip value={$tooltip.tp.smiReddit}
polarization={$highlightPolarization ? $tooltip.tp.polarization : null}
label="Reddit" />
</ul>
{#if ($highlightPolarization && ($tooltip.tp.polarization.fulfills10Articles || $tooltip.tp.polarization.fulfills25Percent))}
<PolarizationLegend />
{/if}
{/if}
</div>
{#if ($tooltip.tp.imageUrl)}
@ -206,6 +209,12 @@
<h3>Description</h3>
<p>{@html highlight($tooltip.tp.shortDescription)}</p>
</div>
{#if ($tooltip.tp.cib.hasCib)}
<div class="cib">
<h3>Removed Content</h3>
<CibTable data={$tooltip.tp.cib} />
</div>
{/if}
{#if (!($tooltip.tp.tags.length === 1 && $tooltip.tp.tags[0] === 'unspecified'))}
<div class="tags">
<h3>Tags</h3>
@ -251,7 +260,7 @@
</ul>
</div>
<div class="source-category">
<h3>Source categor{$tooltip.tp.sourceCategory.length !== 1 ? 'ies' : 'y'}</h3>
<h3>Source Categor{$tooltip.tp.sourceCategory.length !== 1 ? 'ies' : 'y'}</h3>
<ul>
{#each $tooltip.tp.sourceCategory as cat (cat)}
<li class="card" on:click|self={() => handleLiClick('sourceCategory', cat)}>{@html highlight(cat)}</li>
@ -286,6 +295,7 @@
.content {
display: flex;
flex-direction: column;
max-width: 550px;
max-height: 60vh;
color: var(--text-black);
background-color: var(--bg);
@ -426,26 +436,6 @@
display: flex;
}
.smi li {
margin: 0.2rem 0.3rem 0.2rem 0;
font-size: 0.8rem;
}
.smi-score {
padding: 0 0.2rem;
border: none;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0,0,0,0.07),
0 2px 4px rgba(0,0,0,0.07);
}
.smi-label {
display: inline-block;
padding: 0 0.1rem;
border: none;
border-radius: 3px;
}
a {
text-decoration: none;
}
@ -458,11 +448,6 @@
font-size: 0.6rem;
}
.no-break {
word-break: keep-all;
white-space: nowrap;
}
.scroll-wrapper .image {
min-height: 1%;
width: 100%;

52
src/components/ImpactStrip.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,52 @@
<script>
import { format } from 'd3';
import PolarizationStrip from './PolarizationStrip.svelte';
export let value = 0;
export let polarization;
export let label = '';
const commaFormat = format(',');
let valueWidth = 0;
</script>
<li>
<div class="smi-score {label.toLowerCase()}"
bind:clientWidth={valueWidth}>
{commaFormat(value)}
</div>
<span class="smi-label">
{label}
</span>
{#if (polarization && (polarization.fulfills10Articles || polarization.fulfills25Percent) && value > 0)}
<PolarizationStrip polarization={polarization[label.toLowerCase()]}
smi={value}
valueWidth={valueWidth} />
{/if}
</li>
<style>
li {
margin: 0.2rem 0.3rem 0.2rem 0;
font-size: 0.8rem;
min-width: 30%;
}
.smi-score {
display: inline-block;
padding: 0 0.2rem;
border: none;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07),
0 2px 4px rgba(0, 0, 0, 0.07);
}
.smi-label {
display: inline-block;
padding: 0 0.1rem;
border: none;
border-radius: 3px;
}
</style>

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

@ -17,7 +17,7 @@
dy="4">
Attribution Impact
</text>
<g class="total-r-scale" transform="translate(0 {-4.5 * $smiTotalRScale(rTicks.slice(-1)[0])})">
<g class="total-r-scale" transform="translate(0 {-2 * $smiTotalRScale(rTicks.slice(-1)[0])})">
{#each rTicks as tick, i}
<line x1="0"
y1={$smiTotalRScale(rTicks[0]) - 2 * $smiTotalRScale(tick)}
@ -32,7 +32,7 @@
r={$smiTotalRScale(tick)}></circle>
{/each}
</g>
<g class="smi-pending">
<!-- <g class="smi-pending">
<line x1="0"
y1={-$smiTotalRScale(rTicks.slice(-1)[0])}
x2={$smiTotalRScale(rTicks[0]) + 15}
@ -44,7 +44,7 @@
<circle cx="0"
cy="0"
r={$smiTotalRScale(rTicks.slice(-1)[0])}></circle>
</g>
</g> -->
</g>
<style>

52
src/components/PolarizationLegend.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,52 @@
<script>
import { categories } from '../inputs/polarization';
</script>
<div class="pol-legend">
<p>Polarization:</p>
<ul>
{#each categories as category (category)}
<li>
<div class="pol-legend-field pol-{category.id}"></div>
<p>{category.name}</p>
</li>
{/each}
</ul>
</div>
<style>
.pol-legend {
display: flex;
align-items: center;
font-family: var(--font-02);
color: var(--dfrlab-gray);
}
.pol-legend p {
margin-right: 0.7rem;
font-size: 0.7rem;
}
.pol-legend ul {
display: flex;
align-items: center;
height: 100%;
list-style-type: none;
}
.pol-legend ul li {
display: flex;
align-items: center;
height: 100%;
}
.pol-legend ul li p {
font-size: 0.6rem;
}
.pol-legend-field {
width: 0.5rem;
height: 0.5rem;
margin-right: 0.2rem;
}
</style>

110
src/components/PolarizationStrip.svelte Обычный файл
Просмотреть файл

@ -0,0 +1,110 @@
<script>
import { categories } from '../inputs/polarization';
import { scaleLinear, line as d3line, curveBasis } from 'd3';
export let polarization;
export let smi = 0;
export let valueWidth = 0;
const magnifierHeight = 20;
const margin = {
top: 4,
right: 2,
bottom: 0,
left: 2
};
const yScale = scaleLinear()
.domain([0, 1])
.range([margin.top, magnifierHeight - margin.bottom]);
const line = d3line()
.x((d) => d[0])
.y((d) => yScale(d[1]))
.curve(curveBasis);
let width;
let stack = {};
$: totalEngagement = Object.keys(polarization).map((k) => polarization[k]).reduce((acc, cur) => acc + cur);
$: engagementExplained = totalEngagement / smi;
$: Object.keys(polarization).forEach((k) => {
const value = polarization[k];
stack[k] = {
value,
width: Math.floor(100 * value / totalEngagement, 2)
};
});
$: leftPathData = [
[margin.left + valueWidth / 2 - engagementExplained * valueWidth / 2, 0],
[margin.left + valueWidth / 2 - engagementExplained * valueWidth / 2, 0.3],
[margin.left, 0.7],
[margin.left, 1]
];
$: rightPathData = [
[valueWidth / 2 + engagementExplained * valueWidth / 2 - margin.right, 0],
[valueWidth / 2 + engagementExplained * valueWidth / 2 - margin.right, 0.3],
[width * 0.98 - margin.right, 0.7],
[width * 0.98 - margin.right, 1]
];
</script>
<div class="polarization-strip"
bind:clientWidth={width}>
{#if (width > 0)}
<svg class="pol-magnifier"
width={width}
height={magnifierHeight}>
<path d={line(leftPathData)} />
<path d={line(rightPathData)} />
<text x={Math.max(15, valueWidth / 2)}
y={yScale(1)}>
{Math.round(engagementExplained * 100)}%
</text>
</svg>
{/if}
<div class="pol-layer-wrapper">
{#each categories as category (category.id)}
<div class="pol-layer pol-{category.id}"
style="width: {stack[category.id].width}%;">
</div>
{/each}
</div>
</div>
<style>
.polarization-strip {
width: 100%;
}
path {
stroke: var(--dfrlab-gray);
stroke-width: 1.5;
stroke-dasharray: 2 3;
stroke-linecap: round;
fill: none;
}
text {
font-family: var(--font-02);
font-size: 0.6rem;
text-anchor: middle;
fill: var(--dfrlab-gray);
}
.pol-layer-wrapper {
width: 100%;
height: 1rem;
border: none;
}
.pol-layer {
display: inline-block;
height: 100%;
}
</style>

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

@ -7,10 +7,12 @@
sourceCategoryFilter,
tagFilter,
attributionScoreFilter,
attributionScoreDef,
polarizationFilter,
textSearchFilter,
originalTimeDomain,
contextData } from '../stores/filters';
contextData,
highlightPolarization,
highlightCib } from '../stores/filters';
import { urlFromFilters } from '../utils/share';
import Icon from 'svelte-awesome';
@ -35,9 +37,12 @@
$sourceCategoryFilter,
$tagFilter,
$attributionScoreFilter,
$polarizationFilter,
$textSearchFilter,
$contextData,
caseId);
caseId,
$highlightPolarization,
$highlightCib);
</script>
<div class="share">
@ -61,6 +66,7 @@
display: flex;
align-items: center;
justify-content: center;
margin: 0 0.5rem 0 0;
font-family: var(--font-02);
font-size: 0.7rem;
pointer-events: all;
@ -68,6 +74,7 @@
p {
color: var(--usa-blue);
white-space: nowrap;
}
a {

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

@ -4,13 +4,17 @@
import { scaleLinear } from 'd3';
import { slidable } from '../actions/slidable';
export let lockInMode = true;
export let label = '';
export let showLabel = true;
export let min = 0;
export let max = 10;
export let value = [0, 10];
export let showHandleLabels = true;
export let startColor = 'white';
export let stopColor = 'red';
export let barOpacity = 1;
export let showBorder = true;
const dispatch = createEventDispatcher();
const handleWidth = 17;
@ -35,8 +39,12 @@
}
function handleSlideEnd(e, side) {
dispatch('changed', [Math.round(scale.invert(pos.left), 0),
if (lockInMode) {
dispatch('changed', [Math.round(scale.invert(pos.left), 0),
Math.round(scale.invert(pos.right), 0)]);
} else {
dispatch('changed', [scale.invert(pos.left), scale.invert(pos.right)]);
}
}
$: scale = scaleLinear()
@ -50,17 +58,20 @@
<div class="slider"
bind:clientWidth={sliderWidth}
style="--handle-width: {handleWidth}px;">
<div class="label">
{label}
</div>
<div class="slider-body">
{#if (showLabel)}
<div class="label">
{label}
</div>
{/if}
<div class="slider-body" class:border={showBorder}>
<div class="slider-selected-range"
style="width: {sliderWidth - 3 * handleWidth}px;
margin-left: {1.5 * handleWidth}px;
style="width: {sliderWidth - 2 * handleWidth}px;
margin-left: {1 * handleWidth}px;
opacity: {barOpacity};
background: linear-gradient(90deg, {startColor}, {stopColor});"></div>
<div class="slider-handle"
class:no-label={!showHandleLabels}
style="left: {(value[0] === value[1]) ? pos.left - 5 : pos.left}px;"
style="left: {(Math.abs(value[0] - value[1]) < 0.1) ? pos.left - 5 : pos.left}px;"
use:slidable
on:slide={(e) => handleSlide(e, 'left')}
on:slideend={(e) => handleSlideEnd(e, 'left')}>
@ -68,7 +79,7 @@
</div>
<div class="slider-handle"
class:no-label={!showHandleLabels}
style="left: {(value[0] === value[1]) ? pos.right + 5 : pos.right}px;"
style="left: {(Math.abs(value[0] - value[1]) < 0.1) ? pos.right + 5 : pos.right}px;"
use:slidable
on:slide={(e) => handleSlide(e, 'right')}
on:slideend={(e) => handleSlideEnd(e, 'right')}>
@ -103,9 +114,13 @@
padding: 0.1rem 0;
font-size: 0.7rem;
background-color: var(--bg);
border: none;
position: relative;
}
.border {
border: 2px solid var(--usa-blue);
border-radius: 3px;
position: relative;
}
.slider-selected-range {

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

@ -6,11 +6,13 @@
import { growDuration, bloomDuration, jitterFactor } from '../transitions/constants';
import { curvyDoubleLine } from '../utils/paths';
import { createTweenedPos } from '../transitions/tween';
import { usaBlue } from '../utils/colors';
export let source;
export let selected = 'unselected';
export let hovered = 'unselected';
export let extraFaint = false;
export let showPolarizationColor = false;
const tweenedPos = createTweenedPos();
@ -45,6 +47,7 @@
$tweenedPos.fy + source.rSmiTot - 5,
source.shift,
$mapHeight / 15)}
stroke={showPolarizationColor ? source.polarizationColor : usaBlue}
stroke-width={$minDim / 200}
in:draw|local={{duration: growDuration, delay: source.id * jitterFactor, easing: linear}}
out:draw|local={{duration: growDuration, delay: bloomDuration + source.id * jitterFactor, easing: linear}}></path>
@ -53,7 +56,6 @@
<style>
path {
stroke: var(--usa-blue);
fill: none;
}

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

@ -6,7 +6,11 @@
import { sortConsistently } from '../utils/misc';
import { hovered as eHovered, selected as eSelected } from '../stores/eventSelections';
import { hovered as cHovered } from '../stores/centroidSelections';
import { disinformantNationFilter, selectAllFilters, unselectAllFilters } from '../stores/filters';
import {
disinformantNationFilter,
selectAllFilters,
unselectAllFilters,
highlightPolarization } from '../stores/filters';
import SourceLink from './SourceLink.svelte';
import Centroid from './Centroid.svelte';
@ -105,7 +109,8 @@
: ($eHovered
? 'background'
: 'unselected')}
extraFaint={source.outOfTimeRange} />
extraFaint={source.outOfTimeRange}
showPolarizationColor={$highlightPolarization} />
{/each}
{#each centroids as [country, centroid]}
<Centroid {centroid}

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

@ -5,7 +5,9 @@
contextData,
sourceFilter,
attributionScoreFilter,
selectAllFilters } from '../stores/filters';
selectAllFilters,
highlightPolarization,
highlightCib } from '../stores/filters';
import { format, timeFormat } from 'd3';
import { drawWrapper } from '../stores/elements';
import { copytooltipable } from '../actions/copytooltipable';
@ -19,6 +21,8 @@
function handleApplyFilter(id) {
selectAllFilters();
contextData.unselectAll();
$highlightPolarization = false;
$highlightCib = false;
switch (id) {
case 0:
disinformantNationFilter.selectOne('China');
@ -26,6 +30,7 @@
break;
case 1:
sourceFilter.selectOne('Facebook');
$highlightCib = true;
break;
case 2:
$attributionScoreFilter = [$attributionScoreFilter[0], 6];
@ -58,13 +63,13 @@
FIAT consists of six elements that work together in order to tell the complete story of foreign interference allegations in the 2020 U.S. elections.
</p>
<p>
<em>Filters</em> enable users to adjust the visibility of cases by <span class="pseudolink copy-tooltip" on:click={() => scrollTo('attribution-score', 'collapsible-methodology')} use:copytooltipable={'The Attribution Score is a framework of eighteen binary statements (true or false) intended to assess the credibility, objectivity, evidence, and transparency of a given case.'}>Attribution Score</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={'Disinformant Nation is the nation from which the case allegedly originated. This does not necessarily denote that the activity was associated with a government.'}>Disinformant Nation</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('platform', 'collapsible-codebook')} use:copytooltipable={'Platform(s) on which the case allegedly took place, divided between the open web, social media platforms, messaging platforms, and other platforms like email and forum boards.'}>Platform</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('method', 'collapsible-codebook')} use:copytooltipable={'Method(s) involved in both the creation and amplification of content related to the case. Sockpuppets are one method; hacking by means of data exfiltration is another.'}>Method</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={'Source describes the individual or entity that originated a foreign interference claim.'}>Source</span>, and <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={'Source Category is the broad classification (e.g. Government, Technology Company) of the Source of a given case.'}>Source Category</span>. Free text search is also supported. This view also supports the addition of contextual datasets.
<em>Filters</em> enable users to adjust the visibility of cases by <span class="pseudolink copy-tooltip" on:click={() => scrollTo('attribution-score', 'collapsible-methodology')} use:copytooltipable={{content: 'The Attribution Score is a framework of eighteen binary statements (true or false) intended to assess the credibility, objectivity, evidence, and transparency of a given case.'}}>Attribution Score</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={{content: 'Disinformant Nation is the nation from which the case allegedly originated. This does not necessarily denote that the activity was associated with a government.'}}>Disinformant Nation</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('platform', 'collapsible-codebook')} use:copytooltipable={{content: 'Platform(s) on which the case allegedly took place, divided between the open web, social media platforms, messaging platforms, and other platforms like email and forum boards.'}}>Platform</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('method', 'collapsible-codebook')} use:copytooltipable={{content: 'Method(s) involved in both the creation and amplification of content related to the case. Sockpuppets are one method; hacking by means of data exfiltration is another.'}}>Method</span>, <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={{content: 'Source describes the individual or entity that originated a foreign interference claim.'}}>Source</span>, and <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={{content: 'Source Category is the broad classification (e.g. Government, Technology Company) of the Source of a given case.'}}>Source Category</span>. Free text search is also supported. This view also supports the addition of contextual datasets.
</p>
<p>
<em>Case View</em> shows a series of interactable circles of various sizes and heights, each of which represents one case. The transparency of the circles corresponds to their Attribution Score. The radii of the circles correspond to the <span class="pseudolink copy-tooltip" on:click={() => scrollTo('attribution-impact', 'collapsible-methodology')} use:copytooltipable={'Attribution Impact measures the spread of case-related articles and content over the seven days following a foreign interference allegation. It is a sum of Facebook engagements, Twitter shares, and Reddit engagements.'}>Attribution Impact</span> of the case, measured on a square root scale with a built-in minimum size for cases without Attribution Impact. The height of the circles also corresponds to the Attribution Impact, measured on a logarithmic scale. Therefore, difference in both the size and height of two given cases indicates exponential variation in their Attribution Impact. The individual thickness of the three surrounding rings represents the relative contribution of Facebook, Twitter and Reddit shares and engagements scaled by total Attribution Impact. This allows for a direct within case comparison. Cases are ordered chronologically by <span class="pseudolink copy-tooltip" on:click={() => scrollTo('activity-attribution-date', 'collapsible-codebook')} use:copytooltipable={'Date of Attribution indicates the publication date of the attribution upon which the case is based.'}>Date of Attribution</span>, from left to right. Cases are attached to a tail that indicates one or more Disinformant Nations. Two or more cases can be selected to compare them in the <em>Dataset View</em>.
<em>Case View</em> shows a series of interactable circles of various sizes and heights, each of which represents one case. The transparency of the circles corresponds to their Attribution Score. The radii of the circles correspond to the <span class="pseudolink copy-tooltip" on:click={() => scrollTo('attribution-impact', 'collapsible-methodology')} use:copytooltipable={{content: 'Attribution Impact measures the spread of case-related articles and content over the seven days following a foreign interference allegation. It is a sum of Facebook engagements, Twitter shares, and Reddit engagements.'}}>Attribution Impact</span> of the case, measured on a square root scale with a built-in minimum size for cases without Attribution Impact. The height of the circles also corresponds to the Attribution Impact, measured on a logarithmic scale. Therefore, difference in both the size and height of two given cases indicates exponential variation in their Attribution Impact. The individual thickness of the three surrounding rings represents the relative contribution of Facebook, Twitter and Reddit shares and engagements scaled by total Attribution Impact. This allows for a direct within case comparison. Cases are ordered chronologically by <span class="pseudolink copy-tooltip" on:click={() => scrollTo('activity-attribution-date', 'collapsible-codebook')} use:copytooltipable={{content: 'Date of Attribution indicates the publication date of the attribution upon which the case is based.'}}>Date of Attribution</span>, from left to right. Cases are attached to a tail that indicates one or more Disinformant Nations. Two or more cases can be selected to compare them in the <em>Dataset View</em>.
</p>
<p>
<em>Case Tooltips</em> are accessible by hovering over a given case. This enables users to see the <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={'Attribution Type indicates how strongly a case is associated with a particular political actor. It denotes the difference between a state-directed military operation versus the efforts of a political troll farm.'}>Attribution Type</span>, Date of Attribution, the <span class="pseudolink copy-tooltip" on:click={() => scrollTo('activity-attribution-date', 'collapsible-codebook')} use:copytooltipable={'Date(s) of Activity capture the earliest and latest activity alleged in a given case. Not always available.'}>Date(s) of Activity</span>, and a <span class="pseudolink copy-tooltip" on:click={() => scrollTo('description-link', 'collapsible-codebook')} use:copytooltipable={'Narrative description of a given case, including Source, Disinformation, and Disinformant Nation. Describes the number and reach (when known) of digital assets employed.'}>Description</span> of a given case. Users can also see a breakdown of a cases Attribution Score by its four subsections (Credibility, Objectivity, Evidence, and Transparency); clicking on the question mark on the right-hand corner of this view also expands the full scorecard. Platforms, Methods, Source, Source Category, and <span class="pseudolink copy-tooltip" on:click={() => scrollTo('description-link', 'collapsible-codebook')} use:copytooltipable={'Link of Attribution links to the source claim upon which assessments are based. An archived link is available in the full downloadable dataset.'}>Link of Attribution</span> are also presented in the tooltip and can be clicked to auto-filter the <em>Case View</em> accordingly.
<em>Case Tooltips</em> are accessible by hovering over a given case. This enables users to see the <span class="pseudolink copy-tooltip" on:click={() => scrollTo('source', 'collapsible-codebook')} use:copytooltipable={{content: 'Attribution Type indicates how strongly a case is associated with a particular political actor. It denotes the difference between a state-directed military operation versus the efforts of a political troll farm.'}}>Attribution Type</span>, Date of Attribution, the <span class="pseudolink copy-tooltip" on:click={() => scrollTo('activity-attribution-date', 'collapsible-codebook')} use:copytooltipable={{content: 'Date(s) of Activity capture the earliest and latest activity alleged in a given case. Not always available.'}}>Date(s) of Activity</span>, and a <span class="pseudolink copy-tooltip" on:click={() => scrollTo('description-link', 'collapsible-codebook')} use:copytooltipable={{content: 'Narrative description of a given case, including Source, Disinformation, and Disinformant Nation. Describes the number and reach (when known) of digital assets employed.'}}>Description</span> of a given case. Users can also see a breakdown of a cases Attribution Score by its four subsections (Credibility, Objectivity, Evidence, and Transparency); clicking on the question mark on the right-hand corner of this view also expands the full scorecard. Platforms, Methods, Source, Source Category, and <span class="pseudolink copy-tooltip" on:click={() => scrollTo('description-link', 'collapsible-codebook')} use:copytooltipable={{content: 'Link of Attribution links to the source claim upon which assessments are based. An archived link is available in the full downloadable dataset.'}}>Link of Attribution</span> are also presented in the tooltip and can be clicked to auto-filter the <em>Case View</em> accordingly.
</p>
<p>
<em>Timeline View</em> enables cases to be ordered chronologically from left to right. Noteworthy U.S. events in the U.S. 2020 election cycle are plotted on the timeline for context and reference. Additional timeline elements can be introduced with the Context Datasets filter. By clicking and dragging on the timeline, users can filter their view to a particular date range. They can return to the default view by clicking "Reset time scale" on the left-hand side of the timeline.
@ -83,7 +88,7 @@
</p>
<ul class="filter-list">
<li on:click|self={() => handleApplyFilter(0)}>China-Related Allegations as Compared to U.S. COVID-19 Cases</li>
<li on:click|self={() => handleApplyFilter(1)}>All Foreign Interference Allegations Made by Facebook</li>
<li on:click|self={() => handleApplyFilter(1)}>All Foreign Interference Allegations Made by Facebook with CIB Data</li>
<li on:click|self={() => handleApplyFilter(2)}>All Foreign Interference Allegations That Lack Significant Evidence</li>
</ul>
<!-- <p>

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

@ -22,7 +22,8 @@
smiTotalYScale,
smiTotalRScale,
smiShareRScale,
attributionScoreScale } from '../stores/scales';
attributionScoreScale,
polarizationScale } from '../stores/scales';
import {
disinformantNationFilter,
platformFilter,
@ -35,8 +36,19 @@
originalTimeDomain,
contextData,
caseIdFilter,
tagFilter } from '../stores/filters';
import { haveOverlap, withinRange, includesTextSearch, isCaseId, preloadImages } from '../utils/misc';
tagFilter,
polarizationFilter,
polarizationDef,
highlightPolarization,
highlightCib } from '../stores/filters';
import {
haveOverlap,
withinRange,
includesTextSearch,
isCaseId,
showPolarization,
showCib,
preloadImages } from '../utils/misc';
import { selected } from '../stores/eventSelections';
import { drawWrapper } from '../stores/elements';
@ -50,7 +62,7 @@
forceCenter,
forceCollide,
timeFormat } from 'd3';
import { sortConsistently } from '../utils/misc';
import { sortConsistently, calculateAveragePolarization } from '../utils/misc';
import { parseUrl } from '../utils/share';
import ToTop from './ToTop.svelte';
@ -91,6 +103,7 @@
sourceCategoryFilter.init(data, 'sourceCategory');
tagFilter.init(data, 'tags');
$attributionScoreFilter = attributionScoreDef;
$polarizationFilter = polarizationDef;
// get context datasets
$contextData = [
@ -132,8 +145,11 @@
tagFilter.applyBoolArray(urlFilters.tags);
contextData.applyBoolArray(urlFilters.contextData);
$attributionScoreFilter = urlFilters.attributionScores;
$polarizationFilter = urlFilters.polarization;
$textSearchFilter = urlFilters.textSearch;
$caseIdFilter = urlFilters.caseId;
$highlightPolarization = urlFilters.highlightPolarization;
$highlightCib = urlFilters.highlightCib;
}
});
@ -142,21 +158,26 @@
$: if (data) {
// calculate scaled data points
const scaledData = data.map((d) => ({
...d,
_x: $timeScale(d.attributionDate),
_y: $smiTotalYScale.range()[0],
color: $attributionScoreScale(d.attributionScore),
rSmiTot: isNaN(d.smiTotal) || d.smiTotal === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiTotal),
rSmiFb: isNaN(d.smiFacebook) || d.smiFacebook === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiFacebook),
rSmiTw: isNaN(d.smiTwitter) || d.smiTwitter === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiTwitter),
rSmiRe: isNaN(d.smiReddit) || d.smiReddit === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiReddit),
rSmiFbShare: $smiShareRScale(d.smiFacebookShare),
rSmiTwShare: $smiShareRScale(d.smiTwitterShare),
rSmiReShare: $smiShareRScale(d.smiRedditShare),
fy: d.smiPending ? Math.min($smiTotalYScale.range()[0], $smiTotalYScale.range()[0] - 2 * $smiTotalRScale.range()[0] + (Math.random() - 0.5) * 20) : $smiTotalYScale(d.smiTotal),
outOfTimeRange: $timeScale(d.attributionDate) < $timeScale.range()[0] || $timeScale(d.attributionDate) > $timeScale.range()[1]
}))
const scaledData = data.map((d) => {
const averagePolarization = calculateAveragePolarization(d.polarization.general);
return {
...d,
_x: $timeScale(d.attributionDate),
_y: $smiTotalYScale.range()[0],
color: $attributionScoreScale(d.attributionScore),
rSmiTot: isNaN(d.smiTotal) || d.smiTotal === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiTotal),
rSmiFb: isNaN(d.smiFacebook) || d.smiFacebook === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiFacebook),
rSmiTw: isNaN(d.smiTwitter) || d.smiTwitter === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiTwitter),
rSmiRe: isNaN(d.smiReddit) || d.smiReddit === 0 ? $smiTotalRScale.range()[0] : $smiTotalRScale(d.smiReddit),
rSmiFbShare: $smiShareRScale(d.smiFacebookShare),
rSmiTwShare: $smiShareRScale(d.smiTwitterShare),
rSmiReShare: $smiShareRScale(d.smiRedditShare),
fy: d.smiPending ? Math.min($smiTotalYScale.range()[0], $smiTotalYScale.range()[0] - 2 * $smiTotalRScale.range()[0] + (Math.random() - 0.5) * 20) : $smiTotalYScale(d.smiTotal),
outOfTimeRange: $timeScale(d.attributionDate) < $timeScale.range()[0] || $timeScale(d.attributionDate) > $timeScale.range()[1],
averagePolarization,
polarizationColor: $polarizationScale(averagePolarization)
};
})
.sort((a, b) => sortConsistently(a, b, 'rSmiTot', 'id'));
// for some rason these definitions need to be in here and not in a gobal scope or module
@ -202,7 +223,10 @@
&& haveOverlap($tagFilter, d.tags)
&& includesTextSearch($textSearchFilter, d.search)
&& withinRange($attributionScoreFilter, d.attributionScore)
&& withinRange($polarizationFilter, d.averagePolarization, !$highlightPolarization)
&& isCaseId($caseIdFilter, d.id)
&& showPolarization($highlightPolarization, d.polarization)
&& showCib($highlightCib, d.cib)
}));
}
</script>

51
src/inputs/cib.js Обычный файл
Просмотреть файл

@ -0,0 +1,51 @@
export const cibTableFields = [
{
number: 'accountsTotalFb',
followers: null,
platformName: 'Facebook',
fieldName: 'Accounts'
},
{
number: 'pagesTotalFb',
followers: 'pagesFollowersTotalFb',
platformName: 'Facebook',
fieldName: 'Pages'
},
{
number: 'groupsTotalFb',
followers: 'groupsFollowersTotalFb',
platformName: 'Facebook',
fieldName: 'Groups'
},
{
number: 'eventsTotal',
followers: null,
platformName: 'Facebook',
fieldName: 'Events'
},
{
number: 'accountsTotalIg',
followers: 'followersTotalIg',
platformName: 'Instagram',
fieldName: 'Accounts'
}
].map((d, i) => ({id: i, ...d}));
export const cibColumnHeaders = [
{
name: '',
type: 'platformName'
},
{
name: '',
type: 'fieldName'
},
{
name: 'Entities',
type: 'number'
},
{
name: 'Followers',
type: 'followers'
}
].map((d, i) => ({id: i, ...d}));

27
src/inputs/polarization.js Обычный файл
Просмотреть файл

@ -0,0 +1,27 @@
export const categories = [
{
id: 'l',
name: 'left',
weight: -2
},
{
id: 'll',
name: 'lean left',
weight: -1
},
{
id: 'c',
name: 'center',
weight: 0
},
{
id: 'lr',
name: 'lean right',
weight: 1
},
{
id: 'r',
name: 'right',
weight: 2
},
];

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

@ -64,6 +64,9 @@ export const tagFilter = createInclusiveFilter();
export const attributionScoreFilter = createRangeFilter();
export const attributionScoreDef = [0, 18];
export const polarizationFilter = createRangeFilter();
export const polarizationDef = [-2, 2];
export const unselectAllFilters = (disinformantNation = true) => {
if (disinformantNation) disinformantNationFilter.unselectAll();
platformFilter.unselectAll();
@ -72,6 +75,7 @@ export const unselectAllFilters = (disinformantNation = true) => {
sourceCategoryFilter.unselectAll();
tagFilter.unselectAll();
attributionScoreFilter.set(attributionScoreDef);
polarizationFilter.set(polarizationDef);
};
export const selectAllFilters = (disinformantNation = true) => {
@ -82,6 +86,7 @@ export const selectAllFilters = (disinformantNation = true) => {
sourceCategoryFilter.selectAll();
tagFilter.selectAll();
attributionScoreFilter.set(attributionScoreDef);
polarizationFilter.set(polarizationDef);
textSearchFilter.reset();
caseIdFilter.set(undefined);
};
@ -94,3 +99,7 @@ export const brushed = writable(false);
export const originalTimeDomain = writable(null);
export const caseIdFilter = writable();
export const highlightPolarization = writable(false);
export const highlightCib = writable(false);

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

@ -8,4 +8,6 @@ export const attributionScoreScale = writable();
export const centroidScale = writable();
export const polarizationScale = writable();
export const scaleFactor = readable(window.devicePixelRatio || 1);

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

@ -1,4 +1,11 @@
export const bg = '#F9F8F8';
export const usaBlue = '#3c3b6e';
export const usaRed = '#b22234';
export const usaLightRed = '#b22234';
export const usaLightLightRed = '#dbb6b6';
export const polBlue = '#2e64a0';
export const polLightBlue = '#61a3de';
export const polPurple = '#96659e';
export const polLightRed = '#a15552';
export const polRed = '#ca0800';

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

@ -12,6 +12,8 @@ const loadData = async () => {
const smiPending = isNaN(smiTotal);
const source = d.source_for_display !== '' ? d.source_for_display : d.source;
const allSidesArticleCount = ((+d.allsides_count_left) + (+d.allsides_count_leanleft) + (+d.allsides_count_center) + (+d.allsides_count_leanright) + (+d.allsides_count_right));
return {
id: i,
timestamp: parseTimestamp([d.timestamp, '-0400'].join(' ')),
@ -71,7 +73,63 @@ const loadData = async () => {
attribution_open_source: +d.attribution_open_source,
attribution_acknowledge_limitations: +d.attribution_acknowledge_limitations,
attribution_corroboration: +d.attribution_corroboration,
tags: splitString(d.tags)
tags: splitString(d.tags),
articleCount: +d.articleCount,
polarization: {
fulfills10Articles: allSidesArticleCount >= 10,
fulfills25Percent: allSidesArticleCount / (+d.article_count) >= 0.25,
count: {
l: +d.allsides_count_left,
ll: +d.allsides_count_leanleft,
c: +d.allsides_count_center,
lr: +d.allsides_count_leanright,
r: +d.allsides_count_right
},
general: {
l: +d.allsides_engagement_left,
ll: +d.allsides_engagments_leanleft,
c: +d.allsides_engagments_center,
lr: +d.allsides_engagments_leanright,
r: +d.allsides_engagments_right
},
facebook: {
l: +d.allsides_engagments_left_facebook,
ll: +d.allsides_engagments_leanleft_facebook,
c: +d.allsides_engagments_center_facebook,
lr: +d.allsides_engagments_leanright_facebook,
r: +d.allsides_engagments_right_facebook
},
twitter: {
l: +d.allsides_engagments_left_twitter,
ll: +d.allsides_engagments_leanleft_twitter,
c: +d.allsides_engagments_center_twitter,
lr: +d.allsides_engagments_leanright_twitter,
r: +d.allsides_engagments_right_twitter
},
reddit: {
l: +d.allsides_engagments_left_reddit,
ll: +d.allsides_engagments_leanleft_reddit,
c: +d.allsides_engagments_center_reddit,
lr: +d.allsides_engagments_leanright_reddit,
r: +d.allsides_engagments_right_reddit
}
},
cib: {
hasCib: +d.cases > 0,
entryDate: parseDate(d.entry_date),
announcedDate: parseDate(d.announced_date),
url: d.url,
pagesTotalFb: +d.fb_pages_total,
budgetTotalUsd: +d.budget_usd_total,
accountsTotalFb: +d.fb_accounts_total,
pagesFollowersTotalFb: +d.fb_pages_followers_total,
groupsTotalFb: +d.fb_groups_total,
groupsFollowersTotalFb: +d.fb_groups_followers_total,
eventsTotal: +d.Events_total,
accountsTotalIg: +d.ig_accounts_total,
followersTotalIg: +d.ig_followers_total,
cases: +d.cases
}
};
});

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

@ -1,6 +1,7 @@
import { uniq } from 'lodash';
import { mean, min, max } from 'd3';
import { images } from '../inputs/dataPaths';
import { categories } from '../inputs/polarization';
// extract attribution date range from data
export const getTimeRange = (data) => {
@ -39,7 +40,7 @@ export const haveOverlap = (filter, arr) =>
filter.filter((d) => d.selected).map((d) => d.id).some((item) => arr.includes(item));
// check, if a number is within a 2D range (given as array with length 2)
export const withinRange = (arr, num) => num >= arr[0] && num <= arr[1];
export const withinRange = (arr, num, bypass = false) => bypass ? true : (num >= arr[0] && num <= arr[1]);
// check, if a search string (filter) is included in a string
export const includesTextSearch = (filter, s) => {
@ -53,6 +54,18 @@ export const includesTextSearch = (filter, s) => {
// check if case id filter is set and if id is matching
export const isCaseId = (filter, id) => filter === undefined ? true : (filter === id);
// check, if polarization data can be shown
export const showPolarization = (filter, polarization) => {
if (!filter) return(true);
return(polarization.fulfills10Articles || polarization.fulfills25Percent);
};
// check, if cib data can be shown
export const showCib = (filter, cib) => {
if (!filter) return(true);
return(cib.hasCib);
};
// extract filter items from data
export const extractFilterCategories = (data, name) =>
uniq(data.map((d) => d[name]).flat());
@ -118,3 +131,16 @@ export const scrollTo = (targetId, collapsibleId) => {
return(false);
};
window.scrollsmooth = scrollTo;
// calculate average polarization using weights
export const calculateAveragePolarization = (polarization) => {
const weightedEngagement = Object.keys(polarization).map((id) => {
const weight = categories.find((c) => c.id === id).weight;
return(weight * polarization[id]);
})
.reduce((acc, cur) => acc + cur);
const totalEngagement = Object.keys(polarization).map((id) => polarization[id]).reduce((acc, cur) => acc + cur);
return(weightedEngagement / totalEngagement);
};

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

@ -4,8 +4,15 @@ import {
smiTotalRScale,
smiShareRScale,
attributionScoreScale,
centroidScale } from '../stores/scales';
import { usaRed } from '../utils/colors';
centroidScale,
polarizationScale } from '../stores/scales';
import {
usaRed,
polBlue,
polLightBlue,
polPurple,
polLightRed,
polRed } from '../utils/colors';
import {
scaleTime,
scaleLinear,
@ -50,4 +57,9 @@ export const setScales = (data, width, minDim, maxDim, panelHeight, margin) => {
centroidScale.set(scaleSqrt()
.domain([0, max(casesPerCountry)])
.range([maxDim * 0.0005, maxDim * 0.01]));
// polarization scale
polarizationScale.set(scaleLinear()
.domain([-2, 0, 2])
.range([polBlue, polPurple, polRed]));
};

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

@ -8,24 +8,34 @@ export const urlFromFilters = (disinformantNations,
sourceCategories,
tags,
attributionScores,
polarization,
textSearch,
contextData,
caseId = '') => {
caseId = '',
highlightPolarization,
highlightCib) => {
const params = {
ts: encodeURIComponent(textSearch),
as: [attributionScores[0], attributionScores[1]].join(';'),
pol: [Math.round(100 * polarization[0]) / 100, Math.round(100 * polarization[1]) / 100].join(';'),
f: filtersToHex([disinformantNations, platforms, methods, sources, sourceCategories, tags, contextData]),
id: caseId
id: caseId,
bool: filtersToBin([highlightPolarization, highlightCib])
};
return `${baseUrl}/#${params.f}-${params.id}-${params.ts}-${params.as}`;
return `${baseUrl}/#${params.f}&${params.id}&${params.ts}&${params.as}&${params.pol}&${params.bool}`;
};
export const filtersToHex = (arr) => {
const hex = arr.map((d) => binaryToHex(d.map((d) => +d.selected).join(''))).join('-');
const hex = arr.map((d) => binaryToHex(d.map((d) => +d.selected).join(''))).join('&');
return hex;
};
export const filtersToBin = (arr) => {
const bin = arr.map((d) => d ? 1 : 0).join('');
return bin;
};
export const binaryToHex = (binary) => parseInt(binary , 2).toString(16).toLowerCase();
export const hexToBinary = (hex) => parseInt(hex, 16).toString(2);
@ -34,7 +44,9 @@ export const binaryToBool = (binary) => binary.split('').map((d) => d === '0' ?
export const parseUrl = (hash) => {
const s = hash.substring(1);
const [ disinformantNations, platforms, methods, sources, sourceCategories, tags, contextData, caseId, textSearch, attributionScores] = s.split('-');
const [ disinformantNations, platforms, methods, sources, sourceCategories, tags, contextData, caseId, textSearch, attributionScores, polarization, bools] = s.split('&');
const boolArray = bools.split('').map((d) => +d === 1 ? true : false);
return {
disinformantNations: binaryToBool(hexToBinary(disinformantNations)),
@ -46,6 +58,9 @@ export const parseUrl = (hash) => {
contextData: binaryToBool(hexToBinary(contextData)),
caseId: caseId === '' ? undefined : +caseId,
textSearch: decodeURIComponent(textSearch),
attributionScores: attributionScores.split(';').map((d) => +d)
attributionScores: attributionScores.split(';').map((d) => +d),
polarization: polarization.split(';').map((d) => +d),
highlightPolarization: boolArray[0],
highlightCib: boolArray[1]
};
};