Modified

November 26, 2023

About this project

Measuring the progress of the global movement for Palestinian liberation.

I started this document on November 7, 2023, and I work on it daily. Please check back for changes and updates.

Tracking the health and progress of the global movement for Palestinian liberation. This tool seeks to answer the question, “Is it having any impact?”

Population effects in Palestine

I am attempting to measure the impact on the population – whether it be in lives, homes, abilities, or health that is lost. 1

⚠️ This visualization will soon be replaced with something that shows more context.

Code
population_loss_chart = {
  // Specify the chart’s dimensions.
  const width = 928;
  const height = Math.min(width, 500);
  // Create the color scale.
  const color = d3.scaleOrdinal()
    .domain(population_loss_in_palestine.map(d => d.name))
    .range(d3.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), population_loss_in_palestine.length).reverse())

  // Create the pie layout and arc generator.
  const pie = d3.pie()
    .sort(null)
    .value(d => d.value);

  const arc = d3.arc()
    .innerRadius(0)
    .outerRadius(Math.min(width, height) / 2 - 1);

  const labelRadius = arc.outerRadius()() * 0.8;

  // A separate arc generator for labels.
  const arcLabel = d3.arc()
    .innerRadius(labelRadius)
    .outerRadius(labelRadius);

  const arcs = pie(population_loss_in_palestine);

  // Create the SVG container.
  const svg = d3.create("svg")
    .attr("class", "panel-fill")
    .attr("width", width)
    .attr("height", height)
    .attr("viewBox", [-width / 2, -height / 2, width, height])
    .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");

  // Add a sector path for each value.
  svg.append("g")
    .attr("stroke", "white")
    .selectAll()
    .data(arcs)
    .join("path")
    .attr("fill", d => d.data.color)
    .attr("d", arc)
    .append("title")
    .text(d => `${d.data.name}: ${d.data.value.toLocaleString("en-US")}`);

  // Create a new arc generator to place a label close to the edge.
  svg.append("g")
    .attr("text-anchor", "middle")
    .selectAll()
    .data(arcs)
    .join("text")
    .attr("transform", d => `translate(${arcLabel.centroid(d)})`)
    .call(text => text.append("tspan")
      .attr("y", "-0.4em")
      .attr("x", (d) => {
        if (d.data.name === "Survivors") {
          return "0.8em"
        }
        if (d.data.name === "Displaced") {
          return "-0.8em"
        }
        return 0

      })
      .attr("font-weight", "bold")
      .attr("font-size", "24px")
      .attr("fill", "#fff")
      .text(d => d.data.name))
    .call(text => text.append("tspan")
      .attr("x", (d) => {
        if (d.data.name === "Survivors") {
          return "0.8em"
        }
        if (d.data.name === "Displaced") {
          return "-0.8em"
        }
        return 0

      })
      .attr("y", "0.7em")
      .attr("font-size", "24px")
      .attr("fill", "#fff")
      .text(d => d.data.value.toLocaleString("en-US")));

  return svg.node();
}

Boycott, Divest, and Sanction

Israel has done a good job of making it impossible for federal contractors to take a stand, but the majority of people can still divest. According to the Palestinian BDS National Committee, these companies are the “top priority boycott targets of the global BDS movement”2. Ideally, they should be losing value, not gaining.

Boycott Progress

The BDS movement has broken down the global boycott efforts into sub-groups. You can learn more about them on their guide: https://bdsmovement.net/Act-Now-Against-These-Companies-Profiting-From-Genocide.

Consumer Boycotts

The BDS movement is asking us to completely boycott these companies. They are chosen specifically because of their complicity in apartheid.

Code
// the other chart has notes since they are nearly the same, and i made it first. this one is listed first, because BDS lists it first.
consumer_boycott_chart = {
    // make a box to put the chart in. the aria label is for screenreaders.
    const container = html`<canvas id="consumer-boycott-chart"  aria-label="Consumer Boycott Chart" style="width:100%;"></canvas>`;

    const data = consumer_boycott_stocks;
    const options = line_chart_options;
    options.plugins.title.text = "Companies Targeted by the Consumer Boycotts since October 7th, 2023"
     

    // This is the chart's settings. It combines the data and scales from above before making the chart.
    const chart_configuration = {
        type: 'line',
        data,
        plugins: [],
        options,
     }

    // build the chart and put it in the container
    const chart = new Chart(
      container, chart_configuration);

 // show the chart 
 yield container;
}
Tip

