diff --git a/src/door-hanger/door-hanger.js b/src/door-hanger/door-hanger.js index 5758042..81fa23d 100644 --- a/src/door-hanger/door-hanger.js +++ b/src/door-hanger/door-hanger.js @@ -259,7 +259,7 @@ const [AppDoorHanger] = (function() { .text(" "); firstRow.append("span") .attr("class", "count") - .text(d => `(${d.count} times)`); + .text(d => d.count === 1 ? "(1 time)" : `(${d.count} times)`); secondRow.append("span") .attr("class", "bar") .style("width", barWidth); diff --git a/src/info/info.html b/src/info/info.html index 59a26e3..6c14952 100644 --- a/src/info/info.html +++ b/src/info/info.html @@ -51,15 +51,15 @@
What you may not know is that even when you haven't shared interests, Facebook makes guesses about your views and preferences based on your activity. This is also used to target you, but it's not as easy for you to see this. When you use Ad Analysis for Facebook, you can see what Facebook knows about you and how that affects the advertising you see.
How Facebook uses what it knows
- +
Unlike the large target audiences of traditional mass media, Facebook is able to micro-target much smaller groups of people with highly specific advertising messages. How does the social networking site use specific information about you for targeted advertising? While Facebook does not give data directly to advertisers, it does allow advertisers to target people who match selected criteria.
- +
Like other forms of advertising, Facebook provides targeting based on simple demographics like age, gender, or location. It also provides targeting based on your stated interests. These interests are part of the data that Facebook allows you to download.
- +
Facebook also infers information about you based on the data it gathers from you, your activities, and even from your relationship to others in your network. Facebook also uses these inferred target values to you as “categories” that advertisers can use to target individual ads. Facebook does not make these advertising categories available to you as a download, which can make it difficult to see the bigger picture of how you're being targeted over time on Facebook.
- +
That is where Ad Analysis for Facebook can help. By using this extension, you can get a fuller picture and see a collection of targeting data used to deliver advertising to you. You can also compare a wide range of targets shared as a public data set by thousands of other Facebook users.
- +
Your Targeting Data
@@ -74,56 +74,58 @@
How the Ad Analysis for Facebook extension works
- +
In order to work, the extension must be on and www.facebook.com must be open in the Firefox browser. If the extension is off, or if you are on any website other than www.facebook.com, the extension will do nothing.
- + +
While you are browsing Facebook, the extension scans the webpage for posts that are visible only in the browser window. It checks for posts that include the phrase “sponsored” to determine which posts are ads.
+
An example of a Facebook post
- +
Then, the extension collects the targeting information available in the modal window opened from the “Why am I seeing this?” menu. This automates the manual steps you would need to take to open the menu and read the modal window for each ad if you wanted to track this information on your own.
- +
The targeting information you see in the Ad Analysis for Facebook panel that extends from the toolbar represents roughly the last 7 days of posts you view. The targeting information you see on this page under Your Targeting Data represents all the posts you've viewed since you've started using this extension.
- +
Examples of a Facebook post with its menu open showing the 'Why am I seeing this?' menu item, and a modal window containing ad targeting information.
- +
All of the targeting information displayed by this extension remains on your computer. None of it is sent to Mozilla or third parties.
- +
This extension is not a Facebook app and does not use the Facebook API. Unlike apps that use the Facebook API, this extension does not share your Facebook data with Facebook or other third parties.
- +
Peek outside your filter bubble
- +
Seeing how and why you're targeted for advertising is one way to examine how Facebook shapes the content you're likely to see and how that might influence your opinions and beliefs. But what about the advertising you don't see? What messages are you being excluded from, and what messages do other people in your community see? How does that shape the conversations people are having online and the things people do offline?
- +
Facebook maintains its own Ad Archive, which offers Facebook users a simple keyword search tool to view ads related to politics or issues of national importance.
- +
The Ad Archive is useful if you know what you are looking for, but may not help you if you are trying to answer questions like “Who is spending the most money on political ads in my state?” or “If I am a Millennial, do I see different ads than Baby Boomers?” Using the tool below, you can see a list of top advertisers; filter advertisers by state, gender, and age; and link to advertiser's ads.
- +
Top Facebook political advertisers
- +
Filter by state:
Filter by gender and age:
- - + +
- +
More about Ad Analysis for Facebook
- +
Source code
- +
Ad Analysis for Facebook is an open source project from Mozilla, makers of the Firefox browser. You can access code and supporting documentation on GitHub.
- +
Your privacy
- +
This extension only collects information from the www.facebook.com page when the extension is on and Facebook is in the active tab on Firefox.
- +
This extension collects information from Facebook ads that appear on your screen. It does not collect information from your posts.
- +
This extension stores the information it collects from the www.facebook.com webpage in the browser. It does not send that data to Mozilla or any other entity.
- +
If other users log in to www.facebook.com using Firefox on this computer, this extension may show and collect information from the ads displayed to them, or allow them to see the ads targeted to you.
- +
You can clear the information saved by this extension in the “Clear all collected ads” section of the toolbar panel.
Credits
@@ -140,7 +142,7 @@
To limit the data Facebook can collect about you while you browse the web, check out Facebook Container.
-
To contribute to the ProPublica Political Ad Collection project, install the Facebook Political Ad Collector extension.
+
To contribute to the ProPublica Political Ad Collection project, install the Political Ad Collector extension.
diff --git a/src/info/info.js b/src/info/info.js index c81eeb7..feb51ae 100644 --- a/src/info/info.js +++ b/src/info/info.js @@ -51,7 +51,7 @@ const [AppPage] = (function() { AppPage.prototype._renderTargets = function(elem, showPercentage = false) { const renderAdCount = (d) => { - return ` (${d.typeCount} times)`; + return (d.typeCount === 1) ? "(1 time)" : ` (${d.typeCount} times)`; }; const renderAdPercentage = (d) => { const percentage = Math.round(1000.0 * d.typeCount / d.adCount) / 10.0; @@ -447,33 +447,37 @@ const [AppPage] = (function() { if (a.lowImpressions !== b.lowImpressions) { return b.lowImpressions - a.lowImpressions; } - else { - return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); + if (a.highImpressions !== b.highImpressions) { + return b.highImpressions - a.highImpressions; } + return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); }; const highImpressionsSorter = (a, b) => { if (a.highImpressions !== b.highImpressions) { return b.highImpressions - a.highImpressions; } - else { - return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); + if (a.lowImpressions !== b.lowImpressions) { + return b.lowImpressions - a.lowImpressions; } + return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); }; const lowSpendingSorter = (a, b) => { if (a.lowSpending !== b.lowSpending) { return b.lowSpending - a.lowSpending; } - else { - return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); + if (a.highSpending !== b.highSpending) { + return b.highSpending - a.highSpending; } + return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); }; const highSpendingSorter = (a, b) => { if (a.highSpending !== b.highSpending) { return b.highSpending - a.highSpending; } - else { - return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); + if (a.lowSpending !== b.lowSpending) { + return b.lowSpending - a.lowSpending; } + return alphanumeric(a.advertiser).localeCompare(alphanumeric(b.advertiser)); }; const getTopAdvertisers = () => { const threshold = (data) => { @@ -482,8 +486,10 @@ const [AppPage] = (function() { const data = rawTable.map(row => { return { "advertiser": row[0], - "highImpressions": parseInt(row[1]), - "highSpending": parseInt(row[2]), + "lowImpressions": parseInt(row[1]), + "highImpressions": parseInt(row[2]), + "lowSpending": parseInt(row[3]), + "highSpending": parseInt(row[4]), }; }); if (this.sortByKey === SORT_BY_LABEL) { @@ -519,52 +525,97 @@ const [AppPage] = (function() { this.renderTopAdvertisers(); }; }; - const labelHeader = header.append("span") - .attr("class", "label"); - labelHeader.append("span") - .text("Advertiser"); - const impressionsHeader = header.append("span") - .attr("class", "impressions action actionSortBy actionSortByHighImpressions") + const labelHeader = header.append("span").attr("class", "field label") + .append("div").attr("class", "doubleLabel action actionSortBy actionSortByLabel") + .on("mouseover", onMouseOverEvent) + .on("mouseout", onMouseOutEvent) + .on("click",createOnClickEvent(SORT_BY_LABEL)); + labelHeader.append("span").text("Advertiser"); + labelHeader.append("img") + .attr("class", "sort sortAZ") + .attr("alt", "Sort by advertiser") + .attr("src", "sort_az.svg"); + + const impressionsHeader = header.append("span").attr("class", "field impressions"); + impressionsHeader.append("div").attr("class", "topLabel") + .append("span").text("Impressions"); + const bottomImpressionsHeader = impressionsHeader.append("div").attr("class", "bottomLabel"); + const bottomLowImpressionsHeader = bottomImpressionsHeader.append("span") + .attr("class", "lowImpressions action actionSortBy actionSortByLowImpressions") + .on("mouseover", onMouseOverEvent) + .on("mouseout", onMouseOutEvent) + .on("click",createOnClickEvent(SORT_BY_LOW_IMPRESSIONS)); + bottomImpressionsHeader.append("span").attr("class", "dash"); + const bottomHighImpressionsHeader = bottomImpressionsHeader.append("span") + .attr("class", "highImpressions action actionSortBy actionSortByHighImpressions") .on("mouseover", onMouseOverEvent) .on("mouseout", onMouseOutEvent) .on("click",createOnClickEvent(SORT_BY_HIGH_IMPRESSIONS)); - impressionsHeader.append("span") - .text("Impressions"); - impressionsHeader.append("span") - .append("img") - .attr("class", "sort sortNumber") - .attr("alt", "Sort by impressions") - .attr("src", "sort_number.svg"); - const spendingHeader = header.append("span") - .attr("class", "spending action actionSortBy actionSortByHighSpending") + bottomLowImpressionsHeader.append("span").text("Low"); + bottomLowImpressionsHeader.append("img") + .attr("class", "sort sortNumber") + .attr("alt", "Sort by low impressions") + .attr("src", "sort_number.svg"); + bottomHighImpressionsHeader.append("span").text("High"); + bottomHighImpressionsHeader.append("img") + .attr("class", "sort sortNumber") + .attr("alt", "Sort by high impressions") + .attr("src", "sort_number.svg"); + const spendingHeader = header.append("span").attr("class", "field spending"); + spendingHeader.append("div").attr("class", "topLabel") + .append("span").text("Spending"); + const bottomSpendingHeader = spendingHeader.append("div").attr("class", "bottomLabel"); + const bottomLowSpendingHeader = bottomSpendingHeader.append("span") + .attr("class", "lowSpending action actionSortBy actionSortByLowSpending") .on("mouseover", onMouseOverEvent) .on("mouseout", onMouseOutEvent) - .on("click", createOnClickEvent(SORT_BY_HIGH_SPENDING)); - spendingHeader.append("span") - .text("Spending"); - spendingHeader.append("span") - .append("img") - .attr("class", "sort sortNumber") - .attr("alt", "Sort by spending") - .attr("src", "sort_number.svg"); + .on("click",createOnClickEvent(SORT_BY_LOW_SPENDING)); + bottomSpendingHeader.append("span").attr("class", "dash"); + const bottomHighSpendingHeader = bottomSpendingHeader.append("span") + .attr("class", "highSpending action actionSortBy actionSortByHighSpending") + .on("mouseover", onMouseOverEvent) + .on("mouseout", onMouseOutEvent) + .on("click",createOnClickEvent(SORT_BY_HIGH_SPENDING)); + bottomLowSpendingHeader.append("span").text("Low"); + bottomLowSpendingHeader.append("img") + .attr("class", "sort sortNumber action actionSortBy actionSortByLowSpending") + .attr("alt", "Sort by low spending") + .attr("src", "sort_number.svg"); + bottomHighSpendingHeader.append("span").text("High"); + bottomHighSpendingHeader.append("img") + .attr("class", "sort sortNumber action actionSortBy actionSortByHighSpending") + .attr("alt", "Sort by high spending") + .attr("src", "sort_number.svg"); }; const renderTableBody = (body) => { const rows = body.selectAll("div.row").data(getTopAdvertisers).enter() .append("div") .attr("class", "row"); - const labelField = rows.append("span").attr("class", "label"); - labelField.append("span") + rows.append("span") + .attr("class", "field label") .append("a") .attr("href", d => getQueryURL(d.advertiser)) .attr("rel", "noopener noreferrer") .attr("target", "_blank") .text(d => d.advertiser); - const impressionsField = rows.append("span").attr("class", "impressions"); - impressionsField.append("span") - .text(d => `< ${formatter(d.highImpressions)}`); - const spendingField = rows.append("span").attr("class", "spending"); - spendingField.append("span") - .text(d => d.highSpending === 0 ? "-" : `< $${formatter(d.highSpending)}`); + rows.append("span") + .attr("class", "field lowImpressions") + .text(d => `${formatter(d.lowImpressions)}`); + rows.append("span") + .attr("class", "field dash") + .text("-"); + rows.append("span") + .attr("class", "field highImpressions") + .text(d => `${formatter(d.highImpressions)}`); + rows.append("span") + .attr("class", "field lowSpending") + .text(d => d.lowSpending === 0 && d.highSpending === 0 ? "" : `$${formatter(d.lowSpending)}`); + rows.append("span") + .attr("class", "field dash") + .text(d => d.highSpending === 0 ? "" : "-"); + rows.append("span") + .attr("class", "field highSpending") + .text(d => d.highSpending === 0 ? "" : `$${formatter(d.highSpending)}`); }; const renderTopAdvertisersTable = (container) => { container.append("div") diff --git a/src/info/info.less b/src/info/info.less index 7177d6d..edf3b07 100644 --- a/src/info/info.less +++ b/src/info/info.less @@ -11,11 +11,16 @@ html, body, div, h1, h2, h3, h4 { border: 0; } +html { + margin-bottom: -1px; +} + body { font-family: @pageFontFamily; font-weight: @fontNormalWeight; font-size: 14px; background-color: @white-100; + margin-bottom: -1px; } a { @@ -257,7 +262,7 @@ select { max-height: 0px; margin-bottom: 32px; overflow-y: scroll; - + .targetValue { display: inline-block; font-size: 12px; @@ -292,11 +297,11 @@ select { } } } - + #PageImpressionsContainer { .topAdvertisersTable { display: none; - font-size: 16px; + font-size: 14px; a { color: @extensionLightBlue; text-decoration: none; @@ -311,27 +316,25 @@ select { * { pointer-events: none; } + } + .sort { + height: 14px; + vertical-align: -2px; + margin-left: 5px; + } + .sortActive { + font-weight: @fontNormalWeight; .sort { - height: 14px; - vertical-align: -2px; - margin-left: 5px; - visibility: hidden; + opacity: 1.0; } - &.sortActive { - .sort { - visibility: visible; - opacity: 1.0; - } - } - &.sortInactive { - .sort { - visibility: visible; - opacity: 0.25; - } - } - &.hover { - color: @extensionDarkBlue; + } + .sortInactive { + .sort { + opacity: 0.25; } + } + &.hover { + color: @extensionDarkBlue; } } .body { @@ -349,22 +352,77 @@ select { } .header, .row { white-space: nowrap; - .label { + .field { display: inline-block; vertical-align: top; white-space: normal; - width: 425px; - overflow: hidden; - } - .impressions, .spending, .counts { - display: inline-block; - vertical-align: top; - white-space: normal; - margin-left: 10px; - width: 160px; - overflow: hidden; + &.label { + width: 340px; + .doubleLabel { + height: 36px; + span { + vertical-align: -8px; + } + img { + vertical-align: -10px; + } + } + } + &.impressions, + &.spending { + margin-left: 50px; + width: 155px; + .topLabel { + margin-bottom: 2px; + } + .bottomLabel { + font-weight: @fontSemiLightWeight; + img { + height: 12px; + vertical-align: -2px; + } + .dash { + display: inline-block; + width: 25px; + } + .lowImpressions { + display: inline-block; + width: 65px; + } + .highImpressions { + display: inline-block; + width: 65px; + } + .lowSpending { + display: inline-block; + width: 60px; + } + .highSpending { + display: inline-block; + width: 60px; + } + } + } + &.dash { + width: 25px; + text-align: center; + } + &.lowImpressions { + margin-left: 50px; + width: 65px; + } + &.highImpressions { + width: 65px; + } + &.lowSpending { + margin-left: 50px; + width: 60px; + } + &.highSpending { + width: 60px; + } } } } } -} +} \ No newline at end of file