├── outputs ├── keyword.png ├── hash_cloud.png ├── k_search_diag.png ├── overall_trend.png ├── dynamic_topic_day.png ├── stacked_bar_plots2.png ├── animated_gtrends_plot.gif ├── animated_twitter_plot.gif ├── topic_modeling_static.png ├── anti_asian_topic_dynamic_trend.png └── hash_cloud_files │ ├── wordcloud2-0.0.1 │ ├── wordcloud.css │ ├── hover.js │ └── wordcloud2-all.js │ ├── wordcloud2-binding-0.2.1 │ └── wordcloud2.js │ └── htmlwidgets-1.5.1 │ └── htmlwidgets.js ├── .gitignore ├── code ├── __pycache__ │ └── clean_text.cpython-37.pyc ├── .ipynb_checkpoints │ ├── 05_hashtags-checkpoint.ipynb │ └── 04_clean-checkpoint.ipynb ├── clean_text.py ├── 02_parse.r ├── 00_setup.sh ├── App ├── 01_sample.Rmd ├── 01_google_trends.R ├── 04_01_hashtags.R ├── 03_explore.Rmd ├── 04_clean.ipynb └── 05_topic_modeling.Rmd ├── functions ├── add_normalized.R ├── stacked_area_plot.R ├── stacked_bar_plot.R ├── date2index.R ├── visualize_diag.R └── add_US_location.R ├── hateasiancovid.Rproj ├── App ├── rsconnect │ └── shinyapps.io │ │ └── jaeyeonkim │ │ └── covid19antiasian_hashcloud.dcf └── app.R ├── README.md └── raw_data └── tweet_ids /outputs/keyword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/keyword.png -------------------------------------------------------------------------------- /outputs/hash_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/hash_cloud.png -------------------------------------------------------------------------------- /outputs/k_search_diag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/k_search_diag.png -------------------------------------------------------------------------------- /outputs/overall_trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/overall_trend.png -------------------------------------------------------------------------------- /outputs/dynamic_topic_day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/dynamic_topic_day.png -------------------------------------------------------------------------------- /outputs/stacked_bar_plots2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/stacked_bar_plots2.png -------------------------------------------------------------------------------- /outputs/animated_gtrends_plot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/animated_gtrends_plot.gif -------------------------------------------------------------------------------- /outputs/animated_twitter_plot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/animated_twitter_plot.gif -------------------------------------------------------------------------------- /outputs/topic_modeling_static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/topic_modeling_static.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | *.csv 6 | *.tsv 7 | *.jsonl 8 | *.rds 9 | *.pdf 10 | *.log 11 | -------------------------------------------------------------------------------- /code/__pycache__/clean_text.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/code/__pycache__/clean_text.cpython-37.pyc -------------------------------------------------------------------------------- /outputs/anti_asian_topic_dynamic_trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeyk/covid19antiasian/HEAD/outputs/anti_asian_topic_dynamic_trend.png -------------------------------------------------------------------------------- /code/.ipynb_checkpoints/05_hashtags-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [], 3 | "metadata": {}, 4 | "nbformat": 4, 5 | "nbformat_minor": 4 6 | } 7 | -------------------------------------------------------------------------------- /functions/add_normalized.R: -------------------------------------------------------------------------------- 1 | add_normalized <- function(data, var){ 2 | df <- data %>% 3 | filter({{var}} == 1) %>% 4 | group_by({{var}}, date) %>% 5 | count() 6 | 7 | df$rescaled <- scales::rescale(df$n, to = c(0,1)) %>% round(2) * 100 8 | 9 | df[,-1] 10 | } -------------------------------------------------------------------------------- /hateasiancovid.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | -------------------------------------------------------------------------------- /App/rsconnect/shinyapps.io/jaeyeonkim/covid19antiasian_hashcloud.dcf: -------------------------------------------------------------------------------- 1 | name: covid19antiasian_hashcloud 2 | title: 3 | username: 4 | account: jaeyeonkim 5 | server: shinyapps.io 6 | hostUrl: https://api.shinyapps.io/v1 7 | appId: 3025877 8 | bundleId: 3768360 9 | url: https://jaeyeonkim.shinyapps.io/covid19antiasian_hashcloud/ 10 | when: 1603322007.98313 11 | -------------------------------------------------------------------------------- /outputs/hash_cloud_files/wordcloud2-0.0.1/wordcloud.css: -------------------------------------------------------------------------------- 1 | 2 | #wcLabel { 3 | position: absolute; 4 | border: 2px solid #fff; 5 | box-shadow: 0 0 4px 0 #008; 6 | padding: 2px; 7 | /*margin: -4px 0 0 -4px;*/ 8 | pointer-events: none; } 9 | 10 | #wcSpan { 11 | position: absolute; 12 | top: 100%; 13 | left: 0; 14 | background-color: rgba(255, 255, 255, 0.8); 15 | color: #333; 16 | margin-top: 6px; 17 | padding: 0 0.5em; 18 | border-radius: 0.5em; 19 | white-space: nowrap; } 20 | -------------------------------------------------------------------------------- /functions/stacked_area_plot.R: -------------------------------------------------------------------------------- 1 | 2 | stacked_area_plot <- function(df, var){ 3 | 4 | df %>% 5 | mutate(var = recode({{var}}, 6 | '1' = 'Yes', 7 | '0' = 'No')) %>% 8 | group_by(date, var) %>% 9 | dplyr::summarize(n = n()) %>% 10 | mutate(prop = n / sum(n), 11 | prop = round(prop,2)) %>% 12 | ggplot(aes(x = as.Date(date), y = prop, 13 | fill = factor(var))) + 14 | geom_area() + 15 | scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + 16 | theme_pubr() + 17 | scale_fill_npg() 18 | 19 | } -------------------------------------------------------------------------------- /functions/stacked_bar_plot.R: -------------------------------------------------------------------------------- 1 | 2 | stacked_bar_plot <- function(df, var){ 3 | 4 | df %>% 5 | mutate(var = recode({{var}}, 6 | '1' = 'Yes', 7 | '0' = 'No')) %>% 8 | group_by(date, var) %>% 9 | dplyr::summarize(n = n()) %>% 10 | mutate(prop = n / sum(n), 11 | prop = round(prop,2)) %>% 12 | ggplot(aes(x = as.Date(date), y = prop, group = 1, 13 | fill = factor(var))) + 14 | geom_col(position = "fill") + 15 | scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + 16 | theme_pubr() + 17 | scale_fill_npg() 18 | 19 | } -------------------------------------------------------------------------------- /code/clean_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Tue May 5 16:34:09 2020 5 | 6 | @author: jae 7 | """ 8 | 9 | # HTML tags and punctuation 10 | url_re = r'http\S+' 11 | at_re = r'@[\w]*' 12 | rt_re = r'^[rt]{2}' 13 | punct_re = r'[^\w\s]' 14 | 15 | def clean_tweet(document): 16 | document = document.str.lower() # Lower Case 17 | document = document.str.replace(url_re, '') # Remove Links/URL 18 | document = document.str.replace(at_re, '') # Remove @ 19 | document = document.str.replace(rt_re, '') # Remove rt 20 | document = document.str.replace(punct_re, '') # Remove Punctation 21 | return(document) 22 | -------------------------------------------------------------------------------- /functions/date2index.R: -------------------------------------------------------------------------------- 1 | # Time index 2 | 3 | date2index <- function(df){ 4 | 5 | # Convert date into integer 6 | 7 | index <- as.integer(gsub("-", "", docvars(df)$date)) 8 | 9 | # Replace elements in the numeric vector with the new list of the character vector 10 | 11 | char_index <- as.character(index) 12 | 13 | # Condition 14 | given <- sort(unique(index)) %>% as.character() 15 | 16 | # For loop 17 | for (i in seq(1:length(unique(index)))){ 18 | 19 | char_index[char_index == given[i]] = paste(i) 20 | 21 | message(paste("replaced", i)) 22 | 23 | } 24 | 25 | # Check 26 | # unique(char_index) %>% as.numeric() %>% sort() 27 | 28 | docvars(df, "index") <- char_index %>% as.integer() 29 | 30 | docvars(df) <- docvars(df) %>% 31 | arrange(index) 32 | 33 | docvars(df) 34 | } -------------------------------------------------------------------------------- /code/02_parse.r: -------------------------------------------------------------------------------- 1 | 2 | # Import packages 3 | 4 | pacman::p_load(data.table, # for fast data manipulation 5 | jsonlite, # for importing json data 6 | tidyverse, # for tidyverse 7 | here, # for reproducibility 8 | tidyjson, # for json data manipulation 9 | purrr, # for functional programming 10 | tictoc) # for performance test 11 | 12 | library(tidytweetjson) 13 | 14 | # Parse all 15 | 16 | future::plan("multiprocess") 17 | 18 | tictoc::tic() 19 | df <- jsonl_to_df_all(dir_path = "/home/jae/hateasiancovid/processed_data/splitted_data") 20 | tictoc::toc() 21 | 22 | # Save it. Note that `Compress = FALSE` makes saving fast 23 | 24 | saveRDS(df, "/home/jae/hateasiancovid/processed_data/parsed.rds", 25 | compress = FALSE) 26 | -------------------------------------------------------------------------------- /code/00_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set up automatically 4 | 5 | # Download the file in the raw_data subdirectory of the project directory 6 | 7 | curl -O https://zenodo.org/record/3902855/files/clean_languages.tar.gz?download=1 8 | 9 | # Check the downloaded file, including information on its size 10 | # $ ls -lh 11 | 12 | # Unpack the .tar.gz file 13 | tar -xzf 'clean_languages.tar.gz?download=1' 14 | 15 | # The above command will create combined_languages subdirectory. 16 | 17 | # Change directory to the subdirectory then move the English twitter data to the parent directory and then remove the subdirectory 18 | 19 | cd combined_languages | mv clean_language_en.tsv ../ | rm-r combined_languages/ 20 | 21 | # Inspect the English Twitter data 22 | # $ head clean_language_en.tsv 23 | 24 | # Count the number of rows in the tsv file: 59,650,755 25 | wc -l clean_language_en.tsv 26 | 27 | # 59,650,755 28 | -------------------------------------------------------------------------------- /functions/visualize_diag.R: -------------------------------------------------------------------------------- 1 | visualize_diag <- function(sparse_matrix, many_models){ 2 | 3 | k_result <- many_models %>% 4 | mutate(exclusivity = purrr::map(topic_model, exclusivity), 5 | semantic_coherence = purrr::map(topic_model, semanticCoherence, sparse_matrix)) 6 | 7 | 8 | k_result %>% 9 | transmute(K, 10 | "Exclusivity" = map_dbl(exclusivity, mean), 11 | "Semantic coherence" = map_dbl(semantic_coherence, mean)) %>% 12 | pivot_longer(cols = c("Exclusivity", "Semantic coherence"), 13 | names_to = "Metric", 14 | values_to = "Value") %>% 15 | ggplot(aes(K, Value, color = Metric)) + 16 | geom_line(size = 1.5, show.legend = FALSE) + 17 | labs(x = "K (number of topics)", 18 | y = NULL) + 19 | facet_wrap(~Metric, scales = "free_y") + 20 | theme_pubr() 21 | 22 | } 23 | -------------------------------------------------------------------------------- /functions/add_US_location.R: -------------------------------------------------------------------------------- 1 | 2 | add_US_location <- function(df){ 3 | 4 | # US cities and states dictionary 5 | 6 | us_cities <- maps::us.cities$name %>% stringr::str_replace_all(" [[:alpha:]]*$", "") %>% unique() %>% tolower() 7 | 8 | us_states <- maps::us.cities$name %>% stringr::word(-1) %>% unique() %>% abbr2state() %>% tolower() 9 | 10 | us_country <- c("United States", "USA", "US", "U.S.A.") %>% tolower() 11 | 12 | us_location_list <- c(us_cities, us_states, us_country) 13 | 14 | # Other country dictionary 15 | 16 | countryname_dict <- unique(maps::world.cities$country.etc) 17 | 18 | countryname_dict <- countryname_dict[!countryname_dict %in% c("United States", "USA", "U.S.A.")] 19 | 20 | # Filter 21 | 22 | df[["US"]] <- str_detect(df[["location"]] %>% tolower(), us_location_list %>% paste(collapse = "|")) %>% as.numeric() 23 | 24 | df[["non_US"]] <- str_detect(df[["location"]] %>% tolower(), countryname_dict %>% tolower() %>% paste(collapse = "|")) %>% as.numeric() 25 | 26 | # Output 27 | 28 | df 29 | 30 | } 31 | -------------------------------------------------------------------------------- /code/App: -------------------------------------------------------------------------------- 1 | 2 | # load package 3 | pacman::p_load( 4 | tidyverse, # tidyverse 5 | wordcloud2, # Interactive wordcloud 6 | shiny, # for Shiny 7 | shinydashboard, # for Shiny dashboard 8 | colourpicker, # for better visual 9 | here) # for reproducibility 10 | 11 | # load data 12 | 13 | df <- read.csv(url("https://github.com/jaeyk/covid19antiasian/raw/master/processed_data/hash_counts.csv"))[,-1] 14 | 15 | ui <- fluidPage( 16 | 17 | h1("Word Cloud on the Hashtags of the Tweets related to COVID-19 & Asian|Chinese|Wuhan"), 18 | 19 | h4(tags$a(href = "https://jaeyk.github.io/", "Developer: Jae Yeon Kim")), 20 | 21 | mainPanel( 22 | 23 | wordcloud2Output("cloud"), 24 | 25 | ) 26 | 27 | ) 28 | 29 | server <- function(input, output) { 30 | 31 | output$cloud <- renderWordcloud2({ 32 | 33 | wordcloud2(df, 34 | size = 2.5, 35 | color = "random-dark") 36 | 37 | }) 38 | 39 | } 40 | 41 | shinyApp(ui = ui, server = server) 42 | 43 | runGist(e859632d10d73b9d53a83a59ad0a7acb) -------------------------------------------------------------------------------- /App/app.R: -------------------------------------------------------------------------------- 1 | 2 | # load package 3 | 4 | require("wordcloud2") 5 | require("vroom") 6 | require("shiny") 7 | require("shinydashboard") 8 | require("colourpicker") 9 | 10 | # load data 11 | 12 | df <- vroom::vroom(url("https://github.com/jaeyk/covid19antiasian/raw/master/processed_data/hash_counts.csv"))[,-1] 13 | 14 | ui <- fluidPage( 15 | 16 | # Application title 17 | titlePanel("Word Cloud on the Hashtags of the Tweets related to COVID-19 & Asian|Chinese|Wuhan"), 18 | 19 | h4(tags$a(href = "https://jaeyk.github.io/", "Developer: Jae Yeon Kim")), 20 | 21 | sidebarLayout( 22 | 23 | # Sidebar with sliders 24 | sidebarPanel( 25 | sliderInput("size", 26 | "Font size:", 27 | min = 1, max = 10, 28 | value = 2) 29 | ), 30 | 31 | mainPanel( 32 | 33 | wordcloud2Output("cloud"), 34 | 35 | ) 36 | 37 | ) 38 | ) 39 | 40 | server <- function(input, output, session) { 41 | 42 | output$cloud <- renderWordcloud2({ 43 | 44 | wordcloud2(df, 45 | size = input$size, 46 | color = "random-dark") 47 | 48 | }) 49 | 50 | } 51 | 52 | shinyApp(ui = ui, server = server) 53 | 54 | -------------------------------------------------------------------------------- /code/01_sample.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "01_sample_data" 3 | author: "Jae Yeon Kim" 4 | date: "6/29/2020" 5 | output: html_document 6 | --- 7 | 8 | # Import libs and files 9 | 10 | ## Libs 11 | 12 | ```{r} 13 | 14 | pacman::p_load(data.table, # for fast data import 15 | tidyverse, # for tidyverse 16 | here) # for reproducibility 17 | 18 | ``` 19 | 20 | ## Files 21 | 22 | ```{r} 23 | 24 | cle <- data.table::fread(here("raw_data", "clean_language_en.tsv")) 25 | 26 | ``` 27 | 28 | # Sample Tweet IDs 29 | 30 | ## Create a stratifying variable 31 | 32 | ```{r} 33 | 34 | cle$month <- cle$V2 %>% 35 | str_replace_all("-", "") %>% 36 | str_replace_all(".{2}$", "") 37 | 38 | ``` 39 | 40 | ## Sample 41 | 42 | ```{r} 43 | 44 | # For reproducibility 45 | set.seed(1234) 46 | 47 | # Random sampling stratified by month 48 | sampled <- cle %>% 49 | group_by(month) %>% 50 | slice_sample(n = 1000000, 51 | replace = FALSE) 52 | 53 | ``` 54 | 55 | # Export 56 | 57 | ```{r} 58 | 59 | # dir.create("../processed_data") 60 | 61 | # Full data 62 | fwrite(sampled[-1,], here("processed_data", "sampled.tsv")) 63 | 64 | # Only Tweet IDs. This file will be used for hydrating. 65 | fwrite(sampled[-1,1], here("processed_data", "sampled1.tsv")) 66 | 67 | ``` 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /code/01_google_trends.R: -------------------------------------------------------------------------------- 1 | 2 | # Devtools library 3 | 4 | # Import libraries 5 | pacman::p_load(tidyverse, # Tidyverse 6 | gtrendsR, # Interface for retrieving data from the Google Search API 7 | here) # Reproducibility 8 | 9 | search_gtrends <- function(terms){ 10 | 11 | # Search 12 | trends <- gtrends(keyword = terms, 13 | geo = 'US', 14 | # match to the twitter data 15 | time = "2020-01-09 2020-06-21", 16 | low_search_volume = TRUE) 17 | 18 | # Transform list into a tibble 19 | results <- tibble(date = lubridate::ymd(trends$interest_over_time$date), 20 | hits = as.numeric(as.character(trends$interest_over_time$hits)), 21 | keywords = trends$interest_over_time$keyword) 22 | 23 | } 24 | 25 | ## Search and bind 26 | 27 | gtrends <- bind_rows(search_gtrends("'Racism' + 'Antiasian' + 'Racist' + 'hate crime'"), 28 | search_gtrends("'Chinese flu' + 'Chinese virus'"), 29 | search_gtrends("Wuhan virus"), 30 | search_gtrends("Kung Flu")) %>% 31 | mutate(date = lubridate::ymd(date)) 32 | 33 | ## Export 34 | write_csv(gtrends, here("processed_data", "gtrends.csv")) -------------------------------------------------------------------------------- /code/04_01_hashtags.R: -------------------------------------------------------------------------------- 1 | 2 | # load package 3 | pacman::p_load( 4 | tidyverse, # tidyverse 5 | stringr, # for string manipulation 6 | here, # for reproducibility 7 | wordcloud, # Static wordcloud 8 | wordcloud2, # Interactive wordcloud 9 | webshot, # Save jave script wordcloud 10 | htmlwidgets) # Save jave script wordcloud 11 | 12 | # load data 13 | US_tweets <- read.csv(here("processed_data", "US_tweets.csv")) 14 | 15 | # Extract hashtags 16 | US_tweets$hashtags <- str_extract(US_tweets$full_text, "#\\S+") 17 | 18 | # Remove too common hashtags (I defined them as stop_hashs) 19 | US_tweets$stop_hashs <- str_detect(US_tweets$hashtags, "covid|corona") 20 | 21 | US_tweets <- US_tweets %>% 22 | filter(stop_hashs == 0) %>% 23 | filter(wuhan == 1 | asian == 1 | chinese == 1) 24 | 25 | # Count 26 | hash_counts <- US_tweets %>% 27 | mutate(hashtags = str_replace(hashtags, "#", "")) %>% 28 | mutate(hashtags = tm::removePunctuation(hashtags)) %>% 29 | count(hashtags, sort = TRUE) %>% 30 | filter(hashtags != "china" & hashtags != "wuhan" ) 31 | 32 | # Export data 33 | write.csv(hash_counts, here("processed_data", "hash_counts.csv")) 34 | 35 | # Produce word cloud 36 | hash_cloud <- wordcloud2(hash_counts, size = 2.5, 37 | color = "random-dark") 38 | 39 | hash_cloud 40 | 41 | # Save in HTML 42 | htmlwidgets::saveWidget(hash_cloud, here("outputs", "hash_cloud.html"), selfcontained = FALSE) 43 | 44 | # Save in PNG 45 | webshot(here("outputs", "hash_cloud.html"), 46 | here("outputs", "hash_cloud.png"), 47 | delay =5, 48 | vwidth = 600, 49 | vheight= 600) 50 | -------------------------------------------------------------------------------- /outputs/hash_cloud_files/wordcloud2-binding-0.2.1/wordcloud2.js: -------------------------------------------------------------------------------- 1 | HTMLWidgets.widget({ 2 | 3 | name: 'wordcloud2', 4 | 5 | type: 'output', 6 | 7 | initialize: function(el, width, height) { 8 | var newCanvas = document.createElement("canvas"); 9 | newCanvas.height = height; 10 | newCanvas.width = width; 11 | newCanvas.id = "canvas"; 12 | 13 | el.appendChild(newCanvas); 14 | newlabel(el); 15 | return(el.firstChild); 16 | }, 17 | renderValue: function(el, x, instance) { 18 | // parse gexf data 19 | listData=[]; 20 | for(var i=0; i tweet_ids` 26 | 27 | **Replication code** 28 | 29 | * [00_setup.sh](https://github.com/jaeyk/covid19antiasian/blob/master/code/00_setup.sh): Shell script for collecting Tweets and their related metadata based on Tweet IDs 30 | 31 | * [01_google_trends.r](https://github.com/jaeyk/covid19antiasian/blob/master/code/01_google_trends.R): R script for collecting Google search API data 32 | 33 | * [01_sample.Rmd](https://github.com/jaeyk/covid19antiasian/blob/master/code/01_sample.Rmd): R markdown file for sampling Twitter data 34 | 35 | * [02_parse.r](https://github.com/jaeyk/covid19antiasian/blob/master/code/02_parse.r): R script for parsing Twitter data. This script produced a cleaned and wrangled data named 'parsed.rds.' This file is not included in this repository to not violate Twitter's Developer Terms. Also, its file size is quite large (**1.4 GB**). 36 | 37 | ## Descriptive analysis 38 | 39 | **Replication code** 40 | 41 | * [03_explore.Rmd](https://github.com/jaeyk/covid19antiasian/blob/master/code/03_explore.Rmd): R markdown file for further wrangling and exploring data. This file creates **Figure 2.** (overall_trend.png) 42 | 43 | * [04_01_hashtags.R](https://github.com/jaeyk/covid19antiasian/blob/master/code/04_01_hashtags.R): R script file for creating a wordlcoud of hashtags. This file creates **Figure 1.** (hash_cloud.png) 44 | 45 | * [04_clean.ipynb](https://github.com/jaeyk/covid19antiasian/blob/master/code/04_clean.ipynb): Python notebook for cleaning texts 46 | 47 | ## Topic modeling 48 | 49 | **Replication code** 50 | 51 | * [05_topic_modeling.Rmd](https://github.com/jaeyk/covid19antiasian/blob/master/code/05_topic_modeling.Rmd): R markdown for topic modeling analysis. This file creates **Figure 3** (dynamic_topic_day.png) 52 | -------------------------------------------------------------------------------- /outputs/hash_cloud_files/wordcloud2-0.0.1/hover.js: -------------------------------------------------------------------------------- 1 | // create element 2 | function newlabel(el){ 3 | var newDiv = document.createElement("div"); 4 | var newSpan = document.createElement("span"); 5 | newDiv.id = 'wcLabel'; 6 | newSpan.id = "wcSpan"; 7 | el.appendChild(newDiv); 8 | document.getElementById("wcLabel").appendChild(newSpan); 9 | } 10 | 11 | // hover function 12 | function cv_handleHover(item, dimension, evt) { 13 | var el = document.getElementById("wcLabel"); 14 | if (!item) { 15 | el.setAttribute('hidden', true); 16 | 17 | return; 18 | } 19 | 20 | el.removeAttribute('hidden'); 21 | // console.log(evt.srcElement.offsetLeft); 22 | 23 | el.style.left = dimension.x + evt.srcElement.offsetLeft + 'px'; 24 | el.style.top = dimension.y + evt.srcElement.offsetTop + 'px'; 25 | el.style.width = dimension.w + 'px'; 26 | el.style.height = dimension.h + 'px'; 27 | 28 | this.hoverDimension = dimension; 29 | 30 | document.getElementById("wcSpan").setAttribute( 31 | 'data-l10n-args', JSON.stringify({ word: item[0], count: item[1] })); 32 | document.getElementById("wcSpan").innerHTML =item[0]+":" + item[1]; 33 | 34 | } 35 | 36 | function updateCanvasMask(maskCanvas) { 37 | var ctx = maskCanvas.getContext('2d'); 38 | var imageData = ctx.getImageData( 39 | 0, 0, maskCanvas.width, maskCanvas.height); 40 | var newImageData = ctx.createImageData(imageData); 41 | 42 | var toneSum = 0; 43 | var toneCnt = 0; 44 | for (var i = 0; i < imageData.data.length; i += 4) { 45 | var alpha = imageData.data[i + 3]; 46 | if (alpha > 128) { 47 | var tone = imageData.data[i] 48 | + imageData.data[i + 1] 49 | + imageData.data[i + 2]; 50 | toneSum += tone; 51 | ++toneCnt; 52 | } 53 | } 54 | var threshold = toneSum / toneCnt; 55 | 56 | for (var i = 0; i < imageData.data.length; i += 4) { 57 | var tone = imageData.data[i] 58 | + imageData.data[i + 1] 59 | + imageData.data[i + 2]; 60 | var alpha = imageData.data[i + 3]; 61 | 62 | if (alpha < 128 || tone > threshold) { 63 | // Area not to draw 64 | newImageData.data[i] = 0; 65 | newImageData.data[i + 1] = 0; 66 | newImageData.data[i + 2] = 0; 67 | newImageData.data[i + 3] = 0; 68 | } 69 | else { 70 | // Area to draw 71 | // The color must be same with backgroundColor 72 | newImageData.data[i] = 255; 73 | newImageData.data[i + 1] = 255; 74 | newImageData.data[i + 2] = 255; 75 | newImageData.data[i + 3] = 255; 76 | } 77 | } 78 | 79 | ctx.putImageData(newImageData, 0, 0); 80 | console.log(maskCanvas.toDataURL()); 81 | } 82 | 83 | 84 | 85 | //mask function 86 | function maskInit(el,x){ 87 | console.log(1) 88 | str = x.figBase64; 89 | //console.log(str) 90 | var newImg = new Image(); 91 | newImg.src = str; 92 | newImg.style.position = 'absolute'; 93 | newImg.style.left = 0; 94 | // console.log(el.clientHeight); 95 | newImg.width = el.clientWidth; 96 | newImg.height = el.clientHeight; 97 | // maskCanvas = init(el, x, newImg); 98 | vvalue = 128 99 | 100 | ctx = el.firstChild.getContext('2d'); 101 | 102 | ctx.drawImage(newImg, 0, 0, canvas.width, canvas.height); 103 | updateCanvasMask(ctx); 104 | 105 | 106 | 107 | 108 | 109 | WordCloud(el.firstChild, { list: listData, 110 | fontFamily: x.fontFamily, 111 | fontWeight: x.fontWeight, 112 | color: x.color, 113 | minSize: x.minSize, 114 | weightFactor: x.weightFactor, 115 | backgroundColor: x.backgroundColor, 116 | gridSize: x.gridSize, 117 | minRotation: x.minRotation, 118 | maxRotation: x.maxRotation, 119 | shuffle: x.shuffle, 120 | shape: x.shape, 121 | rotateRatio: x.rotateRatio, 122 | ellipticity: x.ellipticity, 123 | clearCanvas: false, 124 | drawMask: true, 125 | hover: x.hover || cv_handleHover, 126 | abortThreshold: 3000 127 | }); 128 | } 129 | 130 | -------------------------------------------------------------------------------- /code/.ipynb_checkpoints/04_clean-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Preprocessing text \n" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Import libraries " 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 2, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "# Basic libs \n", 24 | "import pandas as pd\n", 25 | "import string\n", 26 | "\n", 27 | "import re\n", 28 | "\n", 29 | "# Custom functions\n", 30 | "from clean_text import clean_tweet" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "# Import files " 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "# Import CSV file \n", 47 | "\n", 48 | "df = pd.read_csv('/home/jae/hateasiancovid/processed_data/text_ready.csv',\n", 49 | " usecols = ['full_text', 'wuhan', 'asian', 'chinese', 'date', 'trump'],\n", 50 | " parse_dates = ['date'])\n" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 3, 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "data": { 60 | "text/plain": [ 61 | "(1550822, 6)" 62 | ] 63 | }, 64 | "execution_count": 3, 65 | "metadata": {}, 66 | "output_type": "execute_result" 67 | } 68 | ], 69 | "source": [ 70 | "# 1,550,822 obs, 5 columns \n", 71 | "\n", 72 | "df.shape" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "# Clean text " 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 4, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "# Copy\n", 89 | "df_clean = df.copy()\n", 90 | "\n", 91 | "# Clean\n", 92 | "df_clean['full_text'] = clean_tweet(df_clean['full_text'])\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 5, 98 | "metadata": {}, 99 | "outputs": [ 100 | { 101 | "data": { 102 | "text/html": [ 103 | "
\n", 104 | "\n", 117 | "\n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | "
full_textwuhanasianchinesedatetrump
0i just can wait for the song to drop feb how f...0002020-01-280
1my step dad got pneumonia and said he got the ...0002020-01-310
2coronavirus 133 dead out of 6091 infected 92...0002020-01-290
3thank you for helping to get information to d...0002020-01-310
4them asu students dirty af mfs got multiple s...0002020-01-290
\n", 177 | "
" 178 | ], 179 | "text/plain": [ 180 | " full_text wuhan asian chinese \\\n", 181 | "0 i just can wait for the song to drop feb how f... 0 0 0 \n", 182 | "1 my step dad got pneumonia and said he got the ... 0 0 0 \n", 183 | "2 coronavirus 133 dead out of 6091 infected 92... 0 0 0 \n", 184 | "3 thank you for helping to get information to d... 0 0 0 \n", 185 | "4 them asu students dirty af mfs got multiple s... 0 0 0 \n", 186 | "\n", 187 | " date trump \n", 188 | "0 2020-01-28 0 \n", 189 | "1 2020-01-31 0 \n", 190 | "2 2020-01-29 0 \n", 191 | "3 2020-01-31 0 \n", 192 | "4 2020-01-29 0 " 193 | ] 194 | }, 195 | "execution_count": 5, 196 | "metadata": {}, 197 | "output_type": "execute_result" 198 | } 199 | ], 200 | "source": [ 201 | "# Check \n", 202 | "df_clean.head()" 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "metadata": {}, 208 | "source": [ 209 | "# Export " 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 6, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "df_clean.to_csv(\"/home/jae/hateasiancovid/processed_data/cleaned_data.csv\")" 219 | ] 220 | } 221 | ], 222 | "metadata": { 223 | "kernelspec": { 224 | "display_name": "Python 3", 225 | "language": "python", 226 | "name": "python3" 227 | }, 228 | "language_info": { 229 | "codemirror_mode": { 230 | "name": "ipython", 231 | "version": 3 232 | }, 233 | "file_extension": ".py", 234 | "mimetype": "text/x-python", 235 | "name": "python", 236 | "nbconvert_exporter": "python", 237 | "pygments_lexer": "ipython3", 238 | "version": "3.7.4" 239 | }, 240 | "toc": { 241 | "base_numbering": 1, 242 | "nav_menu": {}, 243 | "number_sections": true, 244 | "sideBar": true, 245 | "skip_h1_title": false, 246 | "title_cell": "Table of Contents", 247 | "title_sidebar": "Contents", 248 | "toc_cell": false, 249 | "toc_position": {}, 250 | "toc_section_display": true, 251 | "toc_window_display": false 252 | } 253 | }, 254 | "nbformat": 4, 255 | "nbformat_minor": 4 256 | } 257 | -------------------------------------------------------------------------------- /code/03_explore.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "03_preprocess_data" 3 | author: "Jae Yeon Kim" 4 | date: "7/2/2020" 5 | output: html_document 6 | --- 7 | 8 | # Import packages and files 9 | 10 | ## Packages 11 | 12 | ```{r} 13 | pacman::p_load(data.table, # for fast data manipulation 14 | tidyverse, # for tidyverse 15 | ggpubr, # for arranging ggplots 16 | ggthemes, # for fancy ggplot themes 17 | here, # for reproducibility 18 | maps, # for US city data 19 | openintro, # for revert state name abbreviations to their original forms 20 | stringr, # for easy regular expression 21 | lubridate, # for easy time var manipulation 22 | forcats, # reverse factor order 23 | patchwork, # for easy ggarrange 24 | ggsci, # for pubs 25 | gganimate) # for animated data viz 26 | 27 | # devtools::install_github("jaeyk/tidytweetjson", force = TRUE) 28 | # devtools::install_github("jaeyk/makereproducible", force = TRUE) 29 | 30 | library(tidytweetjson) 31 | library(makereproducible) 32 | 33 | # for publication-friendly theme 34 | theme_set(theme_pubr()) 35 | 36 | # custom functions 37 | source(here("functions", "stacked_area_plot.R")) 38 | source(here("functions", "add_normalized.R")) 39 | 40 | ``` 41 | 42 | ## Files 43 | 44 | - 5,050,042 obs 45 | 46 | ```{r} 47 | parsed <- readRDS(here("processed_data", "parsed.rds")) 48 | ``` 49 | 50 | # Wrangle 51 | 52 | ## Filter 53 | 54 | - Filtered if location field is empty or NA: 3,734,756 obs (73% of the original data) 55 | - I did not use country_code field as it is mostly filled with NAs (97%). 56 | 57 | ```{r} 58 | sum(is.na(parsed$country_code))/nrow(parsed) 59 | filtered <- parsed %>% 60 | filter(!(location == "" | is.na(location))) 61 | ``` 62 | 63 | ## Select 64 | 65 | ```{r} 66 | # Select columns 67 | selected <- filtered %>% 68 | select(created_at, full_text, location) 69 | ``` 70 | 71 | ## Mutate 72 | 73 | ### Location 74 | 75 | - 37% of the Tweets (1,394,468) were created by the users located in the US. 76 | 77 | ```{r} 78 | # Add two columns 79 | # US: located in the US identified by the names of the US states and cities 80 | # non_US: located not in the US identified by the names of the non-US countries 81 | df <- add_US_location(selected) 82 | df <- subset(df, US_location == 1) 83 | # nrow(df)/nrow(selected) 84 | ``` 85 | 86 | ### Text 87 | 88 | ```{r} 89 | # Create new columns 90 | df <- df %>% 91 | mutate(full_text = str_to_lower(full_text), 92 | wuhan = as.numeric(str_detect(full_text, "wuhan")), 93 | asian = as.numeric(str_detect(full_text, "asia|asian")), 94 | chinese = as.numeric(str_detect(full_text, "china|chinese")), 95 | trump = as.numeric(str_detect(full_text, "trump")), 96 | chinese_virus = as.numeric(str_detect(full_text, "chinese flu|chineseflu|chinese virus|chinesevirus")), 97 | kung_flu = as.numeric(str_detect(full_text, "kung flu|kungflu")), 98 | wuhan_virus = as.numeric(str_detect(full_text, "wuhan virus|wuhanvirus")), 99 | anti_racism = as.numeric(str_detect(full_text, "antiracism|antiasian|stophate|acttochange|stopaapihate|stophatecrimes|racism|racist|hatecrime")), 100 | racism = ifelse(chinese_virus == 1 | kung_flu == 1 | wuhan_virus == 1, 1, 0) 101 | ) 102 | 103 | ``` 104 | 105 | ### Date 106 | 107 | ```{r} 108 | df <- tidytweetjson::add_date(df) 109 | #df[1:10,] %>% select(created_at, date) 110 | 111 | fwrite(df, here("processed_data", "US_tweets.csv")) 112 | ``` 113 | 114 | ```{r} 115 | df <- fread(here("processed_data", "US_tweets.csv")) 116 | ``` 117 | 118 | # Explore 119 | 120 | ## Time-series 121 | 122 | ### Overall 123 | 124 | ```{r} 125 | # Google 126 | 127 | gtrends <- read_csv(here("processed_data", "gtrends.csv")) 128 | 129 | gtrends <- gtrends %>% 130 | mutate(keywords = recode(keywords, "Kung Flu" = "Kung flu")) %>% 131 | mutate(keywords = str_replace(keywords, ".*Chinese.*", "Chinese virus"), 132 | keywords = str_replace(keywords, ".*Racism.*", "Anti-racism")) %>% 133 | rename(Terms = keywords) 134 | 135 | unique(gtrends$Terms) 136 | ``` 137 | 138 | ```{r} 139 | 140 | gtrends_plot <- gtrends %>% 141 | ggplot(aes(x = as.Date(date), y = hits)) + 142 | geom_line(size = 1.2) + 143 | labs(x = "Date", 144 | y = "Count", 145 | title = "Google Searches on COVID-19 (In the US)", 146 | subtitle = "Normalized to a 0-100 range") + 147 | geom_vline(xintercept = as.Date(c("2020-03-16")), 148 | linetype = "dashed", 149 | size = 1.2, 150 | color = "blue") + 151 | facet_wrap(~Terms) 152 | ``` 153 | 154 | ```{r} 155 | animated_gtrends_plot <- gtrends %>% 156 | ggplot(aes(x = as.Date(date), y = hits)) + 157 | geom_line(size = 1.2) + 158 | labs(x = "Date", 159 | y = "Count", 160 | title = "Google Searches on COVID-19 (In the US)", 161 | subtitle = "Normalized to a 0-100 range") + 162 | geom_vline(xintercept = as.Date(c("2020-03-16")), 163 | linetype = "dashed", 164 | size = 1.2, 165 | color = "blue") + 166 | facet_wrap(~Terms) + 167 | transition_reveal(date) 168 | 169 | install.packages("gifski") 170 | 171 | animate(animated_gtrends_plot, duration = 5, fps = 20, width = 200, height = 200, renderer = gifski_renderer()) 172 | 173 | anim_save(here("outputs", "animated_gtrends_plot.gif")) 174 | ``` 175 | 176 | ```{r} 177 | # Twitter 178 | ## Group by date then visualize 179 | 180 | twitter <- bind_rows( 181 | mutate(add_normalized(df, chinese_virus), Terms = "Chinese virus"), 182 | mutate(add_normalized(df, kung_flu), Terms = "Kung flu"), 183 | mutate(add_normalized(df, wuhan_virus), Terms = "Wuhan virus"), 184 | mutate(add_normalized(df, anti_racism), Terms = "Anti-racism") 185 | ) 186 | 187 | twitter_plot <- twitter %>% 188 | ggplot(aes(x = as.Date(date), y = rescaled)) + 189 | geom_line(size = 1.2) + 190 | labs(x = "Date", 191 | y = "Count", 192 | title = "Tweets on COVID-19 (in the US)", 193 | subtitle = "Normalized to a 0-100 range") + 194 | geom_vline(xintercept = as.Date(c("2020-03-16")), 195 | linetype = "dashed", 196 | size = 1.2, 197 | color = "blue") + 198 | facet_wrap(~Terms) 199 | ``` 200 | 201 | ```{r} 202 | twitter_plot / gtrends_plot 203 | ggsave(here("outputs", "overall_trend.png"), height = 10, width = 10) 204 | ``` 205 | 206 | ### Keywords 207 | 208 | ```{r} 209 | # df <- data.table::fread(here("processed_data", "text_ready.csv")) 210 | 211 | p1 <- stacked_area_plot(df, wuhan) + 212 | labs(x = "Date", 213 | y = "Proportion", 214 | fill = "Wuhan", 215 | title = "Wuhan", 216 | subtitle = "Tweets created by the users located in the US") 217 | 218 | p2 <- stacked_area_plot(df, chinese) + 219 | labs(x = "Date", 220 | y = "Proportion", 221 | fill = "Chinese", 222 | title = "Chinese", 223 | subtitle = "Tweets created by the users located in the US") 224 | 225 | p3 <- stacked_area_plot(df, asian) + 226 | labs(x = "Date", 227 | y = "Proportion", 228 | fill = "Asian", 229 | title = "Asian", 230 | subtitle = "Tweets created by the users located in the US") 231 | 232 | p4 <- stacked_area_plot(df, chinese_virus) + 233 | labs(x = "Date", 234 | y = "Proportion", 235 | fill = "Chinese virus", 236 | title = "Chinese virus", 237 | subtitle = "Tweets created by the users located in the US") 238 | 239 | p5 <- stacked_area_plot(df, kung_flu) + 240 | labs(x = "Date", 241 | y = "Proportion", 242 | fill = "Kung flu", 243 | title = "Kung flu", 244 | subtitle = "Tweets created by the users located in the US") 245 | 246 | p6 <- stacked_area_plot(df, wuhan_virus) + 247 | labs(x = "Date", 248 | y = "Proportion", 249 | fill = "Wuhan virus", 250 | title = "Wuhan virus", 251 | subtitle = "Tweets created by the users located in the US") 252 | 253 | (p1 + p2) / (p3 + p4) / (p5 + p6) 254 | 255 | ggsave(here("outputs", "stacked_bar_plots2.png"), 256 | height = 10, width = 9) 257 | 258 | df$virus <- ifelse(df$wuhan_virus == 1 | df$chinese_virus == 1 | df$kung_flu == 1, 1, 0) 259 | 260 | sum(df$virus)/nrow(df) 261 | 262 | 263 | (sum(df$asia)/nrow(df))/(sum(df$virus)/nrow(df)) 264 | ``` 265 | # Export 266 | 267 | ```{r} 268 | fwrite(df, here("processed_data", "text_ready.csv")) 269 | ``` -------------------------------------------------------------------------------- /code/04_clean.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Preprocessing text \n" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Import libraries " 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 3, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "# Basic libs \n", 24 | "import pandas as pd\n", 25 | "import string\n", 26 | "\n", 27 | "import re\n", 28 | "\n", 29 | "# Custom functions\n", 30 | "from clean_text import clean_tweet" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "# Import files " 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 4, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "# Import CSV file \n", 47 | "\n", 48 | "df = pd.read_csv('/home/jae/hateasiancovid/processed_data/US_tweets.csv',\n", 49 | " parse_dates = ['date'])\n" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 5, 55 | "metadata": {}, 56 | "outputs": [ 57 | { 58 | "data": { 59 | "text/plain": [ 60 | "(1394478, 16)" 61 | ] 62 | }, 63 | "execution_count": 5, 64 | "metadata": {}, 65 | "output_type": "execute_result" 66 | } 67 | ], 68 | "source": [ 69 | "# 1,394,478 obs, 16 columns \n", 70 | "\n", 71 | "df.shape" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "# Clean text " 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 6, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "# Copy\n", 88 | "df_clean = df.copy()\n", 89 | "\n", 90 | "# Clean\n", 91 | "df_clean['full_text'] = clean_tweet(df_clean['full_text'])\n" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 7, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "data": { 101 | "text/html": [ 102 | "
\n", 103 | "\n", 116 | "\n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | "
created_atfull_textlocationUSnon_USwuhanasianchinesetrumpdateanti_asianchinese_viruskung_fluwuhan_virusanti_racismracism
0Fri Jan 31 19:33:48 +0000 2020my step dad got pneumonia and said he got the ...Los Angeles, CA1000002020-01-31000000
1Wed Jan 29 17:02:34 +0000 2020coronavirus 133 dead out of 6091 infected 92...San Jose, CA1000002020-01-29000000
2Fri Jan 31 14:21:47 +0000 2020thank you for helping to get information to d...Delaware1000002020-01-31000000
3Wed Jan 29 21:57:23 +0000 2020them asu students dirty af mfs got multiple s...United States1000002020-01-29000000
4Tue Jan 28 14:00:27 +0000 2020lord protect every one from the coronavirusWashington, DC1000002020-01-28000000
\n", 236 | "
" 237 | ], 238 | "text/plain": [ 239 | " created_at \\\n", 240 | "0 Fri Jan 31 19:33:48 +0000 2020 \n", 241 | "1 Wed Jan 29 17:02:34 +0000 2020 \n", 242 | "2 Fri Jan 31 14:21:47 +0000 2020 \n", 243 | "3 Wed Jan 29 21:57:23 +0000 2020 \n", 244 | "4 Tue Jan 28 14:00:27 +0000 2020 \n", 245 | "\n", 246 | " full_text location US \\\n", 247 | "0 my step dad got pneumonia and said he got the ... Los Angeles, CA 1 \n", 248 | "1 coronavirus 133 dead out of 6091 infected 92... San Jose, CA 1 \n", 249 | "2 thank you for helping to get information to d... Delaware 1 \n", 250 | "3 them asu students dirty af mfs got multiple s... United States 1 \n", 251 | "4 lord protect every one from the coronavirus Washington, DC 1 \n", 252 | "\n", 253 | " non_US wuhan asian chinese trump date anti_asian chinese_virus \\\n", 254 | "0 0 0 0 0 0 2020-01-31 0 0 \n", 255 | "1 0 0 0 0 0 2020-01-29 0 0 \n", 256 | "2 0 0 0 0 0 2020-01-31 0 0 \n", 257 | "3 0 0 0 0 0 2020-01-29 0 0 \n", 258 | "4 0 0 0 0 0 2020-01-28 0 0 \n", 259 | "\n", 260 | " kung_flu wuhan_virus anti_racism racism \n", 261 | "0 0 0 0 0 \n", 262 | "1 0 0 0 0 \n", 263 | "2 0 0 0 0 \n", 264 | "3 0 0 0 0 \n", 265 | "4 0 0 0 0 " 266 | ] 267 | }, 268 | "execution_count": 7, 269 | "metadata": {}, 270 | "output_type": "execute_result" 271 | } 272 | ], 273 | "source": [ 274 | "# Check \n", 275 | "df_clean.head()" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "metadata": {}, 281 | "source": [ 282 | "# Export " 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": 9, 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "df_clean = df_clean.drop(columns = ['created_at', 'location'])" 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": 10, 297 | "metadata": {}, 298 | "outputs": [], 299 | "source": [ 300 | "df_clean.to_csv(\"/home/jae/hateasiancovid/processed_data/cleaned_data.csv\")" 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": null, 306 | "metadata": {}, 307 | "outputs": [], 308 | "source": [] 309 | } 310 | ], 311 | "metadata": { 312 | "kernelspec": { 313 | "display_name": "Python 3", 314 | "language": "python", 315 | "name": "python3" 316 | }, 317 | "language_info": { 318 | "codemirror_mode": { 319 | "name": "ipython", 320 | "version": 3 321 | }, 322 | "file_extension": ".py", 323 | "mimetype": "text/x-python", 324 | "name": "python", 325 | "nbconvert_exporter": "python", 326 | "pygments_lexer": "ipython3", 327 | "version": "3.7.4" 328 | }, 329 | "toc": { 330 | "base_numbering": 1, 331 | "nav_menu": {}, 332 | "number_sections": true, 333 | "sideBar": true, 334 | "skip_h1_title": false, 335 | "title_cell": "Table of Contents", 336 | "title_sidebar": "Contents", 337 | "toc_cell": false, 338 | "toc_position": {}, 339 | "toc_section_display": true, 340 | "toc_window_display": false 341 | } 342 | }, 343 | "nbformat": 4, 344 | "nbformat_minor": 4 345 | } 346 | -------------------------------------------------------------------------------- /code/05_topic_modeling.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "03_preprocess_data" 3 | author: "Jae Yeon Kim" 4 | date: "7/2/2020" 5 | output: html_document 6 | --- 7 | 8 | # Import packages and files 9 | 10 | ## Packages 11 | 12 | ```{r} 13 | 14 | pacman::p_load(tidyverse, # for tidyverse 15 | ggpubr, # for arranging ggplots 16 | ggthemes, # for fancy ggplot themes 17 | here, # for reproducibility 18 | patchwork, # for easy ggarrange 19 | ggsci, # for pubs 20 | fastDummies, # to create dummy variables fast 21 | readtext, # for reading text 22 | quanteda, # for text preprocessing 23 | data.table, # for fast data manipulation 24 | stm, # for structural topic modeling 25 | future, # for parallel and distributed computing 26 | purrr, # for functional programming 27 | keyATM, # keyATM 28 | latex2exp) 29 | 30 | # for publication-friendly theme 31 | theme_set(theme_pubr()) 32 | 33 | # custom functions 34 | source(here("functions", "stacked_bar_plot.R")) 35 | source(here("functions", "visualize_diag.R")) 36 | source(here("functions", "date2index.R")) 37 | 38 | # For keyword based topic modeling (development version) 39 | devtools::install_github("keyATM/keyATM", ref = "package_dev") 40 | 41 | library(keyATM) 42 | ``` 43 | 44 | ## Files 45 | 46 | - Filter 90% of the data (by `chinese` and `wuhan` variables) 47 | 48 | ```{r} 49 | 50 | # Subset 51 | df <- data.table::fread(here("processed_data", "cleaned_data.csv"))[,-1] 52 | 53 | small_df <- df %>% filter(chinese == 1 | wuhan == 1 | asian == 1) 54 | 55 | 1 - nrow(small_df)/nrow(df) 56 | 57 | # Filter (doc length 0 ones) 58 | 59 | small_df <- small_df[-c(2930, 25557, 33569, 49217, 49784, 51572, 71270, 89453, 90285, 102360, 108111, 110351, 113704, 116119, 120643, 121220, 122300, 122695, 125647, 133053),] 60 | 61 | # Add intervention variable 62 | small_df$intervention <- ifelse(small_df$date >= "2020-03-16", 1, 0) 63 | ``` 64 | 65 | # Preprocess 66 | 67 | ```{r} 68 | 69 | # Build a corpus 70 | my_corpus <- corpus(small_df$full_text) 71 | 72 | # Add the document-level covariates 73 | docvars(my_corpus, "wuhan") <- small_df$wuhan 74 | 75 | docvars(my_corpus, "asian") <- small_df$asian 76 | 77 | docvars(my_corpus, 78 | "chinese") <- small_df$chinese 79 | 80 | docvars(my_corpus, 81 | "trump") <- small_df$trump %>% as.factor() 82 | 83 | docvars(my_corpus, 84 | "intervention") <- small_df$intervention %>% as.factor() 85 | 86 | # Date 87 | 88 | docvars(my_corpus, 89 | "date") <- small_df$date 90 | 91 | # Month 92 | 93 | docvars(my_corpus, 94 | "month") <- lubridate::month(small_df$date) 95 | 96 | # Date into index 97 | 98 | docvars(my_corpus) <- date2index(my_corpus) 99 | 100 | write_rds(my_corpus, here("outputs", "my_corpus.rds")) 101 | 102 | my_corpus <- read_rds(here("outputs", "my_corpus.rds")) 103 | 104 | ``` 105 | 106 | ```{r} 107 | # Tokenize 108 | data_tokens <- tokens(my_corpus, 109 | remove_url = TRUE) %>% 110 | tokens_remove(c(stopwords("english"), 111 | "may", "shall", "can", 112 | "must", "upon", "with", "without", 113 | "covid", "covid19", "covid-19")) 114 | 115 | ``` 116 | 117 | # Document-term matrix 118 | 119 | ```{r} 120 | # Construct a document-term matrix 121 | 122 | data_dfm <- dfm(data_tokens) %>% 123 | dfm_trim(min_termfreq = 100, 124 | min_docfreq = 100) 125 | 126 | ``` 127 | 128 | 129 | ```{r} 130 | write_rds(data_dfm, here("processed_data", "data_dfm.rds")) 131 | 132 | data_dfm <- read_rds(here("processed_data", "data_dfm.rds")) 133 | ``` 134 | 135 | # KeyATM 136 | 137 | ## Prepare the data 138 | 139 | ```{r} 140 | # Prepare the data for keyATM 141 | 142 | future::plan("multiprocess") 143 | 144 | tictoc::tic() 145 | keyATM_docs <- keyATM_read(texts = data_dfm) 146 | tictoc::toc() 147 | 148 | # 243.578 sec elapsed 149 | 150 | # Export 151 | write_rds(keyATM_docs, here("processed_data", 152 | "keyATM_docs.rds")) 153 | 154 | keyATM_docs <- read_rds(here("processed_data", "keyATM_docs.rds")) 155 | 156 | ``` 157 | 158 | ## Create a dictionary of the key words 159 | 160 | ```{r} 161 | 162 | keywords <- list( 163 | 164 | "Anti-Asian" = c("wuhanvirus", "chinesevirus", "chinavirus", "wuhancoronavirus", "wuhanpneumonia", "ccpvirus", "chinaliedpeopledied"), 165 | 166 | "Anti-racism" = c("antiracism", "stophate", "acttochange", "stopaapihate", "stophatecrimes", "antiasian", "racism", "racist") 167 | 168 | ) 169 | 170 | ``` 171 | 172 | 173 | ## Check keywords 174 | 175 | ```{r} 176 | key_viz <- visualize_keywords(docs = keyATM_docs, 177 | keywords = keywords) 178 | 179 | save_fig(key_viz, here("outputs", "keyword.png")) 180 | 181 | vf <- values_fig(key_viz) 182 | 183 | key_viz 184 | ``` 185 | 186 | ## Number of K 187 | 188 | ```{r} 189 | 190 | # future::plan(multiprocess) 191 | 192 | # Run many models 193 | many_models <- tibble(K = c(3:5)) %>% 194 | mutate(topic_model = furrr::future_map(K, ~stm(data_dfm, 195 | K = ., 196 | verbose = TRUE))) 197 | 198 | write_rds(many_models, here("outputs", "many_models.rds")) 199 | 200 | many_models <- read_rds(here("outputs", "many_models.rds")) 201 | 202 | ``` 203 | 204 | ```{r} 205 | 206 | # Resolve conflicts 207 | 208 | conflicted::conflict_prefer("purrr", "map") 209 | 210 | k_search_diag <- visualize_diag(data_dfm, many_models) 211 | 212 | ggsave(here("outputs", "k_search_diag.png")) 213 | 214 | ``` 215 | 216 | ## Static topic modeling 217 | 218 | ```{r} 219 | 220 | future::plan("multiprocess") 221 | 222 | out <- keyATM(docs = keyATM_docs, # text input 223 | no_keyword_topics = 1, # number of topics without keywords 224 | keywords = keywords, # keywords 225 | model = "base", # select the model 226 | options = list(seed = 250, 227 | store_theta = TRUE)) 228 | 229 | write_rds(out, here("outputs", "keyATM_out.rds")) 230 | 231 | out <- read_rds(here("outputs", "keyATM_out.rds")) 232 | 233 | # theta = document-topic distribution 234 | out$theta <- round(out$theta, 0) 235 | 236 | # sum 237 | sums <- c(sum(out$theta[,1]), sum(out$theta[,2]), sum(out$theta[,3])) 238 | 239 | ``` 240 | 241 | ```{r} 242 | topic_out <- tibble(topic_sums = sums, 243 | names = c("Anti-Asian", "Anti-racism","Others")) %>% 244 | mutate(prop = topic_sums / sum(topic_sums), 245 | prop = round(prop,2)) 246 | 247 | topic_out %>% 248 | ggplot(aes(x = names, y = prop)) + 249 | geom_col(position = "dodge") + 250 | scale_y_continuous(labels = 251 | scales::percent_format(accuracy = 1)) + 252 | labs(x = "Topic name", 253 | y = "Topic proportion", 254 | title = "Topic-document distributions", 255 | subtitle = "Tweets mentioned COVID-19 and either Asian, Chinese, or Wuhan related words") 256 | 257 | ggsave(here("outputs", "topic_modeling_static.png")) 258 | 259 | ``` 260 | 261 | ## Covariate topic modeling 262 | 263 | ```{r} 264 | 265 | # Extract covariates 266 | vars <- docvars(my_corpus) 267 | 268 | vars_selected <- vars %>% select(intervention) %>% 269 | mutate(intervention = ifelse(intervention == 1, "Post-Trump speech", "Pre-Trump speech")) 270 | 271 | # Topic modeling 272 | covariate_out <- keyATM(docs = keyATM_docs, # text input 273 | no_keyword_topics = 1, # number of topics without keywords 274 | keywords = keywords, # keywords 275 | model = "covariate", # select the model 276 | model_settings = list(covariates_data = vars_selected, 277 | covariates_formula = ~ intervention), 278 | options = list(seed = 250, 279 | store_theta = TRUE)) 280 | 281 | ``` 282 | 283 | ```{r} 284 | # Predicted mean of the document-term distribution for intervention 285 | strata_topic <- by_strata_DocTopic(out, by_var = "intervention", 286 | labels = c("Post-Trump speech", "Pre-Trump speech")) 287 | 288 | est <- summary(strata_topic) 289 | 290 | # Baseline 291 | new_data <- covariates_get(covariate_out) 292 | 293 | new_data[, "intervention"] <- 0 294 | 295 | pred <- predict(covariate_out, new_data, label = "Others") 296 | 297 | # Bind them together 298 | res <- bind_rows(est, pred) 299 | 300 | labels <- unique(res$label) 301 | 302 | ggplot(res, aes(x = label, ymin = Lower, ymax = Upper, group = Topic)) + 303 | geom_errorbar(width = 0.1) + 304 | coord_flip() + 305 | facet_wrap(~Topic) + 306 | geom_point(aes(x = label, y = Point)) + 307 | scale_x_discrete(limits = rev(labels)) + 308 | xlab("Intervention") + 309 | scale_y_continuous(labels = 310 | scales::percent_format(accuracy = 1)) 311 | ``` 312 | 313 | ## Dynamic topic modeling 314 | 315 | ````{r} 316 | 317 | tictoc::tic() 318 | dynamic_out_day <- keyATM(docs = keyATM_docs, # text input 319 | no_keyword_topics = 1, # number of topics without keywords 320 | keywords = keywords, # keywords 321 | model = "dynamic", # select the model 322 | model_settings = list(time_index = docvars(my_corpus)$index, num_states = 5), 323 | options = list(seed = 250, store_theta = TRUE, thinning = 5)) 324 | tictoc::toc() 325 | 326 | # Save 327 | write_rds(dynamic_out_day, here("outputs", "dynamic_out_day.rds")) 328 | 329 | ``` 330 | 331 | ```{r} 332 | dynamic_out_day <- read_rds(here("outputs", "dynamic_out_day.rds")) 333 | 334 | # Visualize 335 | fig_timetrend_day <- plot_timetrend(dynamic_out_day, time_index_label = as.Date(docvars(my_corpus)$date), xlab = "Date", width = 5) 336 | 337 | keyATM::save_fig(fig_timetrend_day, here("outputs", "dynamic_topic_day.png")) 338 | 339 | # Alt visualize 340 | 341 | df <- data.frame(date = fig_timetrend_day$values$time_index, 342 | mean = fig_timetrend_day$values$Point, 343 | upper = fig_timetrend_day$values$Upper, 344 | lower = fig_timetrend_day$values$Lower, 345 | topic = fig_timetrend_day$values$Topic) 346 | 347 | ``` 348 | 349 | ```{r} 350 | df %>% ggplot() + 351 | geom_line(aes(x = date, y = mean), 352 | alpha = 0.5, size = 1.2) + 353 | geom_ribbon(aes(x = date, y = mean, ymax = upper, ymin = lower), 354 | alpha = 0.3) + 355 | geom_smooth(aes(x = date, y = mean, ymax = upper, ymin = lower), 356 | method = "loess", 357 | size = 1.5, 358 | span = 0.3) + # for given x, loess will use the 0.3 * N closet poitns to x to fit. source: https://rafalab.github.io/dsbook/smoothing.html 359 | labs(title = "Topic trends over time", 360 | subtitle = "Tweets mentioned COVID-19 and either Asian, Chinese, or Wuhan", 361 | x = "Date", 362 | y = "Topic proportion") + 363 | facet_wrap(~topic) + 364 | geom_vline(xintercept = as.Date(c("2020-03-16")), 365 | linetype = "dashed", 366 | size = 1.2, 367 | color = "black") + 368 | scale_y_continuous(labels = 369 | scales::percent_format(accuracy = 1)) 370 | 371 | ggsave(here("outputs", "anti_asian_topic_dynamic_trend.png")) 372 | 373 | ``` -------------------------------------------------------------------------------- /raw_data/tweet_ids: -------------------------------------------------------------------------------- 1 | 1281789363681136640 2 | 1281789293736984577 3 | 1281789209884479490 4 | 1281788827099693056 5 | 1281788588166991872 6 | 1281787531785371649 7 | 1281781028558962689 8 | 1281780681379586053 9 | 1281761719552036867 10 | 1281760079558184960 11 | 1281757895860740096 12 | 1281741630383353856 13 | 1281731565278515202 14 | 1281722202908483592 15 | 1281680644288974859 16 | 1281680566182715393 17 | 1281680298326073344 18 | 1281680267703459846 19 | 1281637188850143232 20 | 1281637140951109632 21 | 1281626706349088770 22 | 1281616590060965888 23 | 1281616586273468416 24 | 1281603566071771136 25 | 1281556766086574080 26 | 1281556758457188352 27 | 1281556745211523072 28 | 1281554521748119553 29 | 1281554061972692994 30 | 1281552520612057088 31 | 1281449591116890112 32 | 1281447465774919680 33 | 1281408447074971653 34 | 1281365695020896259 35 | 1281355449523146752 36 | 1281354809807970305 37 | 1281260329247399936 38 | 1281250568342769665 39 | 1281250567373967360 40 | 1281250566199541761 41 | 1281250565163532288 42 | 1281236412667432961 43 | 1281236214646034432 44 | 1281209521675874304 45 | 1281209215953113098 46 | 1281206685395292160 47 | 1281206354334625793 48 | 1281193507051515905 49 | 1281193266160054272 50 | 1281071891307016192 51 | 1281071885887975424 52 | 1281071779386187777 53 | 1281017490370371593 54 | 1280983120184061954 55 | 1280978927532548097 56 | 1280978909815869443 57 | 1280963259043102720 58 | 1280948726945677313 59 | 1280896789940494343 60 | 1280861869507567616 61 | 1280857657365200902 62 | 1280853299600789505 63 | 1280649677894037509 64 | 1280518510184144898 65 | 1280517860725530624 66 | 1280484878744793090 67 | 1280384628453498880 68 | 1280383987068829696 69 | 1280332215285579776 70 | 1280328830218051584 71 | 1280320684204404737 72 | 1280285412188213248 73 | 1280234504985157637 74 | 1280232979781111808 75 | 1280227642294308865 76 | 1280210947014119424 77 | 1280209946085339136 78 | 1280209143975084032 79 | 1280209143127773184 80 | 1280209106826125313 81 | 1280205902742781958 82 | 1280203174008303616 83 | 1280134205696094208 84 | 1280132362152693761 85 | 1280128809258364928 86 | 1280126076719706113 87 | 1280117571874951170 88 | 1280116392990253056 89 | 1280115980090380288 90 | 1280112938125348865 91 | 1280112728850513922 92 | 1279906540791779334 93 | 1279890194653724672 94 | 1279888307200176131 95 | 1279887994107891717 96 | 1279884213055926272 97 | 1279879585627279365 98 | 1279823128810659841 99 | 1279649292966395904 100 | 1279623260490039298 101 | 1279621608521547776 102 | 1279611869515440129 103 | 1279545900138991617 104 | 1279510068606701568 105 | 1279508996295196673 106 | 1279507107663040513 107 | 1279507056601554945 108 | 1279506263370616833 109 | 1279506043836530693 110 | 1279505559092412417 111 | 1279504269784289281 112 | 1279503442612977665 113 | 1279502286549651458 114 | 1279501477787074560 115 | 1279501202657546242 116 | 1279500758212280321 117 | 1279500682366763008 118 | 1279500349552963586 119 | 1279500298256605184 120 | 1279499601016426496 121 | 1279487628732256257 122 | 1279487627977252864 123 | 1279482640693972992 124 | 1279482515905040385 125 | 1279481818698391552 126 | 1279481431325061120 127 | 1279481339310493698 128 | 1279478432976510978 129 | 1279475529951973382 130 | 1279475489858609152 131 | 1279475438423814150 132 | 1279475358610358273 133 | 1279474506101395456 134 | 1279469940400107522 135 | 1279469749567655936 136 | 1279469360277577729 137 | 1279468358744911873 138 | 1279467315076177921 139 | 1279467151720579072 140 | 1279467062214168577 141 | 1279465587891220482 142 | 1279465310391873536 143 | 1279464993457668098 144 | 1279464546097418240 145 | 1279459128054894593 146 | 1279458984752287744 147 | 1279458490310950912 148 | 1279458327307657217 149 | 1279458021308084227 150 | 1279457838256009217 151 | 1279457045188612096 152 | 1279452164310683649 153 | 1279450447191977987 154 | 1279448145819295747 155 | 1279448072683167746 156 | 1279446869719089152 157 | 1279446229496279040 158 | 1279446018984226818 159 | 1279445961811656704 160 | 1279445476073508865 161 | 1279439607482941441 162 | 1279354659220656129 163 | 1279301988639899648 164 | 1279290634101075969 165 | 1279287248299552768 166 | 1279282949175644160 167 | 1279267590305701893 168 | 1279267576053448710 169 | 1279267549977497600 170 | 1279267507405283328 171 | 1279267489051017216 172 | 1279267466938650631 173 | 1279267306716233728 174 | 1279190966394515457 175 | 1279161192167215106 176 | 1279161110336278531 177 | 1279161075926274048 178 | 1279160972935135232 179 | 1279160970959601667 180 | 1279160286117867521 181 | 1279160285132136455 182 | 1279160140546088962 183 | 1279159953039724544 184 | 1279025204963020800 185 | 1278901518444486656 186 | 1278901503600852993 187 | 1278898912011726848 188 | 1278897430378041344 189 | 1278892544894668801 190 | 1278891780512059392 191 | 1278891135063244805 192 | 1278891032390836229 193 | 1278890935900913665 194 | 1278890671492018176 195 | 1278890408710418432 196 | 1278890366725427202 197 | 1278890042723835904 198 | 1278889570893996032 199 | 1278845759274844165 200 | 1278845376901132291 201 | 1278836342609379328 202 | 1278833085522509824 203 | 1278830576141729792 204 | 1278830490812899329 205 | 1278830096418320385 206 | 1278830005422874624 207 | 1278828759672672269 208 | 1278828245161594886 209 | 1278827752385323010 210 | 1278826814383837184 211 | 1278825777052729345 212 | 1278824441611419648 213 | 1278824285717639170 214 | 1278824133258862593 215 | 1278822989711228928 216 | 1278806988806410241 217 | 1278746816935350273 218 | 1278715465205010433 219 | 1278715220677013504 220 | 1278711846627917830 221 | 1278700018355048448 222 | 1278699680348717056 223 | 1278677687050084352 224 | 1278673786913599488 225 | 1281789363681136640 226 | 1281789293736984577 227 | 1281789209884479490 228 | 1281788827099693056 229 | 1281788588166991872 230 | 1281787531785371649 231 | 1281781028558962689 232 | 1281780681379586053 233 | 1281761719552036867 234 | 1281760079558184960 235 | 1281757895860740096 236 | 1281741630383353856 237 | 1281731565278515202 238 | 1281722202908483592 239 | 1281680644288974859 240 | 1281680566182715393 241 | 1281680298326073344 242 | 1281680267703459846 243 | 1281637188850143232 244 | 1281637140951109632 245 | 1281626706349088770 246 | 1281616590060965888 247 | 1281616586273468416 248 | 1281603566071771136 249 | 1281556766086574080 250 | 1281556758457188352 251 | 1281556745211523072 252 | 1281554521748119553 253 | 1281554061972692994 254 | 1281552520612057088 255 | 1281449591116890112 256 | 1281447465774919680 257 | 1281408447074971653 258 | 1281365695020896259 259 | 1281355449523146752 260 | 1281354809807970305 261 | 1281260329247399936 262 | 1281250568342769665 263 | 1281250567373967360 264 | 1281250566199541761 265 | 1281250565163532288 266 | 1281236412667432961 267 | 1281236214646034432 268 | 1281209521675874304 269 | 1281209215953113098 270 | 1281206685395292160 271 | 1281206354334625793 272 | 1281193507051515905 273 | 1281193266160054272 274 | 1281071891307016192 275 | 1281071885887975424 276 | 1281071779386187777 277 | 1281017490370371593 278 | 1280983120184061954 279 | 1280978927532548097 280 | 1280978909815869443 281 | 1280963259043102720 282 | 1280948726945677313 283 | 1280896789940494343 284 | 1280861869507567616 285 | 1280857657365200902 286 | 1280853299600789505 287 | 1280649677894037509 288 | 1280518510184144898 289 | 1280517860725530624 290 | 1280484878744793090 291 | 1280384628453498880 292 | 1280383987068829696 293 | 1280332215285579776 294 | 1280328830218051584 295 | 1280320684204404737 296 | 1280285412188213248 297 | 1280234504985157637 298 | 1280232979781111808 299 | 1280227642294308865 300 | 1280210947014119424 301 | 1280209946085339136 302 | 1280209143975084032 303 | 1280209143127773184 304 | 1280209106826125313 305 | 1280205902742781958 306 | 1280203174008303616 307 | 1280134205696094208 308 | 1280132362152693761 309 | 1280128809258364928 310 | 1280126076719706113 311 | 1280117571874951170 312 | 1280116392990253056 313 | 1280115980090380288 314 | 1280112938125348865 315 | 1280112728850513922 316 | 1279906540791779334 317 | 1279890194653724672 318 | 1279888307200176131 319 | 1279887994107891717 320 | 1279884213055926272 321 | 1279879585627279365 322 | 1279823128810659841 323 | 1279649292966395904 324 | 1279623260490039298 325 | 1279621608521547776 326 | 1279611869515440129 327 | 1279545900138991617 328 | 1279510068606701568 329 | 1279508996295196673 330 | 1279507107663040513 331 | 1279507056601554945 332 | 1279506263370616833 333 | 1279506043836530693 334 | 1279505559092412417 335 | 1279504269784289281 336 | 1279503442612977665 337 | 1279502286549651458 338 | 1279501477787074560 339 | 1279501202657546242 340 | 1279500758212280321 341 | 1279500682366763008 342 | 1279500349552963586 343 | 1279500298256605184 344 | 1279499601016426496 345 | 1279487628732256257 346 | 1279487627977252864 347 | 1279482640693972992 348 | 1279482515905040385 349 | 1279481818698391552 350 | 1279481431325061120 351 | 1279481339310493698 352 | 1279478432976510978 353 | 1279475529951973382 354 | 1279475489858609152 355 | 1279475438423814150 356 | 1279475358610358273 357 | 1279474506101395456 358 | 1279469940400107522 359 | 1279469749567655936 360 | 1279469360277577729 361 | 1279468358744911873 362 | 1279467315076177921 363 | 1279467151720579072 364 | 1279467062214168577 365 | 1279465587891220482 366 | 1279465310391873536 367 | 1279464993457668098 368 | 1279464546097418240 369 | 1279459128054894593 370 | 1279458984752287744 371 | 1279458490310950912 372 | 1279458327307657217 373 | 1279458021308084227 374 | 1279457838256009217 375 | 1279457045188612096 376 | 1279452164310683649 377 | 1279450447191977987 378 | 1279448145819295747 379 | 1279448072683167746 380 | 1279446869719089152 381 | 1279446229496279040 382 | 1279446018984226818 383 | 1279445961811656704 384 | 1279445476073508865 385 | 1279439607482941441 386 | 1279354659220656129 387 | 1279301988639899648 388 | 1279290634101075969 389 | 1279287248299552768 390 | 1279282949175644160 391 | 1279267590305701893 392 | 1279267576053448710 393 | 1279267549977497600 394 | 1279267507405283328 395 | 1279267489051017216 396 | 1279267466938650631 397 | 1279267306716233728 398 | 1279190966394515457 399 | 1279161192167215106 400 | 1279161110336278531 401 | 1279161075926274048 402 | 1279160972935135232 403 | 1279160970959601667 404 | 1279160286117867521 405 | 1279160285132136455 406 | 1279160140546088962 407 | 1279159953039724544 408 | 1279025204963020800 409 | 1278901518444486656 410 | 1278901503600852993 411 | 1278898912011726848 412 | 1278897430378041344 413 | 1278892544894668801 414 | 1278891780512059392 415 | 1278891135063244805 416 | 1278891032390836229 417 | 1278890935900913665 418 | 1278890671492018176 419 | 1278890408710418432 420 | 1278890366725427202 421 | 1278890042723835904 422 | 1278889570893996032 423 | 1278845759274844165 424 | 1278845376901132291 425 | 1278836342609379328 426 | 1278833085522509824 427 | 1278830576141729792 428 | 1278830490812899329 429 | 1278830096418320385 430 | 1278830005422874624 431 | 1278828759672672269 432 | 1278828245161594886 433 | 1278827752385323010 434 | 1278826814383837184 435 | 1278825777052729345 436 | 1278824441611419648 437 | 1278824285717639170 438 | 1278824133258862593 439 | 1278822989711228928 440 | 1278806988806410241 441 | 1278746816935350273 442 | 1278715465205010433 443 | 1278715220677013504 444 | 1278711846627917830 445 | 1278700018355048448 446 | 1278699680348717056 447 | 1278677687050084352 448 | 1278673786913599488 449 | 1281789363681136640 450 | 1281789293736984577 451 | 1281789209884479490 452 | 1281788827099693056 453 | 1281788588166991872 454 | 1281787531785371649 455 | 1281781028558962689 456 | 1281780681379586053 457 | 1281761719552036867 458 | 1281760079558184960 459 | 1281757895860740096 460 | 1281741630383353856 461 | 1281731565278515202 462 | 1281722202908483592 463 | 1281680644288974859 464 | 1281680566182715393 465 | 1281680298326073344 466 | 1281680267703459846 467 | 1281637188850143232 468 | 1281637140951109632 469 | 1281626706349088770 470 | 1281616590060965888 471 | 1281616586273468416 472 | 1281603566071771136 473 | 1281556766086574080 474 | 1281556758457188352 475 | 1281556745211523072 476 | 1281554521748119553 477 | 1281554061972692994 478 | 1281552520612057088 479 | 1281449591116890112 480 | 1281447465774919680 481 | 1281408447074971653 482 | 1281365695020896259 483 | 1281355449523146752 484 | 1281354809807970305 485 | 1281260329247399936 486 | 1281250568342769665 487 | 1281250567373967360 488 | 1281250566199541761 489 | 1281250565163532288 490 | 1281236412667432961 491 | 1281236214646034432 492 | 1281209521675874304 493 | 1281209215953113098 494 | 1281206685395292160 495 | 1281206354334625793 496 | 1281193507051515905 497 | 1281193266160054272 498 | 1281071891307016192 499 | 1281071885887975424 500 | 1281071779386187777 501 | 1281017490370371593 502 | 1280983120184061954 503 | 1280978927532548097 504 | 1280978909815869443 505 | 1280963259043102720 506 | 1280948726945677313 507 | 1280896789940494343 508 | 1280861869507567616 509 | 1280857657365200902 510 | 1280853299600789505 511 | 1280649677894037509 512 | 1280518510184144898 513 | 1280517860725530624 514 | 1280484878744793090 515 | 1280384628453498880 516 | 1280383987068829696 517 | 1280332215285579776 518 | 1280328830218051584 519 | 1280320684204404737 520 | 1280285412188213248 521 | 1280234504985157637 522 | 1280232979781111808 523 | 1280227642294308865 524 | 1280210947014119424 525 | 1280209946085339136 526 | 1280209143975084032 527 | 1280209143127773184 528 | 1280209106826125313 529 | 1280205902742781958 530 | 1280203174008303616 531 | 1280134205696094208 532 | 1280132362152693761 533 | 1280128809258364928 534 | 1280126076719706113 535 | 1280117571874951170 536 | 1280116392990253056 537 | 1280115980090380288 538 | 1280112938125348865 539 | 1280112728850513922 540 | 1279906540791779334 541 | 1279890194653724672 542 | 1279888307200176131 543 | 1279887994107891717 544 | 1279884213055926272 545 | 1279879585627279365 546 | 1279823128810659841 547 | 1279649292966395904 548 | 1279623260490039298 549 | 1279621608521547776 550 | 1279611869515440129 551 | 1279545900138991617 552 | 1279510068606701568 553 | 1279508996295196673 554 | 1279507107663040513 555 | 1279507056601554945 556 | 1279506263370616833 557 | 1279506043836530693 558 | 1279505559092412417 559 | 1279504269784289281 560 | 1279503442612977665 561 | 1279502286549651458 562 | 1279501477787074560 563 | 1279501202657546242 564 | 1279500758212280321 565 | 1279500682366763008 566 | 1279500349552963586 567 | 1279500298256605184 568 | 1279499601016426496 569 | 1279487628732256257 570 | 1279487627977252864 571 | 1279482640693972992 572 | 1279482515905040385 573 | 1279481818698391552 574 | 1279481431325061120 575 | 1279481339310493698 576 | 1279478432976510978 577 | 1279475529951973382 578 | 1279475489858609152 579 | 1279475438423814150 580 | 1279475358610358273 581 | 1279474506101395456 582 | 1279469940400107522 583 | 1279469749567655936 584 | 1279469360277577729 585 | 1279468358744911873 586 | 1279467315076177921 587 | 1279467151720579072 588 | 1279467062214168577 589 | 1279465587891220482 590 | 1279465310391873536 591 | 1279464993457668098 592 | 1279464546097418240 593 | 1279459128054894593 594 | 1279458984752287744 595 | 1279458490310950912 596 | 1279458327307657217 597 | 1279458021308084227 598 | 1279457838256009217 599 | 1279457045188612096 600 | 1279452164310683649 601 | 1279450447191977987 602 | 1279448145819295747 603 | 1279448072683167746 604 | 1279446869719089152 605 | 1279446229496279040 606 | 1279446018984226818 607 | 1279445961811656704 608 | 1279445476073508865 609 | 1279439607482941441 610 | 1279354659220656129 611 | 1279301988639899648 612 | 1279290634101075969 613 | 1279287248299552768 614 | 1279282949175644160 615 | 1279267590305701893 616 | 1279267576053448710 617 | 1279267549977497600 618 | 1279267507405283328 619 | 1279267489051017216 620 | 1279267466938650631 621 | 1279267306716233728 622 | 1279190966394515457 623 | 1279161192167215106 624 | 1279161110336278531 625 | 1279161075926274048 626 | 1279160972935135232 627 | 1279160970959601667 628 | 1279160286117867521 629 | 1279160285132136455 630 | 1279160140546088962 631 | 1279159953039724544 632 | 1279025204963020800 633 | 1278901518444486656 634 | 1278901503600852993 635 | 1278898912011726848 636 | 1278897430378041344 637 | 1278892544894668801 638 | 1278891780512059392 639 | 1278891135063244805 640 | 1278891032390836229 641 | 1278890935900913665 642 | 1278890671492018176 643 | 1278890408710418432 644 | 1278890366725427202 645 | 1278890042723835904 646 | 1278889570893996032 647 | 1278845759274844165 648 | 1278845376901132291 649 | 1278836342609379328 650 | 1278833085522509824 651 | 1278830576141729792 652 | 1278830490812899329 653 | 1278830096418320385 654 | 1278830005422874624 655 | 1278828759672672269 656 | 1278828245161594886 657 | 1278827752385323010 658 | 1278826814383837184 659 | 1278825777052729345 660 | 1278824441611419648 661 | 1278824285717639170 662 | 1278824133258862593 663 | 1278822989711228928 664 | 1278806988806410241 665 | 1278746816935350273 666 | 1278715465205010433 667 | 1278715220677013504 668 | 1278711846627917830 669 | 1278700018355048448 670 | 1278699680348717056 671 | 1278677687050084352 672 | 1278673786913599488 673 | 1281789363681136640 674 | 1281789293736984577 675 | 1281789209884479490 676 | 1281788827099693056 677 | 1281788588166991872 678 | 1281787531785371649 679 | 1281781028558962689 680 | 1281780681379586053 681 | 1281761719552036867 682 | 1281760079558184960 683 | 1281757895860740096 684 | 1281741630383353856 685 | 1281731565278515202 686 | 1281722202908483592 687 | 1281680644288974859 688 | 1281680566182715393 689 | 1281680298326073344 690 | 1281680267703459846 691 | 1281637188850143232 692 | 1281637140951109632 693 | 1281626706349088770 694 | 1281616590060965888 695 | 1281616586273468416 696 | 1281603566071771136 697 | 1281556766086574080 698 | 1281556758457188352 699 | 1281556745211523072 700 | 1281554521748119553 701 | 1281554061972692994 702 | 1281552520612057088 703 | 1281449591116890112 704 | 1281447465774919680 705 | 1281408447074971653 706 | 1281365695020896259 707 | 1281355449523146752 708 | 1281354809807970305 709 | 1281260329247399936 710 | 1281250568342769665 711 | 1281250567373967360 712 | 1281250566199541761 713 | 1281250565163532288 714 | 1281236412667432961 715 | 1281236214646034432 716 | 1281209521675874304 717 | 1281209215953113098 718 | 1281206685395292160 719 | 1281206354334625793 720 | 1281193507051515905 721 | 1281193266160054272 722 | 1281071891307016192 723 | 1281071885887975424 724 | 1281071779386187777 725 | 1281017490370371593 726 | 1280983120184061954 727 | 1280978927532548097 728 | 1280978909815869443 729 | 1280963259043102720 730 | 1280948726945677313 731 | 1280896789940494343 732 | 1280861869507567616 733 | 1280857657365200902 734 | 1280853299600789505 735 | 1280649677894037509 736 | 1280518510184144898 737 | 1280517860725530624 738 | 1280484878744793090 739 | 1280384628453498880 740 | 1280383987068829696 741 | 1280332215285579776 742 | 1280328830218051584 743 | 1280320684204404737 744 | 1280285412188213248 745 | 1280234504985157637 746 | 1280232979781111808 747 | 1280227642294308865 748 | 1280210947014119424 749 | 1280209946085339136 750 | 1280209143975084032 751 | 1280209143127773184 752 | 1280209106826125313 753 | 1280205902742781958 754 | 1280203174008303616 755 | 1280134205696094208 756 | 1280132362152693761 757 | 1280128809258364928 758 | 1280126076719706113 759 | 1280117571874951170 760 | 1280116392990253056 761 | 1280115980090380288 762 | 1280112938125348865 763 | 1280112728850513922 764 | 1279906540791779334 765 | 1279890194653724672 766 | 1279888307200176131 767 | 1279887994107891717 768 | 1279884213055926272 769 | 1279879585627279365 770 | 1279823128810659841 771 | 1279649292966395904 772 | 1279623260490039298 773 | 1279621608521547776 774 | 1279611869515440129 775 | 1279545900138991617 776 | 1279510068606701568 777 | 1279508996295196673 778 | 1279507107663040513 779 | 1279507056601554945 780 | 1279506263370616833 781 | 1279506043836530693 782 | 1279505559092412417 783 | 1279504269784289281 784 | 1279503442612977665 785 | 1279502286549651458 786 | 1279501477787074560 787 | 1279501202657546242 788 | 1279500758212280321 789 | 1279500682366763008 790 | 1279500349552963586 791 | 1279500298256605184 792 | 1279499601016426496 793 | 1279487628732256257 794 | 1279487627977252864 795 | 1279482640693972992 796 | 1279482515905040385 797 | 1279481818698391552 798 | 1279481431325061120 799 | 1279481339310493698 800 | 1279478432976510978 801 | 1279475529951973382 802 | 1279475489858609152 803 | 1279475438423814150 804 | 1279475358610358273 805 | 1279474506101395456 806 | 1279469940400107522 807 | 1279469749567655936 808 | 1279469360277577729 809 | 1279468358744911873 810 | 1279467315076177921 811 | 1279467151720579072 812 | 1279467062214168577 813 | 1279465587891220482 814 | 1279465310391873536 815 | 1279464993457668098 816 | 1279464546097418240 817 | 1279459128054894593 818 | 1279458984752287744 819 | 1279458490310950912 820 | 1279458327307657217 821 | 1279458021308084227 822 | 1279457838256009217 823 | 1279457045188612096 824 | 1279452164310683649 825 | 1279450447191977987 826 | 1279448145819295747 827 | 1279448072683167746 828 | 1279446869719089152 829 | 1279446229496279040 830 | 1279446018984226818 831 | 1279445961811656704 832 | 1279445476073508865 833 | 1279439607482941441 834 | 1279354659220656129 835 | 1279301988639899648 836 | 1279290634101075969 837 | 1279287248299552768 838 | 1279282949175644160 839 | 1279267590305701893 840 | 1279267576053448710 841 | 1279267549977497600 842 | 1279267507405283328 843 | 1279267489051017216 844 | 1279267466938650631 845 | 1279267306716233728 846 | 1279190966394515457 847 | 1279161192167215106 848 | 1279161110336278531 849 | 1279161075926274048 850 | 1279160972935135232 851 | 1279160970959601667 852 | 1279160286117867521 853 | 1279160285132136455 854 | 1279160140546088962 855 | 1279159953039724544 856 | 1279025204963020800 857 | 1278901518444486656 858 | 1278901503600852993 859 | 1278898912011726848 860 | 1278897430378041344 861 | 1278892544894668801 862 | 1278891780512059392 863 | 1278891135063244805 864 | 1278891032390836229 865 | 1278890935900913665 866 | 1278890671492018176 867 | 1278890408710418432 868 | 1278890366725427202 869 | 1278890042723835904 870 | 1278889570893996032 871 | 1278845759274844165 872 | 1278845376901132291 873 | 1278836342609379328 874 | 1278833085522509824 875 | 1278830576141729792 876 | 1278830490812899329 877 | 1278830096418320385 878 | 1278830005422874624 879 | 1278828759672672269 880 | 1278828245161594886 881 | 1278827752385323010 882 | 1278826814383837184 883 | 1278825777052729345 884 | 1278824441611419648 885 | 1278824285717639170 886 | 1278824133258862593 887 | 1278822989711228928 888 | 1278806988806410241 889 | 1278746816935350273 890 | 1278715465205010433 891 | 1278715220677013504 892 | 1278711846627917830 893 | 1278700018355048448 894 | 1278699680348717056 895 | 1278677687050084352 896 | 1278673786913599488 897 | 1281820564525428737 898 | 1281802052461490176 899 | 1281791668497219584 900 | 1281789911113232384 901 | 1281785206198788099 902 | 1281783764809445377 903 | 1281782070222569475 904 | 1281735417696407553 905 | 1281730514622115840 906 | 1281708289428881408 907 | 1281688860129284096 908 | 1281681781368840192 909 | 1281681635998437376 910 | 1281679978317283328 911 | 1281672267936657408 912 | 1281664809440645120 913 | 1281664349048721408 914 | 1281663751595278336 915 | 1281651734507425794 916 | 1281643705699430401 917 | 1281643372982046720 918 | 1281643204110987265 919 | 1281503065514733569 920 | 1281501850659774464 921 | 1281501119298985984 922 | 1281476975442325505 923 | 1281476335181811713 924 | 1281475852937490432 925 | 1281475453832663040 926 | 1281464733858697217 927 | 1281444725711499264 928 | 1281443418548232193 929 | 1281439335963717632 930 | 1281439096343322625 931 | 1281438917380542464 932 | 1281396276731363329 933 | 1281368643327062021 934 | 1281337217290403840 935 | 1281313934193029122 936 | 1281309674482987008 937 | 1281307906088919041 938 | 1281301746438963201 939 | 1281299543187845122 940 | 1281297319095238656 941 | 1281261480516567041 942 | 1281260633074196480 943 | 1281098873063174145 944 | 1281096429717450753 945 | 1281080591148969984 946 | 1281078744795381761 947 | 1281070339322069000 948 | 1281070172581711873 949 | 1281024813797007360 950 | 1281023774561714178 951 | 1281023564980744192 952 | 1281023051967033344 953 | 1280964652713668608 954 | 1280959018073235457 955 | 1280954460177489920 956 | 1280947020992151552 957 | 1280945804274876421 958 | 1280904188772274176 959 | 1280900171274960896 960 | 1280815548868161536 961 | 1280758075209576448 962 | 1280751558427213824 963 | 1280751279199776768 964 | 1280750454960295936 965 | 1280749544238505986 966 | 1280721013676716035 967 | 1280711652283805696 968 | 1280703889310793728 969 | 1280699279414304780 970 | 1280698238337642498 971 | 1280697462907256832 972 | 1280690032446074880 973 | 1280689862316716032 974 | 1280668187227373570 975 | 1280661049608302593 976 | 1280658436682706944 977 | 1280643815213158401 978 | 1280605817918521344 979 | 1280603192556834816 980 | 1280583932472201216 981 | 1280582579419090944 982 | 1280575500801335296 983 | 1280575096898285569 984 | 1280550493475893254 985 | 1280550368779268101 986 | 1280544561320747014 987 | 1280536933186199552 988 | 1280535750195294209 989 | 1280535147939422208 990 | 1280534632321040385 991 | 1280533702565433344 992 | 1280533389892710400 993 | 1280533031384608769 994 | 1280532041994366976 995 | 1280531595884064773 996 | 1280531197186076672 997 | 1280530948002463746 998 | 1280530474855587840 999 | 1280530123876233219 1000 | 1280529741129199621 1001 | 1280529205873135616 1002 | 1280526941271977984 1003 | 1280383236024045569 1004 | 1280381045666312193 1005 | 1280380726769139713 1006 | 1280380169979486209 1007 | 1280362606318911488 1008 | 1280362515545812992 1009 | 1280362197470703617 1010 | 1280337425537961984 1011 | 1280332534421663744 1012 | 1280332131026100224 1013 | 1280329162725552128 1014 | 1280279497304993792 1015 | 1280277722254606342 1016 | 1280277110095884288 1017 | 1280276364218691585 1018 | 1280275751346991106 1019 | 1280274998968508416 1020 | 1280274472302338049 1021 | 1280273751905464320 1022 | 1280272856652242944 1023 | 1280258721734520832 1024 | 1280254623509438464 1025 | 1280253291205545985 1026 | 1279836927085080578 1027 | 1279626374991319045 1028 | 1279498136688287745 1029 | 1279496530144063489 1030 | 1279496355623264256 1031 | 1279494652333195264 1032 | 1279493198121799680 1033 | 1279450267834974208 1034 | 1279442218336481280 1035 | 1279435939702398976 1036 | 1279273173343588353 1037 | 1279256487395655680 1038 | 1279233037624987649 1039 | 1279231332644564992 1040 | 1279225181043486720 1041 | 1279224658571608064 1042 | 1279222869189292032 1043 | 1279221688589144064 1044 | 1279221556799893504 1045 | 1279220653145522176 1046 | 1279211541171105792 1047 | 1279211296248852480 1048 | 1279210622358482944 1049 | 1279210321668800514 1050 | 1279209803462488064 1051 | 1279209453242335232 1052 | 1279202688857600000 1053 | 1279198169549508608 1054 | 1279118046615924737 1055 | 1279117737650819072 1056 | 1279114182474870785 1057 | 1279105846056607744 1058 | 1279104004471967745 1059 | 1279103750070603776 1060 | 1279103515705540618 1061 | 1279103069720940544 1062 | 1279083702174117889 1063 | 1279083609836539908 1064 | 1279081141169827841 1065 | 1279080324631150594 1066 | 1279079935714291718 1067 | 1279079264193675265 1068 | 1279079042881208320 1069 | 1279075332767641600 1070 | 1279075048955932673 1071 | 1279072467525332992 1072 | 1279072260368723968 1073 | 1279071908995072000 1074 | 1278961827888590851 1075 | 1278934491147300864 1076 | 1278929492648443904 1077 | 1278928108519780354 1078 | 1278909890187030528 1079 | 1278850013863407616 1080 | 1278848679416225792 1081 | 1278829141819715584 1082 | 1278826926262480897 1083 | 1278824286438883328 1084 | 1278819454604816384 1085 | 1278807057261490177 1086 | 1278806183516618752 1087 | 1278788874840322053 1088 | 1278727829392207872 1089 | 1278719405245952004 1090 | -------------------------------------------------------------------------------- /outputs/hash_cloud_files/htmlwidgets-1.5.1/htmlwidgets.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // If window.HTMLWidgets is already defined, then use it; otherwise create a 3 | // new object. This allows preceding code to set options that affect the 4 | // initialization process (though none currently exist). 5 | window.HTMLWidgets = window.HTMLWidgets || {}; 6 | 7 | // See if we're running in a viewer pane. If not, we're in a web browser. 8 | var viewerMode = window.HTMLWidgets.viewerMode = 9 | /\bviewer_pane=1\b/.test(window.location); 10 | 11 | // See if we're running in Shiny mode. If not, it's a static document. 12 | // Note that static widgets can appear in both Shiny and static modes, but 13 | // obviously, Shiny widgets can only appear in Shiny apps/documents. 14 | var shinyMode = window.HTMLWidgets.shinyMode = 15 | typeof(window.Shiny) !== "undefined" && !!window.Shiny.outputBindings; 16 | 17 | // We can't count on jQuery being available, so we implement our own 18 | // version if necessary. 19 | function querySelectorAll(scope, selector) { 20 | if (typeof(jQuery) !== "undefined" && scope instanceof jQuery) { 21 | return scope.find(selector); 22 | } 23 | if (scope.querySelectorAll) { 24 | return scope.querySelectorAll(selector); 25 | } 26 | } 27 | 28 | function asArray(value) { 29 | if (value === null) 30 | return []; 31 | if ($.isArray(value)) 32 | return value; 33 | return [value]; 34 | } 35 | 36 | // Implement jQuery's extend 37 | function extend(target /*, ... */) { 38 | if (arguments.length == 1) { 39 | return target; 40 | } 41 | for (var i = 1; i < arguments.length; i++) { 42 | var source = arguments[i]; 43 | for (var prop in source) { 44 | if (source.hasOwnProperty(prop)) { 45 | target[prop] = source[prop]; 46 | } 47 | } 48 | } 49 | return target; 50 | } 51 | 52 | // IE8 doesn't support Array.forEach. 53 | function forEach(values, callback, thisArg) { 54 | if (values.forEach) { 55 | values.forEach(callback, thisArg); 56 | } else { 57 | for (var i = 0; i < values.length; i++) { 58 | callback.call(thisArg, values[i], i, values); 59 | } 60 | } 61 | } 62 | 63 | // Replaces the specified method with the return value of funcSource. 64 | // 65 | // Note that funcSource should not BE the new method, it should be a function 66 | // that RETURNS the new method. funcSource receives a single argument that is 67 | // the overridden method, it can be called from the new method. The overridden 68 | // method can be called like a regular function, it has the target permanently 69 | // bound to it so "this" will work correctly. 70 | function overrideMethod(target, methodName, funcSource) { 71 | var superFunc = target[methodName] || function() {}; 72 | var superFuncBound = function() { 73 | return superFunc.apply(target, arguments); 74 | }; 75 | target[methodName] = funcSource(superFuncBound); 76 | } 77 | 78 | // Add a method to delegator that, when invoked, calls 79 | // delegatee.methodName. If there is no such method on 80 | // the delegatee, but there was one on delegator before 81 | // delegateMethod was called, then the original version 82 | // is invoked instead. 83 | // For example: 84 | // 85 | // var a = { 86 | // method1: function() { console.log('a1'); } 87 | // method2: function() { console.log('a2'); } 88 | // }; 89 | // var b = { 90 | // method1: function() { console.log('b1'); } 91 | // }; 92 | // delegateMethod(a, b, "method1"); 93 | // delegateMethod(a, b, "method2"); 94 | // a.method1(); 95 | // a.method2(); 96 | // 97 | // The output would be "b1", "a2". 98 | function delegateMethod(delegator, delegatee, methodName) { 99 | var inherited = delegator[methodName]; 100 | delegator[methodName] = function() { 101 | var target = delegatee; 102 | var method = delegatee[methodName]; 103 | 104 | // The method doesn't exist on the delegatee. Instead, 105 | // call the method on the delegator, if it exists. 106 | if (!method) { 107 | target = delegator; 108 | method = inherited; 109 | } 110 | 111 | if (method) { 112 | return method.apply(target, arguments); 113 | } 114 | }; 115 | } 116 | 117 | // Implement a vague facsimilie of jQuery's data method 118 | function elementData(el, name, value) { 119 | if (arguments.length == 2) { 120 | return el["htmlwidget_data_" + name]; 121 | } else if (arguments.length == 3) { 122 | el["htmlwidget_data_" + name] = value; 123 | return el; 124 | } else { 125 | throw new Error("Wrong number of arguments for elementData: " + 126 | arguments.length); 127 | } 128 | } 129 | 130 | // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex 131 | function escapeRegExp(str) { 132 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 133 | } 134 | 135 | function hasClass(el, className) { 136 | var re = new RegExp("\\b" + escapeRegExp(className) + "\\b"); 137 | return re.test(el.className); 138 | } 139 | 140 | // elements - array (or array-like object) of HTML elements 141 | // className - class name to test for 142 | // include - if true, only return elements with given className; 143 | // if false, only return elements *without* given className 144 | function filterByClass(elements, className, include) { 145 | var results = []; 146 | for (var i = 0; i < elements.length; i++) { 147 | if (hasClass(elements[i], className) == include) 148 | results.push(elements[i]); 149 | } 150 | return results; 151 | } 152 | 153 | function on(obj, eventName, func) { 154 | if (obj.addEventListener) { 155 | obj.addEventListener(eventName, func, false); 156 | } else if (obj.attachEvent) { 157 | obj.attachEvent(eventName, func); 158 | } 159 | } 160 | 161 | function off(obj, eventName, func) { 162 | if (obj.removeEventListener) 163 | obj.removeEventListener(eventName, func, false); 164 | else if (obj.detachEvent) { 165 | obj.detachEvent(eventName, func); 166 | } 167 | } 168 | 169 | // Translate array of values to top/right/bottom/left, as usual with 170 | // the "padding" CSS property 171 | // https://developer.mozilla.org/en-US/docs/Web/CSS/padding 172 | function unpackPadding(value) { 173 | if (typeof(value) === "number") 174 | value = [value]; 175 | if (value.length === 1) { 176 | return {top: value[0], right: value[0], bottom: value[0], left: value[0]}; 177 | } 178 | if (value.length === 2) { 179 | return {top: value[0], right: value[1], bottom: value[0], left: value[1]}; 180 | } 181 | if (value.length === 3) { 182 | return {top: value[0], right: value[1], bottom: value[2], left: value[1]}; 183 | } 184 | if (value.length === 4) { 185 | return {top: value[0], right: value[1], bottom: value[2], left: value[3]}; 186 | } 187 | } 188 | 189 | // Convert an unpacked padding object to a CSS value 190 | function paddingToCss(paddingObj) { 191 | return paddingObj.top + "px " + paddingObj.right + "px " + paddingObj.bottom + "px " + paddingObj.left + "px"; 192 | } 193 | 194 | // Makes a number suitable for CSS 195 | function px(x) { 196 | if (typeof(x) === "number") 197 | return x + "px"; 198 | else 199 | return x; 200 | } 201 | 202 | // Retrieves runtime widget sizing information for an element. 203 | // The return value is either null, or an object with fill, padding, 204 | // defaultWidth, defaultHeight fields. 205 | function sizingPolicy(el) { 206 | var sizingEl = document.querySelector("script[data-for='" + el.id + "'][type='application/htmlwidget-sizing']"); 207 | if (!sizingEl) 208 | return null; 209 | var sp = JSON.parse(sizingEl.textContent || sizingEl.text || "{}"); 210 | if (viewerMode) { 211 | return sp.viewer; 212 | } else { 213 | return sp.browser; 214 | } 215 | } 216 | 217 | // @param tasks Array of strings (or falsy value, in which case no-op). 218 | // Each element must be a valid JavaScript expression that yields a 219 | // function. Or, can be an array of objects with "code" and "data" 220 | // properties; in this case, the "code" property should be a string 221 | // of JS that's an expr that yields a function, and "data" should be 222 | // an object that will be added as an additional argument when that 223 | // function is called. 224 | // @param target The object that will be "this" for each function 225 | // execution. 226 | // @param args Array of arguments to be passed to the functions. (The 227 | // same arguments will be passed to all functions.) 228 | function evalAndRun(tasks, target, args) { 229 | if (tasks) { 230 | forEach(tasks, function(task) { 231 | var theseArgs = args; 232 | if (typeof(task) === "object") { 233 | theseArgs = theseArgs.concat([task.data]); 234 | task = task.code; 235 | } 236 | var taskFunc = tryEval(task); 237 | if (typeof(taskFunc) !== "function") { 238 | throw new Error("Task must be a function! Source:\n" + task); 239 | } 240 | taskFunc.apply(target, theseArgs); 241 | }); 242 | } 243 | } 244 | 245 | // Attempt eval() both with and without enclosing in parentheses. 246 | // Note that enclosing coerces a function declaration into 247 | // an expression that eval() can parse 248 | // (otherwise, a SyntaxError is thrown) 249 | function tryEval(code) { 250 | var result = null; 251 | try { 252 | result = eval(code); 253 | } catch(error) { 254 | if (!error instanceof SyntaxError) { 255 | throw error; 256 | } 257 | try { 258 | result = eval("(" + code + ")"); 259 | } catch(e) { 260 | if (e instanceof SyntaxError) { 261 | throw error; 262 | } else { 263 | throw e; 264 | } 265 | } 266 | } 267 | return result; 268 | } 269 | 270 | function initSizing(el) { 271 | var sizing = sizingPolicy(el); 272 | if (!sizing) 273 | return; 274 | 275 | var cel = document.getElementById("htmlwidget_container"); 276 | if (!cel) 277 | return; 278 | 279 | if (typeof(sizing.padding) !== "undefined") { 280 | document.body.style.margin = "0"; 281 | document.body.style.padding = paddingToCss(unpackPadding(sizing.padding)); 282 | } 283 | 284 | if (sizing.fill) { 285 | document.body.style.overflow = "hidden"; 286 | document.body.style.width = "100%"; 287 | document.body.style.height = "100%"; 288 | document.documentElement.style.width = "100%"; 289 | document.documentElement.style.height = "100%"; 290 | if (cel) { 291 | cel.style.position = "absolute"; 292 | var pad = unpackPadding(sizing.padding); 293 | cel.style.top = pad.top + "px"; 294 | cel.style.right = pad.right + "px"; 295 | cel.style.bottom = pad.bottom + "px"; 296 | cel.style.left = pad.left + "px"; 297 | el.style.width = "100%"; 298 | el.style.height = "100%"; 299 | } 300 | 301 | return { 302 | getWidth: function() { return cel.offsetWidth; }, 303 | getHeight: function() { return cel.offsetHeight; } 304 | }; 305 | 306 | } else { 307 | el.style.width = px(sizing.width); 308 | el.style.height = px(sizing.height); 309 | 310 | return { 311 | getWidth: function() { return el.offsetWidth; }, 312 | getHeight: function() { return el.offsetHeight; } 313 | }; 314 | } 315 | } 316 | 317 | // Default implementations for methods 318 | var defaults = { 319 | find: function(scope) { 320 | return querySelectorAll(scope, "." + this.name); 321 | }, 322 | renderError: function(el, err) { 323 | var $el = $(el); 324 | 325 | this.clearError(el); 326 | 327 | // Add all these error classes, as Shiny does 328 | var errClass = "shiny-output-error"; 329 | if (err.type !== null) { 330 | // use the classes of the error condition as CSS class names 331 | errClass = errClass + " " + $.map(asArray(err.type), function(type) { 332 | return errClass + "-" + type; 333 | }).join(" "); 334 | } 335 | errClass = errClass + " htmlwidgets-error"; 336 | 337 | // Is el inline or block? If inline or inline-block, just display:none it 338 | // and add an inline error. 339 | var display = $el.css("display"); 340 | $el.data("restore-display-mode", display); 341 | 342 | if (display === "inline" || display === "inline-block") { 343 | $el.hide(); 344 | if (err.message !== "") { 345 | var errorSpan = $("").addClass(errClass); 346 | errorSpan.text(err.message); 347 | $el.after(errorSpan); 348 | } 349 | } else if (display === "block") { 350 | // If block, add an error just after the el, set visibility:none on the 351 | // el, and position the error to be on top of the el. 352 | // Mark it with a unique ID and CSS class so we can remove it later. 353 | $el.css("visibility", "hidden"); 354 | if (err.message !== "") { 355 | var errorDiv = $("
").addClass(errClass).css("position", "absolute") 356 | .css("top", el.offsetTop) 357 | .css("left", el.offsetLeft) 358 | // setting width can push out the page size, forcing otherwise 359 | // unnecessary scrollbars to appear and making it impossible for 360 | // the element to shrink; so use max-width instead 361 | .css("maxWidth", el.offsetWidth) 362 | .css("height", el.offsetHeight); 363 | errorDiv.text(err.message); 364 | $el.after(errorDiv); 365 | 366 | // Really dumb way to keep the size/position of the error in sync with 367 | // the parent element as the window is resized or whatever. 368 | var intId = setInterval(function() { 369 | if (!errorDiv[0].parentElement) { 370 | clearInterval(intId); 371 | return; 372 | } 373 | errorDiv 374 | .css("top", el.offsetTop) 375 | .css("left", el.offsetLeft) 376 | .css("maxWidth", el.offsetWidth) 377 | .css("height", el.offsetHeight); 378 | }, 500); 379 | } 380 | } 381 | }, 382 | clearError: function(el) { 383 | var $el = $(el); 384 | var display = $el.data("restore-display-mode"); 385 | $el.data("restore-display-mode", null); 386 | 387 | if (display === "inline" || display === "inline-block") { 388 | if (display) 389 | $el.css("display", display); 390 | $(el.nextSibling).filter(".htmlwidgets-error").remove(); 391 | } else if (display === "block"){ 392 | $el.css("visibility", "inherit"); 393 | $(el.nextSibling).filter(".htmlwidgets-error").remove(); 394 | } 395 | }, 396 | sizing: {} 397 | }; 398 | 399 | // Called by widget bindings to register a new type of widget. The definition 400 | // object can contain the following properties: 401 | // - name (required) - A string indicating the binding name, which will be 402 | // used by default as the CSS classname to look for. 403 | // - initialize (optional) - A function(el) that will be called once per 404 | // widget element; if a value is returned, it will be passed as the third 405 | // value to renderValue. 406 | // - renderValue (required) - A function(el, data, initValue) that will be 407 | // called with data. Static contexts will cause this to be called once per 408 | // element; Shiny apps will cause this to be called multiple times per 409 | // element, as the data changes. 410 | window.HTMLWidgets.widget = function(definition) { 411 | if (!definition.name) { 412 | throw new Error("Widget must have a name"); 413 | } 414 | if (!definition.type) { 415 | throw new Error("Widget must have a type"); 416 | } 417 | // Currently we only support output widgets 418 | if (definition.type !== "output") { 419 | throw new Error("Unrecognized widget type '" + definition.type + "'"); 420 | } 421 | // TODO: Verify that .name is a valid CSS classname 422 | 423 | // Support new-style instance-bound definitions. Old-style class-bound 424 | // definitions have one widget "object" per widget per type/class of 425 | // widget; the renderValue and resize methods on such widget objects 426 | // take el and instance arguments, because the widget object can't 427 | // store them. New-style instance-bound definitions have one widget 428 | // object per widget instance; the definition that's passed in doesn't 429 | // provide renderValue or resize methods at all, just the single method 430 | // factory(el, width, height) 431 | // which returns an object that has renderValue(x) and resize(w, h). 432 | // This enables a far more natural programming style for the widget 433 | // author, who can store per-instance state using either OO-style 434 | // instance fields or functional-style closure variables (I guess this 435 | // is in contrast to what can only be called C-style pseudo-OO which is 436 | // what we required before). 437 | if (definition.factory) { 438 | definition = createLegacyDefinitionAdapter(definition); 439 | } 440 | 441 | if (!definition.renderValue) { 442 | throw new Error("Widget must have a renderValue function"); 443 | } 444 | 445 | // For static rendering (non-Shiny), use a simple widget registration 446 | // scheme. We also use this scheme for Shiny apps/documents that also 447 | // contain static widgets. 448 | window.HTMLWidgets.widgets = window.HTMLWidgets.widgets || []; 449 | // Merge defaults into the definition; don't mutate the original definition. 450 | var staticBinding = extend({}, defaults, definition); 451 | overrideMethod(staticBinding, "find", function(superfunc) { 452 | return function(scope) { 453 | var results = superfunc(scope); 454 | // Filter out Shiny outputs, we only want the static kind 455 | return filterByClass(results, "html-widget-output", false); 456 | }; 457 | }); 458 | window.HTMLWidgets.widgets.push(staticBinding); 459 | 460 | if (shinyMode) { 461 | // Shiny is running. Register the definition with an output binding. 462 | // The definition itself will not be the output binding, instead 463 | // we will make an output binding object that delegates to the 464 | // definition. This is because we foolishly used the same method 465 | // name (renderValue) for htmlwidgets definition and Shiny bindings 466 | // but they actually have quite different semantics (the Shiny 467 | // bindings receive data that includes lots of metadata that it 468 | // strips off before calling htmlwidgets renderValue). We can't 469 | // just ignore the difference because in some widgets it's helpful 470 | // to call this.renderValue() from inside of resize(), and if 471 | // we're not delegating, then that call will go to the Shiny 472 | // version instead of the htmlwidgets version. 473 | 474 | // Merge defaults with definition, without mutating either. 475 | var bindingDef = extend({}, defaults, definition); 476 | 477 | // This object will be our actual Shiny binding. 478 | var shinyBinding = new Shiny.OutputBinding(); 479 | 480 | // With a few exceptions, we'll want to simply use the bindingDef's 481 | // version of methods if they are available, otherwise fall back to 482 | // Shiny's defaults. NOTE: If Shiny's output bindings gain additional 483 | // methods in the future, and we want them to be overrideable by 484 | // HTMLWidget binding definitions, then we'll need to add them to this 485 | // list. 486 | delegateMethod(shinyBinding, bindingDef, "getId"); 487 | delegateMethod(shinyBinding, bindingDef, "onValueChange"); 488 | delegateMethod(shinyBinding, bindingDef, "onValueError"); 489 | delegateMethod(shinyBinding, bindingDef, "renderError"); 490 | delegateMethod(shinyBinding, bindingDef, "clearError"); 491 | delegateMethod(shinyBinding, bindingDef, "showProgress"); 492 | 493 | // The find, renderValue, and resize are handled differently, because we 494 | // want to actually decorate the behavior of the bindingDef methods. 495 | 496 | shinyBinding.find = function(scope) { 497 | var results = bindingDef.find(scope); 498 | 499 | // Only return elements that are Shiny outputs, not static ones 500 | var dynamicResults = results.filter(".html-widget-output"); 501 | 502 | // It's possible that whatever caused Shiny to think there might be 503 | // new dynamic outputs, also caused there to be new static outputs. 504 | // Since there might be lots of different htmlwidgets bindings, we 505 | // schedule execution for later--no need to staticRender multiple 506 | // times. 507 | if (results.length !== dynamicResults.length) 508 | scheduleStaticRender(); 509 | 510 | return dynamicResults; 511 | }; 512 | 513 | // Wrap renderValue to handle initialization, which unfortunately isn't 514 | // supported natively by Shiny at the time of this writing. 515 | 516 | shinyBinding.renderValue = function(el, data) { 517 | Shiny.renderDependencies(data.deps); 518 | // Resolve strings marked as javascript literals to objects 519 | if (!(data.evals instanceof Array)) data.evals = [data.evals]; 520 | for (var i = 0; data.evals && i < data.evals.length; i++) { 521 | window.HTMLWidgets.evaluateStringMember(data.x, data.evals[i]); 522 | } 523 | if (!bindingDef.renderOnNullValue) { 524 | if (data.x === null) { 525 | el.style.visibility = "hidden"; 526 | return; 527 | } else { 528 | el.style.visibility = "inherit"; 529 | } 530 | } 531 | if (!elementData(el, "initialized")) { 532 | initSizing(el); 533 | 534 | elementData(el, "initialized", true); 535 | if (bindingDef.initialize) { 536 | var result = bindingDef.initialize(el, el.offsetWidth, 537 | el.offsetHeight); 538 | elementData(el, "init_result", result); 539 | } 540 | } 541 | bindingDef.renderValue(el, data.x, elementData(el, "init_result")); 542 | evalAndRun(data.jsHooks.render, elementData(el, "init_result"), [el, data.x]); 543 | }; 544 | 545 | // Only override resize if bindingDef implements it 546 | if (bindingDef.resize) { 547 | shinyBinding.resize = function(el, width, height) { 548 | // Shiny can call resize before initialize/renderValue have been 549 | // called, which doesn't make sense for widgets. 550 | if (elementData(el, "initialized")) { 551 | bindingDef.resize(el, width, height, elementData(el, "init_result")); 552 | } 553 | }; 554 | } 555 | 556 | Shiny.outputBindings.register(shinyBinding, bindingDef.name); 557 | } 558 | }; 559 | 560 | var scheduleStaticRenderTimerId = null; 561 | function scheduleStaticRender() { 562 | if (!scheduleStaticRenderTimerId) { 563 | scheduleStaticRenderTimerId = setTimeout(function() { 564 | scheduleStaticRenderTimerId = null; 565 | window.HTMLWidgets.staticRender(); 566 | }, 1); 567 | } 568 | } 569 | 570 | // Render static widgets after the document finishes loading 571 | // Statically render all elements that are of this widget's class 572 | window.HTMLWidgets.staticRender = function() { 573 | var bindings = window.HTMLWidgets.widgets || []; 574 | forEach(bindings, function(binding) { 575 | var matches = binding.find(document.documentElement); 576 | forEach(matches, function(el) { 577 | var sizeObj = initSizing(el, binding); 578 | 579 | if (hasClass(el, "html-widget-static-bound")) 580 | return; 581 | el.className = el.className + " html-widget-static-bound"; 582 | 583 | var initResult; 584 | if (binding.initialize) { 585 | initResult = binding.initialize(el, 586 | sizeObj ? sizeObj.getWidth() : el.offsetWidth, 587 | sizeObj ? sizeObj.getHeight() : el.offsetHeight 588 | ); 589 | elementData(el, "init_result", initResult); 590 | } 591 | 592 | if (binding.resize) { 593 | var lastSize = { 594 | w: sizeObj ? sizeObj.getWidth() : el.offsetWidth, 595 | h: sizeObj ? sizeObj.getHeight() : el.offsetHeight 596 | }; 597 | var resizeHandler = function(e) { 598 | var size = { 599 | w: sizeObj ? sizeObj.getWidth() : el.offsetWidth, 600 | h: sizeObj ? sizeObj.getHeight() : el.offsetHeight 601 | }; 602 | if (size.w === 0 && size.h === 0) 603 | return; 604 | if (size.w === lastSize.w && size.h === lastSize.h) 605 | return; 606 | lastSize = size; 607 | binding.resize(el, size.w, size.h, initResult); 608 | }; 609 | 610 | on(window, "resize", resizeHandler); 611 | 612 | // This is needed for cases where we're running in a Shiny 613 | // app, but the widget itself is not a Shiny output, but 614 | // rather a simple static widget. One example of this is 615 | // an rmarkdown document that has runtime:shiny and widget 616 | // that isn't in a render function. Shiny only knows to 617 | // call resize handlers for Shiny outputs, not for static 618 | // widgets, so we do it ourselves. 619 | if (window.jQuery) { 620 | window.jQuery(document).on( 621 | "shown.htmlwidgets shown.bs.tab.htmlwidgets shown.bs.collapse.htmlwidgets", 622 | resizeHandler 623 | ); 624 | window.jQuery(document).on( 625 | "hidden.htmlwidgets hidden.bs.tab.htmlwidgets hidden.bs.collapse.htmlwidgets", 626 | resizeHandler 627 | ); 628 | } 629 | 630 | // This is needed for the specific case of ioslides, which 631 | // flips slides between display:none and display:block. 632 | // Ideally we would not have to have ioslide-specific code 633 | // here, but rather have ioslides raise a generic event, 634 | // but the rmarkdown package just went to CRAN so the 635 | // window to getting that fixed may be long. 636 | if (window.addEventListener) { 637 | // It's OK to limit this to window.addEventListener 638 | // browsers because ioslides itself only supports 639 | // such browsers. 640 | on(document, "slideenter", resizeHandler); 641 | on(document, "slideleave", resizeHandler); 642 | } 643 | } 644 | 645 | var scriptData = document.querySelector("script[data-for='" + el.id + "'][type='application/json']"); 646 | if (scriptData) { 647 | var data = JSON.parse(scriptData.textContent || scriptData.text); 648 | // Resolve strings marked as javascript literals to objects 649 | if (!(data.evals instanceof Array)) data.evals = [data.evals]; 650 | for (var k = 0; data.evals && k < data.evals.length; k++) { 651 | window.HTMLWidgets.evaluateStringMember(data.x, data.evals[k]); 652 | } 653 | binding.renderValue(el, data.x, initResult); 654 | evalAndRun(data.jsHooks.render, initResult, [el, data.x]); 655 | } 656 | }); 657 | }); 658 | 659 | invokePostRenderHandlers(); 660 | } 661 | 662 | 663 | function has_jQuery3() { 664 | if (!window.jQuery) { 665 | return false; 666 | } 667 | var $version = window.jQuery.fn.jquery; 668 | var $major_version = parseInt($version.split(".")[0]); 669 | return $major_version >= 3; 670 | } 671 | 672 | /* 673 | / Shiny 1.4 bumped jQuery from 1.x to 3.x which means jQuery's 674 | / on-ready handler (i.e., $(fn)) is now asyncronous (i.e., it now 675 | / really means $(setTimeout(fn)). 676 | / https://jquery.com/upgrade-guide/3.0/#breaking-change-document-ready-handlers-are-now-asynchronous 677 | / 678 | / Since Shiny uses $() to schedule initShiny, shiny>=1.4 calls initShiny 679 | / one tick later than it did before, which means staticRender() is 680 | / called renderValue() earlier than (advanced) widget authors might be expecting. 681 | / https://github.com/rstudio/shiny/issues/2630 682 | / 683 | / For a concrete example, leaflet has some methods (e.g., updateBounds) 684 | / which reference Shiny methods registered in initShiny (e.g., setInputValue). 685 | / Since leaflet is privy to this life-cycle, it knows to use setTimeout() to 686 | / delay execution of those methods (until Shiny methods are ready) 687 | / https://github.com/rstudio/leaflet/blob/18ec981/javascript/src/index.js#L266-L268 688 | / 689 | / Ideally widget authors wouldn't need to use this setTimeout() hack that 690 | / leaflet uses to call Shiny methods on a staticRender(). In the long run, 691 | / the logic initShiny should be broken up so that method registration happens 692 | / right away, but binding happens later. 693 | */ 694 | function maybeStaticRenderLater() { 695 | if (shinyMode && has_jQuery3()) { 696 | window.jQuery(window.HTMLWidgets.staticRender); 697 | } else { 698 | window.HTMLWidgets.staticRender(); 699 | } 700 | } 701 | 702 | if (document.addEventListener) { 703 | document.addEventListener("DOMContentLoaded", function() { 704 | document.removeEventListener("DOMContentLoaded", arguments.callee, false); 705 | maybeStaticRenderLater(); 706 | }, false); 707 | } else if (document.attachEvent) { 708 | document.attachEvent("onreadystatechange", function() { 709 | if (document.readyState === "complete") { 710 | document.detachEvent("onreadystatechange", arguments.callee); 711 | maybeStaticRenderLater(); 712 | } 713 | }); 714 | } 715 | 716 | 717 | window.HTMLWidgets.getAttachmentUrl = function(depname, key) { 718 | // If no key, default to the first item 719 | if (typeof(key) === "undefined") 720 | key = 1; 721 | 722 | var link = document.getElementById(depname + "-" + key + "-attachment"); 723 | if (!link) { 724 | throw new Error("Attachment " + depname + "/" + key + " not found in document"); 725 | } 726 | return link.getAttribute("href"); 727 | }; 728 | 729 | window.HTMLWidgets.dataframeToD3 = function(df) { 730 | var names = []; 731 | var length; 732 | for (var name in df) { 733 | if (df.hasOwnProperty(name)) 734 | names.push(name); 735 | if (typeof(df[name]) !== "object" || typeof(df[name].length) === "undefined") { 736 | throw new Error("All fields must be arrays"); 737 | } else if (typeof(length) !== "undefined" && length !== df[name].length) { 738 | throw new Error("All fields must be arrays of the same length"); 739 | } 740 | length = df[name].length; 741 | } 742 | var results = []; 743 | var item; 744 | for (var row = 0; row < length; row++) { 745 | item = {}; 746 | for (var col = 0; col < names.length; col++) { 747 | item[names[col]] = df[names[col]][row]; 748 | } 749 | results.push(item); 750 | } 751 | return results; 752 | }; 753 | 754 | window.HTMLWidgets.transposeArray2D = function(array) { 755 | if (array.length === 0) return array; 756 | var newArray = array[0].map(function(col, i) { 757 | return array.map(function(row) { 758 | return row[i] 759 | }) 760 | }); 761 | return newArray; 762 | }; 763 | // Split value at splitChar, but allow splitChar to be escaped 764 | // using escapeChar. Any other characters escaped by escapeChar 765 | // will be included as usual (including escapeChar itself). 766 | function splitWithEscape(value, splitChar, escapeChar) { 767 | var results = []; 768 | var escapeMode = false; 769 | var currentResult = ""; 770 | for (var pos = 0; pos < value.length; pos++) { 771 | if (!escapeMode) { 772 | if (value[pos] === splitChar) { 773 | results.push(currentResult); 774 | currentResult = ""; 775 | } else if (value[pos] === escapeChar) { 776 | escapeMode = true; 777 | } else { 778 | currentResult += value[pos]; 779 | } 780 | } else { 781 | currentResult += value[pos]; 782 | escapeMode = false; 783 | } 784 | } 785 | if (currentResult !== "") { 786 | results.push(currentResult); 787 | } 788 | return results; 789 | } 790 | // Function authored by Yihui/JJ Allaire 791 | window.HTMLWidgets.evaluateStringMember = function(o, member) { 792 | var parts = splitWithEscape(member, '.', '\\'); 793 | for (var i = 0, l = parts.length; i < l; i++) { 794 | var part = parts[i]; 795 | // part may be a character or 'numeric' member name 796 | if (o !== null && typeof o === "object" && part in o) { 797 | if (i == (l - 1)) { // if we are at the end of the line then evalulate 798 | if (typeof o[part] === "string") 799 | o[part] = tryEval(o[part]); 800 | } else { // otherwise continue to next embedded object 801 | o = o[part]; 802 | } 803 | } 804 | } 805 | }; 806 | 807 | // Retrieve the HTMLWidget instance (i.e. the return value of an 808 | // HTMLWidget binding's initialize() or factory() function) 809 | // associated with an element, or null if none. 810 | window.HTMLWidgets.getInstance = function(el) { 811 | return elementData(el, "init_result"); 812 | }; 813 | 814 | // Finds the first element in the scope that matches the selector, 815 | // and returns the HTMLWidget instance (i.e. the return value of 816 | // an HTMLWidget binding's initialize() or factory() function) 817 | // associated with that element, if any. If no element matches the 818 | // selector, or the first matching element has no HTMLWidget 819 | // instance associated with it, then null is returned. 820 | // 821 | // The scope argument is optional, and defaults to window.document. 822 | window.HTMLWidgets.find = function(scope, selector) { 823 | if (arguments.length == 1) { 824 | selector = scope; 825 | scope = document; 826 | } 827 | 828 | var el = scope.querySelector(selector); 829 | if (el === null) { 830 | return null; 831 | } else { 832 | return window.HTMLWidgets.getInstance(el); 833 | } 834 | }; 835 | 836 | // Finds all elements in the scope that match the selector, and 837 | // returns the HTMLWidget instances (i.e. the return values of 838 | // an HTMLWidget binding's initialize() or factory() function) 839 | // associated with the elements, in an array. If elements that 840 | // match the selector don't have an associated HTMLWidget 841 | // instance, the returned array will contain nulls. 842 | // 843 | // The scope argument is optional, and defaults to window.document. 844 | window.HTMLWidgets.findAll = function(scope, selector) { 845 | if (arguments.length == 1) { 846 | selector = scope; 847 | scope = document; 848 | } 849 | 850 | var nodes = scope.querySelectorAll(selector); 851 | var results = []; 852 | for (var i = 0; i < nodes.length; i++) { 853 | results.push(window.HTMLWidgets.getInstance(nodes[i])); 854 | } 855 | return results; 856 | }; 857 | 858 | var postRenderHandlers = []; 859 | function invokePostRenderHandlers() { 860 | while (postRenderHandlers.length) { 861 | var handler = postRenderHandlers.shift(); 862 | if (handler) { 863 | handler(); 864 | } 865 | } 866 | } 867 | 868 | // Register the given callback function to be invoked after the 869 | // next time static widgets are rendered. 870 | window.HTMLWidgets.addPostRenderHandler = function(callback) { 871 | postRenderHandlers.push(callback); 872 | }; 873 | 874 | // Takes a new-style instance-bound definition, and returns an 875 | // old-style class-bound definition. This saves us from having 876 | // to rewrite all the logic in this file to accomodate both 877 | // types of definitions. 878 | function createLegacyDefinitionAdapter(defn) { 879 | var result = { 880 | name: defn.name, 881 | type: defn.type, 882 | initialize: function(el, width, height) { 883 | return defn.factory(el, width, height); 884 | }, 885 | renderValue: function(el, x, instance) { 886 | return instance.renderValue(x); 887 | }, 888 | resize: function(el, width, height, instance) { 889 | return instance.resize(width, height); 890 | } 891 | }; 892 | 893 | if (defn.find) 894 | result.find = defn.find; 895 | if (defn.renderError) 896 | result.renderError = defn.renderError; 897 | if (defn.clearError) 898 | result.clearError = defn.clearError; 899 | 900 | return result; 901 | } 902 | })(); 903 | 904 | -------------------------------------------------------------------------------- /outputs/hash_cloud_files/wordcloud2-0.0.1/wordcloud2-all.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * wordcloud2.js 3 | * http://timdream.org/wordcloud2.js/ 4 | * 5 | * Copyright 2011 - 2013 Tim Chien 6 | * Released under the MIT license 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // setImmediate 12 | if (!window.setImmediate) { 13 | window.setImmediate = (function setupSetImmediate() { 14 | return window.msSetImmediate || 15 | window.webkitSetImmediate || 16 | window.mozSetImmediate || 17 | window.oSetImmediate || 18 | (function setupSetZeroTimeout() { 19 | if (!window.postMessage || !window.addEventListener) { 20 | return null; 21 | } 22 | 23 | var callbacks = [undefined]; 24 | var message = 'zero-timeout-message'; 25 | 26 | // Like setTimeout, but only takes a function argument. There's 27 | // no time argument (always zero) and no arguments (you have to 28 | // use a closure). 29 | var setZeroTimeout = function setZeroTimeout(callback) { 30 | var id = callbacks.length; 31 | callbacks.push(callback); 32 | window.postMessage(message + id.toString(36), '*'); 33 | 34 | return id; 35 | }; 36 | 37 | window.addEventListener('message', function setZeroTimeoutMessage(evt) { 38 | // Skipping checking event source, retarded IE confused this window 39 | // object with another in the presence of iframe 40 | if (typeof evt.data !== 'string' || 41 | evt.data.substr(0, message.length) !== message/* || 42 | evt.source !== window */) { 43 | return; 44 | } 45 | 46 | evt.stopImmediatePropagation(); 47 | 48 | var id = parseInt(evt.data.substr(message.length), 36); 49 | if (!callbacks[id]) { 50 | return; 51 | } 52 | 53 | callbacks[id](); 54 | callbacks[id] = undefined; 55 | }, true); 56 | 57 | /* specify clearImmediate() here since we need the scope */ 58 | window.clearImmediate = function clearZeroTimeout(id) { 59 | if (!callbacks[id]) { 60 | return; 61 | } 62 | 63 | callbacks[id] = undefined; 64 | }; 65 | 66 | return setZeroTimeout; 67 | })() || 68 | // fallback 69 | function setImmediateFallback(fn) { 70 | window.setTimeout(fn, 0); 71 | }; 72 | })(); 73 | } 74 | 75 | if (!window.clearImmediate) { 76 | window.clearImmediate = (function setupClearImmediate() { 77 | return window.msClearImmediate || 78 | window.webkitClearImmediate || 79 | window.mozClearImmediate || 80 | window.oClearImmediate || 81 | // "clearZeroTimeout" is implement on the previous block || 82 | // fallback 83 | function clearImmediateFallback(timer) { 84 | window.clearTimeout(timer); 85 | }; 86 | })(); 87 | } 88 | 89 | (function(global) { 90 | 91 | // Check if WordCloud can run on this browser 92 | var isSupported = (function isSupported() { 93 | var canvas = document.createElement('canvas'); 94 | if (!canvas || !canvas.getContext) { 95 | return false; 96 | } 97 | 98 | var ctx = canvas.getContext('2d'); 99 | if (!ctx.getImageData) { 100 | return false; 101 | } 102 | if (!ctx.fillText) { 103 | return false; 104 | } 105 | 106 | if (!Array.prototype.some) { 107 | return false; 108 | } 109 | if (!Array.prototype.push) { 110 | return false; 111 | } 112 | 113 | return true; 114 | }()); 115 | 116 | // Find out if the browser impose minium font size by 117 | // drawing small texts on a canvas and measure it's width. 118 | var minFontSize = (function getMinFontSize() { 119 | if (!isSupported) { 120 | return; 121 | } 122 | 123 | var ctx = document.createElement('canvas').getContext('2d'); 124 | 125 | // start from 20 126 | var size = 20; 127 | 128 | // two sizes to measure 129 | var hanWidth, mWidth; 130 | 131 | while (size) { 132 | ctx.font = size.toString(10) + 'px sans-serif'; 133 | if ((ctx.measureText('\uFF37').width === hanWidth) && 134 | (ctx.measureText('m').width) === mWidth) { 135 | return (size + 1); 136 | } 137 | 138 | hanWidth = ctx.measureText('\uFF37').width; 139 | mWidth = ctx.measureText('m').width; 140 | 141 | size--; 142 | } 143 | 144 | return 0; 145 | })(); 146 | 147 | // Based on http://jsfromhell.com/array/shuffle 148 | var shuffleArray = function shuffleArray(arr) { 149 | for (var j, x, i = arr.length; i; 150 | j = Math.floor(Math.random() * i), 151 | x = arr[--i], arr[i] = arr[j], 152 | arr[j] = x) {} 153 | return arr; 154 | }; 155 | 156 | var WordCloud = function WordCloud(elements, options) { 157 | if (!isSupported) { 158 | return; 159 | } 160 | 161 | if (!Array.isArray(elements)) { 162 | elements = [elements]; 163 | } 164 | 165 | elements.forEach(function(el, i) { 166 | if (typeof el === 'string') { 167 | elements[i] = document.getElementById(el); 168 | if (!elements[i]) { 169 | throw 'The element id specified is not found.'; 170 | } 171 | } else if (!el.tagName && !el.appendChild) { 172 | throw 'You must pass valid HTML elements, or ID of the element.'; 173 | } 174 | }); 175 | 176 | /* Default values to be overwritten by options object */ 177 | var settings = { 178 | list: [], 179 | fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' + 180 | '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', 181 | fontWeight: 'normal', 182 | color: 'random-dark', 183 | minSize: 0, // 0 to disable 184 | weightFactor: 1, 185 | clearCanvas: true, 186 | backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) 187 | 188 | gridSize: 8, 189 | origin: null, 190 | 191 | drawMask: false, 192 | maskColor: 'rgba(255,0,0,0.3)', 193 | maskGapWidth: 0.3, 194 | 195 | wait: 0, 196 | abortThreshold: 0, // disabled 197 | abort: function noop() {}, 198 | 199 | minRotation: - Math.PI / 2, 200 | maxRotation: Math.PI / 2, 201 | 202 | shuffle: true, 203 | rotateRatio: 0.1, 204 | 205 | shape: 'circle', 206 | ellipticity: 0.65, 207 | 208 | classes: null, 209 | 210 | hover: null, 211 | click: null 212 | }; 213 | 214 | if (options) { 215 | for (var key in options) { 216 | if (key in settings) { 217 | settings[key] = options[key]; 218 | } 219 | } 220 | } 221 | 222 | /* Convert weightFactor into a function */ 223 | if (typeof settings.weightFactor !== 'function') { 224 | var factor = settings.weightFactor; 225 | settings.weightFactor = function weightFactor(pt) { 226 | return pt * factor; //in px 227 | }; 228 | } 229 | 230 | /* Convert shape into a function */ 231 | if (typeof settings.shape !== 'function') { 232 | switch (settings.shape) { 233 | case 'circle': 234 | /* falls through */ 235 | default: 236 | // 'circle' is the default and a shortcut in the code loop. 237 | settings.shape = 'circle'; 238 | break; 239 | 240 | case 'cardioid': 241 | settings.shape = function shapeCardioid(theta) { 242 | return 1 - Math.sin(theta); 243 | }; 244 | break; 245 | 246 | /* 247 | 248 | To work out an X-gon, one has to calculate "m", 249 | where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0)) 250 | http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28 251 | 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29 252 | 253 | Copy the solution into polar equation r = 1/(cos(t') + m*sin(t')) 254 | where t' equals to mod(t, 2PI/X); 255 | 256 | */ 257 | 258 | case 'diamond': 259 | case 'square': 260 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 261 | // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D 262 | // +0+..+2*PI 263 | settings.shape = function shapeSquare(theta) { 264 | var thetaPrime = theta % (2 * Math.PI / 4); 265 | return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)); 266 | }; 267 | break; 268 | 269 | case 'triangle-forward': 270 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 271 | // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29 272 | // %29%29%2C+t+%3D+0+..+2*PI 273 | settings.shape = function shapeTriangle(theta) { 274 | var thetaPrime = theta % (2 * Math.PI / 3); 275 | return 1 / (Math.cos(thetaPrime) + 276 | Math.sqrt(3) * Math.sin(thetaPrime)); 277 | }; 278 | break; 279 | 280 | case 'triangle': 281 | case 'triangle-upright': 282 | settings.shape = function shapeTriangle(theta) { 283 | var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3); 284 | return 1 / (Math.cos(thetaPrime) + 285 | Math.sqrt(3) * Math.sin(thetaPrime)); 286 | }; 287 | break; 288 | 289 | case 'pentagon': 290 | settings.shape = function shapePentagon(theta) { 291 | var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5); 292 | return 1 / (Math.cos(thetaPrime) + 293 | 0.726543 * Math.sin(thetaPrime)); 294 | }; 295 | break; 296 | 297 | case 'star': 298 | settings.shape = function shapeStar(theta) { 299 | var thetaPrime = (theta + 0.955) % (2 * Math.PI / 10); 300 | if ((theta + 0.955) % (2 * Math.PI / 5) - (2 * Math.PI / 10) >= 0) { 301 | return 1 / (Math.cos((2 * Math.PI / 10) - thetaPrime) + 302 | 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime)); 303 | } else { 304 | return 1 / (Math.cos(thetaPrime) + 305 | 3.07768 * Math.sin(thetaPrime)); 306 | } 307 | }; 308 | break; 309 | } 310 | } 311 | 312 | /* Make sure gridSize is a whole number and is not smaller than 4px */ 313 | settings.gridSize = Math.max(Math.floor(settings.gridSize), 4); 314 | 315 | /* shorthand */ 316 | var g = settings.gridSize; 317 | var maskRectWidth = g - settings.maskGapWidth; 318 | 319 | /* normalize rotation settings */ 320 | var rotationRange = Math.abs(settings.maxRotation - settings.minRotation); 321 | var minRotation = Math.min(settings.maxRotation, settings.minRotation); 322 | 323 | /* information/object available to all functions, set when start() */ 324 | var grid, // 2d array containing filling information 325 | ngx, ngy, // width and height of the grid 326 | center, // position of the center of the cloud 327 | maxRadius; 328 | 329 | /* timestamp for measuring each putWord() action */ 330 | var escapeTime; 331 | 332 | /* function for getting the color of the text */ 333 | var getTextColor; 334 | function random_hsl_color(min, max) { 335 | return 'hsl(' + 336 | (Math.random() * 360).toFixed() + ',' + 337 | (Math.random() * 30 + 70).toFixed() + '%,' + 338 | (Math.random() * (max - min) + min).toFixed() + '%)'; 339 | } 340 | switch (settings.color) { 341 | case 'random-dark': 342 | getTextColor = function getRandomDarkColor() { 343 | return random_hsl_color(10, 50); 344 | }; 345 | break; 346 | 347 | case 'random-light': 348 | getTextColor = function getRandomLightColor() { 349 | return random_hsl_color(50, 90); 350 | }; 351 | break; 352 | 353 | default: 354 | if (typeof settings.color === 'function') { 355 | getTextColor = settings.color; 356 | } 357 | break; 358 | } 359 | 360 | /* function for getting the classes of the text */ 361 | var getTextClasses = null; 362 | if (typeof settings.classes === 'function') { 363 | getTextClasses = settings.classes; 364 | } 365 | 366 | /* Interactive */ 367 | var interactive = false; 368 | var infoGrid = []; 369 | var hovered; 370 | 371 | var getInfoGridFromMouseTouchEvent = 372 | function getInfoGridFromMouseTouchEvent(evt) { 373 | var canvas = evt.currentTarget; 374 | var rect = canvas.getBoundingClientRect(); 375 | var clientX; 376 | var clientY; 377 | /** Detect if touches are available */ 378 | if (evt.touches) { 379 | clientX = evt.touches[0].clientX; 380 | clientY = evt.touches[0].clientY; 381 | } else { 382 | clientX = evt.clientX; 383 | clientY = evt.clientY; 384 | } 385 | var eventX = clientX - rect.left; 386 | var eventY = clientY - rect.top; 387 | 388 | var x = Math.floor(eventX * ((canvas.width / rect.width) || 1) / g); 389 | var y = Math.floor(eventY * ((canvas.height / rect.height) || 1) / g); 390 | 391 | return infoGrid[x][y]; 392 | }; 393 | 394 | var wordcloudhover = function wordcloudhover(evt) { 395 | var info = getInfoGridFromMouseTouchEvent(evt); 396 | 397 | if (hovered === info) { 398 | return; 399 | } 400 | 401 | hovered = info; 402 | if (!info) { 403 | settings.hover(undefined, undefined, evt); 404 | 405 | return; 406 | } 407 | 408 | settings.hover(info.item, info.dimension, evt); 409 | 410 | }; 411 | 412 | var wordcloudclick = function wordcloudclick(evt) { 413 | var info = getInfoGridFromMouseTouchEvent(evt); 414 | if (!info) { 415 | return; 416 | } 417 | 418 | settings.click(info.item, info.dimension, evt); 419 | evt.preventDefault(); 420 | }; 421 | 422 | /* Get points on the grid for a given radius away from the center */ 423 | var pointsAtRadius = []; 424 | var getPointsAtRadius = function getPointsAtRadius(radius) { 425 | if (pointsAtRadius[radius]) { 426 | return pointsAtRadius[radius]; 427 | } 428 | 429 | // Look for these number of points on each radius 430 | var T = radius * 8; 431 | 432 | // Getting all the points at this radius 433 | var t = T; 434 | var points = []; 435 | 436 | if (radius === 0) { 437 | points.push([center[0], center[1], 0]); 438 | } 439 | 440 | while (t--) { 441 | // distort the radius to put the cloud in shape 442 | var rx = 1; 443 | if (settings.shape !== 'circle') { 444 | rx = settings.shape(t / T * 2 * Math.PI); // 0 to 1 445 | } 446 | 447 | // Push [x, y, t]; t is used solely for getTextColor() 448 | points.push([ 449 | center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI), 450 | center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) * 451 | settings.ellipticity, 452 | t / T * 2 * Math.PI]); 453 | } 454 | 455 | pointsAtRadius[radius] = points; 456 | return points; 457 | }; 458 | 459 | /* Return true if we had spent too much time */ 460 | var exceedTime = function exceedTime() { 461 | return ((settings.abortThreshold > 0) && 462 | ((new Date()).getTime() - escapeTime > settings.abortThreshold)); 463 | }; 464 | 465 | /* Get the deg of rotation according to settings, and luck. */ 466 | var getRotateDeg = function getRotateDeg() { 467 | if (settings.rotateRatio === 0) { 468 | return 0; 469 | } 470 | 471 | if (Math.random() > settings.rotateRatio) { 472 | return 0; 473 | } 474 | 475 | if (rotationRange === 0) { 476 | return minRotation; 477 | } 478 | 479 | return minRotation + Math.random() * rotationRange; 480 | }; 481 | 482 | var getTextInfo = function getTextInfo(word, weight, rotateDeg) { 483 | // calculate the acutal font size 484 | // fontSize === 0 means weightFactor function wants the text skipped, 485 | // and size < minSize means we cannot draw the text. 486 | var debug = false; 487 | var fontSize = settings.weightFactor(weight); 488 | if (fontSize <= settings.minSize) { 489 | return false; 490 | } 491 | 492 | // Scale factor here is to make sure fillText is not limited by 493 | // the minium font size set by browser. 494 | // It will always be 1 or 2n. 495 | var mu = 1; 496 | if (fontSize < minFontSize) { 497 | mu = (function calculateScaleFactor() { 498 | var mu = 2; 499 | while (mu * fontSize < minFontSize) { 500 | mu += 2; 501 | } 502 | return mu; 503 | })(); 504 | } 505 | 506 | var fcanvas = document.createElement('canvas'); 507 | var fctx = fcanvas.getContext('2d', { willReadFrequently: true }); 508 | 509 | fctx.font = settings.fontWeight + ' ' + 510 | (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; 511 | 512 | // Estimate the dimension of the text with measureText(). 513 | var fw = fctx.measureText(word).width / mu; 514 | var fh = Math.max(fontSize * mu, 515 | fctx.measureText('m').width, 516 | fctx.measureText('\uFF37').width) / mu; 517 | 518 | // Create a boundary box that is larger than our estimates, 519 | // so text don't get cut of (it sill might) 520 | var boxWidth = fw + fh * 2; 521 | var boxHeight = fh * 3; 522 | var fgw = Math.ceil(boxWidth / g); 523 | var fgh = Math.ceil(boxHeight / g); 524 | boxWidth = fgw * g; 525 | boxHeight = fgh * g; 526 | 527 | // Calculate the proper offsets to make the text centered at 528 | // the preferred position. 529 | 530 | // This is simply half of the width. 531 | var fillTextOffsetX = - fw / 2; 532 | // Instead of moving the box to the exact middle of the preferred 533 | // position, for Y-offset we move 0.4 instead, so Latin alphabets look 534 | // vertical centered. 535 | var fillTextOffsetY = - fh * 0.4; 536 | 537 | // Calculate the actual dimension of the canvas, considering the rotation. 538 | var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) + 539 | boxHeight * Math.abs(Math.cos(rotateDeg))) / g); 540 | var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) + 541 | boxHeight * Math.abs(Math.sin(rotateDeg))) / g); 542 | var width = cgw * g; 543 | var height = cgh * g; 544 | 545 | fcanvas.setAttribute('width', width); 546 | fcanvas.setAttribute('height', height); 547 | 548 | if (debug) { 549 | // Attach fcanvas to the DOM 550 | document.body.appendChild(fcanvas); 551 | // Save it's state so that we could restore and draw the grid correctly. 552 | fctx.save(); 553 | } 554 | 555 | // Scale the canvas with |mu|. 556 | fctx.scale(1 / mu, 1 / mu); 557 | fctx.translate(width * mu / 2, height * mu / 2); 558 | fctx.rotate(- rotateDeg); 559 | 560 | // Once the width/height is set, ctx info will be reset. 561 | // Set it again here. 562 | fctx.font = settings.fontWeight + ' ' + 563 | (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; 564 | 565 | // Fill the text into the fcanvas. 566 | // XXX: We cannot because textBaseline = 'top' here because 567 | // Firefox and Chrome uses different default line-height for canvas. 568 | // Please read https://bugzil.la/737852#c6. 569 | // Here, we use textBaseline = 'middle' and draw the text at exactly 570 | // 0.5 * fontSize lower. 571 | fctx.fillStyle = '#000'; 572 | fctx.textBaseline = 'middle'; 573 | fctx.fillText(word, fillTextOffsetX * mu, 574 | (fillTextOffsetY + fontSize * 0.5) * mu); 575 | 576 | // Get the pixels of the text 577 | var imageData = fctx.getImageData(0, 0, width, height).data; 578 | 579 | if (exceedTime()) { 580 | return false; 581 | } 582 | 583 | if (debug) { 584 | // Draw the box of the original estimation 585 | fctx.strokeRect(fillTextOffsetX * mu, 586 | fillTextOffsetY, fw * mu, fh * mu); 587 | fctx.restore(); 588 | } 589 | 590 | // Read the pixels and save the information to the occupied array 591 | var occupied = []; 592 | var gx = cgw, gy, x, y; 593 | var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; 594 | while (gx--) { 595 | gy = cgh; 596 | while (gy--) { 597 | y = g; 598 | singleGridLoop: { 599 | while (y--) { 600 | x = g; 601 | while (x--) { 602 | if (imageData[((gy * g + y) * width + 603 | (gx * g + x)) * 4 + 3]) { 604 | occupied.push([gx, gy]); 605 | 606 | if (gx < bounds[3]) { 607 | bounds[3] = gx; 608 | } 609 | if (gx > bounds[1]) { 610 | bounds[1] = gx; 611 | } 612 | if (gy < bounds[0]) { 613 | bounds[0] = gy; 614 | } 615 | if (gy > bounds[2]) { 616 | bounds[2] = gy; 617 | } 618 | 619 | if (debug) { 620 | fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; 621 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 622 | } 623 | break singleGridLoop; 624 | } 625 | } 626 | } 627 | if (debug) { 628 | fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; 629 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 630 | } 631 | } 632 | } 633 | } 634 | 635 | if (debug) { 636 | fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; 637 | fctx.fillRect(bounds[3] * g, 638 | bounds[0] * g, 639 | (bounds[1] - bounds[3] + 1) * g, 640 | (bounds[2] - bounds[0] + 1) * g); 641 | } 642 | 643 | // Return information needed to create the text on the real canvas 644 | return { 645 | mu: mu, 646 | occupied: occupied, 647 | bounds: bounds, 648 | gw: cgw, 649 | gh: cgh, 650 | fillTextOffsetX: fillTextOffsetX, 651 | fillTextOffsetY: fillTextOffsetY, 652 | fillTextWidth: fw, 653 | fillTextHeight: fh, 654 | fontSize: fontSize 655 | }; 656 | }; 657 | 658 | /* Determine if there is room available in the given dimension */ 659 | var canFitText = function canFitText(gx, gy, gw, gh, occupied) { 660 | // Go through the occupied points, 661 | // return false if the space is not available. 662 | var i = occupied.length; 663 | while (i--) { 664 | var px = gx + occupied[i][0]; 665 | var py = gy + occupied[i][1]; 666 | 667 | if (px >= ngx || py >= ngy || px < 0 || py < 0 || !grid[px][py]) { 668 | return false; 669 | } 670 | } 671 | return true; 672 | }; 673 | 674 | /* Actually draw the text on the grid */ 675 | var drawText = function drawText(gx, gy, info, word, weight, 676 | distance, theta, rotateDeg, attributes) { 677 | 678 | var fontSize = info.fontSize; 679 | var color; 680 | if (getTextColor) { 681 | color = getTextColor(word, weight, fontSize, distance, theta); 682 | } else if (settings.color instanceof Array) { 683 | color = settings.color.shift() || 'black'; // pass a array in setting, default 'black' 684 | } else { 685 | color = settings.color; 686 | } 687 | 688 | var classes; 689 | if (getTextClasses) { 690 | classes = getTextClasses(word, weight, fontSize, distance, theta); 691 | } else { 692 | classes = settings.classes; 693 | } 694 | 695 | var dimension; 696 | var bounds = info.bounds; 697 | dimension = { 698 | x: (gx + bounds[3]) * g, 699 | y: (gy + bounds[0]) * g, 700 | w: (bounds[1] - bounds[3] + 1) * g, 701 | h: (bounds[2] - bounds[0] + 1) * g 702 | }; 703 | 704 | elements.forEach(function(el) { 705 | if (el.getContext) { 706 | var ctx = el.getContext('2d'); 707 | var mu = info.mu; 708 | 709 | // Save the current state before messing it 710 | ctx.save(); 711 | ctx.scale(1 / mu, 1 / mu); 712 | 713 | ctx.font = settings.fontWeight + ' ' + 714 | (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; 715 | ctx.fillStyle = color; 716 | 717 | // Translate the canvas position to the origin coordinate of where 718 | // the text should be put. 719 | ctx.translate((gx + info.gw / 2) * g * mu, 720 | (gy + info.gh / 2) * g * mu); 721 | 722 | if (rotateDeg !== 0) { 723 | ctx.rotate(- rotateDeg); 724 | } 725 | 726 | // Finally, fill the text. 727 | 728 | // XXX: We cannot because textBaseline = 'top' here because 729 | // Firefox and Chrome uses different default line-height for canvas. 730 | // Please read https://bugzil.la/737852#c6. 731 | // Here, we use textBaseline = 'middle' and draw the text at exactly 732 | // 0.5 * fontSize lower. 733 | ctx.textBaseline = 'middle'; 734 | ctx.fillText(word, info.fillTextOffsetX * mu, 735 | (info.fillTextOffsetY + fontSize * 0.5) * mu); 736 | 737 | // The below box is always matches how s are positioned 738 | /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY, 739 | info.fillTextWidth, info.fillTextHeight); */ 740 | 741 | // Restore the state. 742 | ctx.restore(); 743 | } else { 744 | // drawText on DIV element 745 | var span = document.createElement('span'); 746 | var transformRule = ''; 747 | transformRule = 'rotate(' + (- rotateDeg / Math.PI * 180) + 'deg) '; 748 | if (info.mu !== 1) { 749 | transformRule += 750 | 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' + 751 | 'scale(' + (1 / info.mu) + ')'; 752 | } 753 | var styleRules = { 754 | 'position': 'absolute', 755 | 'display': 'block', 756 | 'font': settings.fontWeight + ' ' + 757 | (fontSize * info.mu) + 'px ' + settings.fontFamily, 758 | 'left': ((gx + info.gw / 2) * g + info.fillTextOffsetX) + 'px', 759 | 'top': ((gy + info.gh / 2) * g + info.fillTextOffsetY) + 'px', 760 | 'width': info.fillTextWidth + 'px', 761 | 'height': info.fillTextHeight + 'px', 762 | 'lineHeight': fontSize + 'px', 763 | 'whiteSpace': 'nowrap', 764 | 'transform': transformRule, 765 | 'webkitTransform': transformRule, 766 | 'msTransform': transformRule, 767 | 'transformOrigin': '50% 40%', 768 | 'webkitTransformOrigin': '50% 40%', 769 | 'msTransformOrigin': '50% 40%' 770 | }; 771 | if (color) { 772 | styleRules.color = color; 773 | } 774 | span.textContent = word; 775 | for (var cssProp in styleRules) { 776 | span.style[cssProp] = styleRules[cssProp]; 777 | } 778 | if (attributes) { 779 | for (var attribute in attributes) { 780 | span.setAttribute(attribute, attributes[attribute]); 781 | } 782 | } 783 | if (classes) { 784 | span.className += classes; 785 | } 786 | el.appendChild(span); 787 | } 788 | }); 789 | }; 790 | 791 | /* Help function to updateGrid */ 792 | var fillGridAt = function fillGridAt(x, y, drawMask, dimension, item) { 793 | if (x >= ngx || y >= ngy || x < 0 || y < 0) { 794 | return; 795 | } 796 | 797 | grid[x][y] = false; 798 | 799 | if (drawMask) { 800 | var ctx = elements[0].getContext('2d'); 801 | ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth); 802 | } 803 | 804 | if (interactive) { 805 | infoGrid[x][y] = { item: item, dimension: dimension }; 806 | } 807 | }; 808 | 809 | /* Update the filling information of the given space with occupied points. 810 | Draw the mask on the canvas if necessary. */ 811 | var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) { 812 | var occupied = info.occupied; 813 | var drawMask = settings.drawMask; 814 | var ctx; 815 | if (drawMask) { 816 | ctx = elements[0].getContext('2d'); 817 | ctx.save(); 818 | ctx.fillStyle = settings.maskColor; 819 | } 820 | 821 | var dimension; 822 | if (interactive) { 823 | var bounds = info.bounds; 824 | dimension = { 825 | x: (gx + bounds[3]) * g, 826 | y: (gy + bounds[0]) * g, 827 | w: (bounds[1] - bounds[3] + 1) * g, 828 | h: (bounds[2] - bounds[0] + 1) * g 829 | }; 830 | } 831 | 832 | var i = occupied.length; 833 | while (i--) { 834 | fillGridAt(gx + occupied[i][0], gy + occupied[i][1], 835 | drawMask, dimension, item); 836 | } 837 | 838 | if (drawMask) { 839 | ctx.restore(); 840 | } 841 | }; 842 | 843 | /* putWord() processes each item on the list, 844 | calculate it's size and determine it's position, and actually 845 | put it on the canvas. */ 846 | var putWord = function putWord(item) { 847 | var word, weight, attributes; 848 | if (Array.isArray(item)) { 849 | word = item[0]; 850 | weight = item[1]; 851 | } else { 852 | word = item.word; 853 | weight = item.weight; 854 | attributes = item.attributes; 855 | } 856 | var rotateDeg = getRotateDeg(); 857 | 858 | // get info needed to put the text onto the canvas 859 | var info = getTextInfo(word, weight, rotateDeg); 860 | 861 | // not getting the info means we shouldn't be drawing this one. 862 | if (!info) { 863 | return false; 864 | } 865 | 866 | if (exceedTime()) { 867 | return false; 868 | } 869 | 870 | // Skip the loop if we have already know the bounding box of 871 | // word is larger than the canvas. 872 | var bounds = info.bounds; 873 | if ((bounds[1] - bounds[3] + 1) > ngx || 874 | (bounds[2] - bounds[0] + 1) > ngy) { 875 | return false; 876 | } 877 | 878 | // Determine the position to put the text by 879 | // start looking for the nearest points 880 | var r = maxRadius + 1; 881 | 882 | var tryToPutWordAtPoint = function(gxy) { 883 | var gx = Math.floor(gxy[0] - info.gw / 2); 884 | var gy = Math.floor(gxy[1] - info.gh / 2); 885 | var gw = info.gw; 886 | var gh = info.gh; 887 | 888 | // If we cannot fit the text at this position, return false 889 | // and go to the next position. 890 | if (!canFitText(gx, gy, gw, gh, info.occupied)) { 891 | return false; 892 | } 893 | 894 | // Actually put the text on the canvas 895 | drawText(gx, gy, info, word, weight, 896 | (maxRadius - r), gxy[2], rotateDeg, attributes); 897 | 898 | // Mark the spaces on the grid as filled 899 | updateGrid(gx, gy, gw, gh, info, item); 900 | 901 | // Return true so some() will stop and also return true. 902 | return true; 903 | }; 904 | 905 | while (r--) { 906 | var points = getPointsAtRadius(maxRadius - r); 907 | 908 | if (settings.shuffle) { 909 | points = [].concat(points); 910 | shuffleArray(points); 911 | } 912 | 913 | // Try to fit the words by looking at each point. 914 | // array.some() will stop and return true 915 | // when putWordAtPoint() returns true. 916 | // If all the points returns false, array.some() returns false. 917 | var drawn = points.some(tryToPutWordAtPoint); 918 | 919 | if (drawn) { 920 | // leave putWord() and return true 921 | return true; 922 | } 923 | } 924 | // we tried all distances but text won't fit, return false 925 | return false; 926 | }; 927 | 928 | /* Send DOM event to all elements. Will stop sending event and return 929 | if the previous one is canceled (for cancelable events). */ 930 | var sendEvent = function sendEvent(type, cancelable, detail) { 931 | if (cancelable) { 932 | return !elements.some(function(el) { 933 | var evt = document.createEvent('CustomEvent'); 934 | evt.initCustomEvent(type, true, cancelable, detail || {}); 935 | return !el.dispatchEvent(evt); 936 | }, this); 937 | } else { 938 | elements.forEach(function(el) { 939 | var evt = document.createEvent('CustomEvent'); 940 | evt.initCustomEvent(type, true, cancelable, detail || {}); 941 | el.dispatchEvent(evt); 942 | }, this); 943 | } 944 | }; 945 | 946 | /* Start drawing on a canvas */ 947 | var start = function start() { 948 | // For dimensions, clearCanvas etc., 949 | // we only care about the first element. 950 | var canvas = elements[0]; 951 | 952 | if (canvas.getContext) { 953 | ngx = Math.floor(canvas.width / g); 954 | ngy = Math.floor(canvas.height / g); 955 | } else { 956 | var rect = canvas.getBoundingClientRect(); 957 | ngx = Math.floor(rect.width / g); 958 | ngy = Math.floor(rect.height / g); 959 | } 960 | 961 | // Sending a wordcloudstart event which cause the previous loop to stop. 962 | // Do nothing if the event is canceled. 963 | if (!sendEvent('wordcloudstart', true)) { 964 | return; 965 | } 966 | 967 | // Determine the center of the word cloud 968 | center = (settings.origin) ? 969 | [settings.origin[0]/g, settings.origin[1]/g] : 970 | [ngx / 2, ngy / 2]; 971 | 972 | // Maxium radius to look for space 973 | maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy)); 974 | 975 | /* Clear the canvas only if the clearCanvas is set, 976 | if not, update the grid to the current canvas state */ 977 | grid = []; 978 | 979 | var gx, gy, i; 980 | if (!canvas.getContext || settings.clearCanvas) { 981 | elements.forEach(function(el) { 982 | if (el.getContext) { 983 | var ctx = el.getContext('2d'); 984 | ctx.fillStyle = settings.backgroundColor; 985 | ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 986 | ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 987 | } else { 988 | el.textContent = ''; 989 | el.style.backgroundColor = settings.backgroundColor; 990 | } 991 | }); 992 | 993 | /* fill the grid with empty state */ 994 | gx = ngx; 995 | while (gx--) { 996 | grid[gx] = []; 997 | gy = ngy; 998 | while (gy--) { 999 | grid[gx][gy] = true; 1000 | } 1001 | } 1002 | } else { 1003 | /* Determine bgPixel by creating 1004 | another canvas and fill the specified background color. */ 1005 | var bctx = document.createElement('canvas').getContext('2d'); 1006 | 1007 | bctx.fillStyle = settings.backgroundColor; 1008 | bctx.fillRect(0, 0, 1, 1); 1009 | var bgPixel = bctx.getImageData(0, 0, 1, 1).data; 1010 | 1011 | /* Read back the pixels of the canvas we got to tell which part of the 1012 | canvas is empty. 1013 | (no clearCanvas only works with a canvas, not divs) */ 1014 | var imageData = 1015 | canvas.getContext('2d').getImageData(0, 0, ngx * g, ngy * g).data; 1016 | 1017 | gx = ngx; 1018 | var x, y; 1019 | while (gx--) { 1020 | grid[gx] = []; 1021 | gy = ngy; 1022 | while (gy--) { 1023 | y = g; 1024 | singleGridLoop: while (y--) { 1025 | x = g; 1026 | while (x--) { 1027 | i = 4; 1028 | while (i--) { 1029 | if (imageData[((gy * g + y) * ngx * g + 1030 | (gx * g + x)) * 4 + i] !== bgPixel[i]) { 1031 | grid[gx][gy] = false; 1032 | break singleGridLoop; 1033 | } 1034 | } 1035 | } 1036 | } 1037 | if (grid[gx][gy] !== false) { 1038 | grid[gx][gy] = true; 1039 | } 1040 | } 1041 | } 1042 | 1043 | imageData = bctx = bgPixel = undefined; 1044 | } 1045 | 1046 | // fill the infoGrid with empty state if we need it 1047 | if (settings.hover || settings.click) { 1048 | 1049 | interactive = true; 1050 | 1051 | /* fill the grid with empty state */ 1052 | gx = ngx + 1; 1053 | while (gx--) { 1054 | infoGrid[gx] = []; 1055 | } 1056 | 1057 | if (settings.hover) { 1058 | canvas.addEventListener('mousemove', wordcloudhover); 1059 | } 1060 | 1061 | if (settings.click) { 1062 | canvas.addEventListener('click', wordcloudclick); 1063 | canvas.addEventListener('touchstart', wordcloudclick); 1064 | canvas.addEventListener('touchend', function (e) { 1065 | e.preventDefault(); 1066 | }); 1067 | canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'; 1068 | } 1069 | 1070 | canvas.addEventListener('wordcloudstart', function stopInteraction() { 1071 | canvas.removeEventListener('wordcloudstart', stopInteraction); 1072 | 1073 | canvas.removeEventListener('mousemove', wordcloudhover); 1074 | canvas.removeEventListener('click', wordcloudclick); 1075 | hovered = undefined; 1076 | }); 1077 | } 1078 | 1079 | i = 0; 1080 | var loopingFunction, stoppingFunction; 1081 | if (settings.wait !== 0) { 1082 | loopingFunction = window.setTimeout; 1083 | stoppingFunction = window.clearTimeout; 1084 | } else { 1085 | loopingFunction = window.setImmediate; 1086 | stoppingFunction = window.clearImmediate; 1087 | } 1088 | 1089 | var addEventListener = function addEventListener(type, listener) { 1090 | elements.forEach(function(el) { 1091 | el.addEventListener(type, listener); 1092 | }, this); 1093 | }; 1094 | 1095 | var removeEventListener = function removeEventListener(type, listener) { 1096 | elements.forEach(function(el) { 1097 | el.removeEventListener(type, listener); 1098 | }, this); 1099 | }; 1100 | 1101 | var anotherWordCloudStart = function anotherWordCloudStart() { 1102 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1103 | stoppingFunction(timer); 1104 | }; 1105 | 1106 | addEventListener('wordcloudstart', anotherWordCloudStart); 1107 | 1108 | var timer = loopingFunction(function loop() { 1109 | if (i >= settings.list.length) { 1110 | stoppingFunction(timer); 1111 | sendEvent('wordcloudstop', false); 1112 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1113 | 1114 | return; 1115 | } 1116 | escapeTime = (new Date()).getTime(); 1117 | var drawn = putWord(settings.list[i]); 1118 | var canceled = !sendEvent('wordclouddrawn', true, { 1119 | item: settings.list[i], drawn: drawn }); 1120 | if (exceedTime() || canceled) { 1121 | stoppingFunction(timer); 1122 | settings.abort(); 1123 | sendEvent('wordcloudabort', false); 1124 | sendEvent('wordcloudstop', false); 1125 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1126 | return; 1127 | } 1128 | i++; 1129 | timer = loopingFunction(loop, settings.wait); 1130 | }, settings.wait); 1131 | }; 1132 | 1133 | // All set, start the drawing 1134 | start(); 1135 | }; 1136 | 1137 | WordCloud.isSupported = isSupported; 1138 | WordCloud.minFontSize = minFontSize; 1139 | 1140 | // Expose the library as an AMD module 1141 | if (typeof define === 'function' && define.amd) { 1142 | define('wordcloud', [], function() { return WordCloud; }); 1143 | } else if (typeof module !== 'undefined' && module.exports) { 1144 | module.exports = WordCloud; 1145 | } else { 1146 | global.WordCloud = WordCloud; 1147 | } 1148 | 1149 | })(this); //jshint ignore:line 1150 | --------------------------------------------------------------------------------