There’s a lot of data! Click or tap the legend to hide lines from the chart.


Organic Boycotts

Following October 7th, this aspect of the boycott efforts occurred as businesses made announcements/took actions to support the Israeli apartheid on Palestine.

This graph measures how much their stocks have changed since October 7th. You can hover over/tap the lines to get more information.

Code
organic_boycott_chart = {
    // make a box to put the chart in. the aria label is for screenreaders.
    const container = html`<canvas id="organic-boycott-chart"  aria-label="Organic Boycott Chart" style="width:100%;"></canvas>`;
    
    // Chart.js requires me to combine the configuration for the chart and the data used in the chart, and it needs them in a certain format. The formatting is at the bottom of the document.
    const data = organic_boycott_stocks

    const options = line_chart_options
    options.plugins.title.text = "Companies Targeted by the Organic Boycotts since October 7th, 2023"

    // This is the chart's settings. It combines the data and scales from above before making the chart.
    const chart_configuration = {
        type: 'line',
        data,
        plugins: [ /*I cannot get plugins to work at all. This is to apply plugins just to this chart. The `Chart.register` method can be used to add plugins to all charts on the page.  */ ],
        options,
     }

    // build the chart and put it in the container
    const chart = new Chart(
      container, chart_configuration);

 // show chart
 yield container;
}

Divestment Progress

Coming soon!

Pressure Campaign Progress

Coming soon!

Action Map

This map is meant to help you “zoom out” when considering where to plan an event. It also serves as an opportunity to be inspired by others around the world.

This map is under construction

This project now has a team of volunteers behind it. We are currently working on collecting as much event data from around the world and making sure this map can be updating daily/automatically.

Code
viewof get_gps = Inputs.button("📍 Go to my location [under construction]")

gps_message = html`<span></span>`
Code
gps = {
  get_gps;
  let gps_data = null
  if (navigator.geolocation) {
    console.log("navigator", navigator.geolocation.getCurrentPosition(got_gps_data))
    return navigator.geolocation.getCurrentPosition(got_gps_data);
  } else {
    gps_message.innerHTML = `Geolocation is not supported by this browser.`;
  }
  function got_gps_data (data){
    console.log("DATA", data.coords)
    gps_data = [data.coords.latitude, data.coords.longitude]
    return gps_data
  }
  return gps_data
}
Code
container = html`<div style="height: 500px;">`;
action_map = {
  console.log({gps})
  const map = L.map(container).setView(gps ? gps : [42.79065, -102.28398], 0);
  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    attribution: "© <a href=https://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors"
  }).addTo(map);
  const shadowUrl = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png";
  const iconSize = [25, 41];
  const iconAnchor = [12, 41];
  const popupAnchor = [1, -34];
  
   // the following icons were made thanks to Maps Icons Collection https://mapicons.mapsmarker.com
  //blue: #265bb2
  //red: #b0273a
  const icons = {
    blueShipIcon: new L.Icon({
    iconUrl: `https://res.cloudinary.com/dooqpphlg/image/upload/v1699735540/mapiconscollection/blue/military/battleship-3_un0ksz.png`,
    shadowUrl,
    iconSize,
    iconAnchor,
    popupAnchor
  }),
    blueMissileIcon: new L.Icon({
    iconUrl: "https://res.cloudinary.com/dooqpphlg/image/upload/v1699565523/missile-2_fex4k5.png",
    shadowUrl,
    iconSize,
    iconAnchor,
    popupAnchor
  }), 
    blueTruckIcon: new L.Icon({
    iconUrl: "https://res.cloudinary.com/dooqpphlg/image/upload/v1699744432/mapiconscollection/blue/transportation/truck3_cgely7.png",
    shadowUrl,
    iconSize,
    iconAnchor,
    popupAnchor
  }),
    redIcon: new L.Icon({
    iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
    shadowUrl,
    iconSize,
    iconAnchor,
    popupAnchor
  }),
    redMissileIcon: new L.Icon({
    iconUrl: "https://res.cloudinary.com/dooqpphlg/image/upload/v1699743356/mapiconscollection/red/military/missile-2_yjevvn.png",
    shadowUrl,
    iconSize,
    iconAnchor,
    popupAnchor
  })
  }

  // Create marker groups for past and future events
  const pastEvents = []
  // create a group for the future stuff that will be filled in with markers
  const future_marker_group = L.layerGroup().addTo(map);

  marker_data.forEach(marker => {
    const date = marker.Date ? new Date(marker.Date) : 0
    const currentDate = new Date();
    // if it's in the past, add to the past list.
    if (date < currentDate){
      pastEvents.push([marker.Latitude, marker.Longitude, 1]) // I would like to change the intensity based on the attendance if i can or event type
    } else {
      // otherwise, make a marker and add it to the future group
      const future_marker = L.marker([marker.Latitude, marker.Longitude], { icon: icons[marker.Icon] })
        .addTo(future_marker_group)
      if(future_marker.Popup){
        future_marker.bindPopup(marker.Popup);
      }
    }
  });
  //create the heatmap using the past events list that was just built
  const past_marker_group =  heatLayer(pastEvents, {radius: 10, blur: 15, minOpacity: 0.2 }).addTo(map);

  //and a group for key locations
  const key_locations_group = L.layerGroup().addTo(map);
  key_locations.forEach(marker => {
    const location_marker = L.marker([marker.Latitude, marker.Longitude], { icon: icons[marker.Icon] }).addTo(key_locations_group)
    if(marker.Popup){
      location_marker.bindPopup(location_marker.Popup);
    }
  })
  return map;
}

Data

Population Counts

Code
palestinian_population = [{
    location: "State of Palestine - West Bank", 
    population: 2077357 
  },
  {
    location: "State of Palestine - Gaza Strip",
    population: 3086816
  },
  {
    location: "1948 Palestinian Territory",
    population: 1634482
  }]
total_amount_of_palestinians_at_risk = palestinian_population.reduce(function(acc, curr){ // this little bit just adds up the populations of the different subregions
  return acc + curr.population
}, 0)
palestinian_martyrs = 11261

There are probably better ways to label palestinians_in_palestine_who_need_our_help, since many in the diaspora are losing their family members, friends, and loved ones and need help and support as they wade through grief.

Code
palestinians_in_palestine_who_need_our_help = total_amount_of_palestinians_at_risk - palestinian_martyrs

This does not include the detainees and hostages Israel has taken. I am still figuring out how to track and incorporate that. Detainees from the West Bank are tracked here: https://www.pcbs.gov.ps/default.aspx.

Code
injured_palestinians = 29901 
// 2023-11-09, 29901
// 2023-11-08, 29355
displaced_palestinians = 1500000

This provides the labels and colors for the pie chart. The pie chart uses this list to get the data listed above as well.

Code
population_loss_in_palestine = [
  {
    value: injured_palestinians,
    name: "Injured",
    color: "#cc9900"
  },
  {
    value: displaced_palestinians,
    name: "Displaced",
    color: "#cccc00"
  },
  {
    value: palestinian_martyrs,
    name: "Martyred",
    color: "#800000"
  },
  {
    value: palestinian_population[1].population - injured_palestinians - displaced_palestinians,
    name: "Survivors",
    color: "#339966"
  },
]

Stock Prices

How the data is processed

Code
function calculatePercentChanged(price_on_day, price_on_day_before = null) {
  // This is where this formula comes from: https://money.stackexchange.com/questions/24741/how-to-calculate-the-closing-price-percentage-change-for-a-stock
  if (price_on_day_before === null) {
    // if it's the first day, start at 0
    return 0
  } else {
    return ((price_on_day - price_on_day_before) / price_on_day_before) * 100
  }
}

// This data comes from Yahoo Finance. I downloaded the CSV's and have not altered them in any way. The stock prices are combined into one lists `STOCK-GROUP_stocks` programmatically. The abbreviations are the stock tickers used in the New York Stock Exchange.

consumer_boycott_stocks = ({ 
    labels: hpq.data.map(d => d.x),
    datasets: [].concat.apply([], [ cspa, hpq, enrde, pumde, capa, fosuf, rmax, pep  ]) 
});

fosuf_file = await FileAttachment("FOSUF.csv").csv({ typed: false })
fosuf = ({
  label: "Fosun (Owns Ahava)",
  data: fosuf_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : fosuf_file[i-1].Close), x: d.Date}))
});
rmax_file = await FileAttachment("RMAX.csv").csv({ typed: false })
rmax = ({
  label: "Re/Max",
  data: rmax_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : rmax_file[i-1].Close), x: d.Date}))
});
pep_file = await FileAttachment("PEP.csv").csv({ typed: false })
pep = ({
  label: "Pepsi (Owns Sabra)",
  data: pep_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : pep_file[i-1].Close), x: d.Date}))
});
enrde_file = await FileAttachment("ENR.DE.csv").csv({ typed: false })
enrde = ({
  label: "Siemens",
  // backgroundColor: "#1682af99", // last 2 digits are opacity percentage
  // borderColor: "#1682af99", 
  data: enrde_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : enrde_file[i-1].Close), x: d.Date}))
});
pumde_file = await FileAttachment("PUM.DE.csv").csv({ typed: false })
pumde = ({
  label: "Puma",
  // backgroundColor: "#ac308d99",
  // borderColor: "#ac308d99",
  data: pumde_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : pumde_file[i-1].Close), x: d.Date}))
});
capa_file = await FileAttachment("CA.PA.csv").csv({ typed: false })
capa = ({
  label: "Carrefour",
  // backgroundColor: "#2f2aae99",
  // borderColor: "#2f2aae99",
  data: capa_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : capa_file[i-1].Close), x: d.Date}))
});
cspa_file = await FileAttachment("CS.PA.csv").csv({ typed: false })
cspa = ({
  label: "AXA",
  // backgroundColor: "#e9542099",
  // borderColor: "#e9542099",
  data: cspa_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : cspa_file[i-1].Close), x: d.Date}))
});
hpq_file = await FileAttachment("HPQ.csv").csv({ typed: false })
  hpq = ({
    // backgroundColor: "#17b35a99",
    // borderColor: "#17b35a99",
    label: "Hewlett Packard",
    data: hpq_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : hpq_file[i-1].Close), x: d.Date}))
});


organic_boycott_stocks = ({ 
    labels: sbux.data.map(d => d.x),
    datasets: [].concat.apply([], [ sbux, mcd, dpz, pzza, wix, yum]) 
});
wix_file = await FileAttachment("WIX.csv").csv({ typed: false })
wix = ({
  label: "Wix",
  data: wix_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : wix_file[i-1].Close), x: d.Date}))
});
yum_file = await FileAttachment("YUM.csv").csv({ typed: false })
yum = ({
  label: "YUM (Owns Pizza Hut)",
  data: yum_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : yum_file[i-1].Close), x: d.Date}))
});
pzza_file = await FileAttachment("PZZA.csv").csv({ typed: false })
pzza = ({
  label: "Papa John's",
  data: yum_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : pzza_file[i-1].Close), x: d.Date}))
});
mcd_file = await FileAttachment("MCD.csv").csv({ typed: false })
mcd = ({
  label: "McDonald's",
  // backgroundColor: "#ac308d99",
  // borderColor: "#ac308d99",
  data: mcd_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : mcd_file[i-1].Close), x: d.Date}))
});
sbux_file = await FileAttachment("SBUX.csv").csv({ typed: false })
sbux = ({
  label: "Starbucks",
  // backgroundColor: "#17b35a99",
  // borderColor: "#17b35a99",
  data: sbux_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : sbux_file[i-1].Close), x: d.Date}))
});
dpz_file = await FileAttachment("DPZ.csv").csv({ typed: false })
dpz = ({
  label: "Domino's",
  // backgroundColor: "#e9542099",
  // borderColor: "#e9542099",
  data: dpz_file.map((d, i) => ({y: calculatePercentChanged(d.Close, i === 0 ? null : dpz_file[i-1].Close), x: d.Date}))
});

Download the data

Map Data

How the data is processed

Code
map_data = await FileAttachment("map.csv").csv({typed: true})
// if it doesn't have a date, it's a location ()
key_locations = map_data.filter(marker => {
  return !marker.Date && marker.Icon !== "redIcon"
})
// this filters the map data to remove the items that don't have geolocation data
marker_data = map_data.filter(marker => {
  return !!marker.Latitude
})

Download the data

Tools

Note

These tools are from other places on the internet, which is why the download buttons are not available here.

This is used to create the stock chart.

Code
// For devs, you'll need these docs to understand why I am using this format just to import chartjs and its plugins
// https://observablehq.com/documentation/debugging/require-stubborn-modules#requiring-stubborn-add-ons
// This tool is also kinda helpful: https://observablehq.com/@observablehq/module-require-debugger

Chart = {
  const Chart = (window.Chart = await require("chart.js@4.4.0/dist/chart.umd.js"));
  // if I can get any of the plugins to work, they would be imported here.
  return Chart
}

This is used to configure the chart.

Code
stock_chart_scales= ({
            y: {
              title: {
                display: true,
                text: "Change in stock price"
              },
              ticks: {
                callback: d => `${d}%`, 
              },
            },
            x: {
              title: {
                display: true,
                text: "Date"
              },
            }
          });

  line_chart_options = ({
          tension: 0.4, // makes the lines kind of curvy and less pointy
          scales: stock_chart_scales, // determine how the x and y axes work
          maintainAspectRatio: false, // allows graph to grow tall for mobile devices
          plugins: { // these plugins are built-in
            title: {
              display: true,
              text: "" // this is set later, depending on the graph that uses it
            },
            subtitle: {
              display: true,
              text: "Percent Change in Stock Value Over Time"
            },
            tooltip: {
              // I can render notes that explain more context for the dates here
              // https://www.chartjs.org/docs/latest/configuration/tooltip.html#tooltip-callbacks
               callbacks: {
                // adds $ to values. From the docs: https://www.chartjs.org/docs/latest/configuration/tooltip.html#label-callback
                    label: function(context) {
                        // Chart.js uses this function and tells us when they use it, they give it this context data. 
                        let label = context.dataset.label || '';
                        if (label) {
                            label += ': ';
                        }
                        if (context.parsed.y !== null) {
                          // format euro
                            if (label === "Carrefour: " || label === "AXA: " || label === "Siemens: " || label === "Puma: " ){
                              label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(context.parsed.y);
                            } else if (label === "Fosun (Owns Ahava): "){
                              // format hkd
                              label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'HKD' }).format(context.parsed.y);
                            } else {
                              // format usd
                              label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
                            }
                        }
                        return label;
                    }
                }
            }
          }
        })

This is used to make the map.

Code
L = require("leaflet")
heatLayer = require('leaflet.heat').catch(() => L.heatLayer)

Footnotes

  1. My data on Palestinian populations comes from the Palestinian Central Bureau of Statistics. Their data doesn’t account for the overlap occuring between measurements (e.g. people who are displaced and injured), repeat experiences (people who’ve been displaced more than once), or how injury is being measured (the inclusion/exclusion or overlapping data regarding illnesses). Additionally, some data is still coming out. For example, there were 5,500 patients in the maternity ward of Al-Shifa hospital expecting to give birth in October 2023 who were transferred to al-Helou hospital, and I don’t know of their current status. Several western countries like the United States and Italy refuse to affirm or investigate these numbers, though the UN has confirmed the Palestinian Authority’s accuracy in reporting deaths in the past. As a result, I am limited in the data sources I can find.↩︎

  2. The emphasis is mine. The Palestinian BDS National Committee is a group of Palestinian organizations that leads the BDS movement. You can learn more at https://bdsmovement.net.↩︎