├── .coveragerc
├── .flake8
├── .gitignore
├── .hound.yml
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── INSTALL.md
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── Procfile
├── README.md
├── analyser.py
├── archive-analyser.ipynb
├── dev-requirements.txt
├── docs
├── gender_reply.png
└── tweet_class.png
├── manage.py
├── new_tweet_subset.js
├── static
└── foo
├── tasks.py
├── test_archive_2016_2017_2_months.zip
├── tweet_display
├── __init__.py
├── admin.py
├── analyse_data.py
├── apps.py
├── helper.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_graph_open_humans_member.py
│ └── __init__.py
├── models.py
├── read_data.py
├── static
│ ├── css
│ │ ├── logo.svg
│ │ └── metricsgraphics.css
│ ├── favicon.ico
│ ├── javascripts
│ │ └── leaflet.timeline.js
│ └── profile.jpg
├── tasks.py
├── templates
│ └── tweet_display
│ │ ├── application.html
│ │ ├── index.html
│ │ ├── interactions.html
│ │ ├── location.html
│ │ └── partials
│ │ ├── graph_buttons.html
│ │ ├── graph_in_making.html
│ │ └── graph_status.html
├── tests
│ ├── __init__.py
│ └── tests_data.py
├── urls.py
└── views.py
├── twitteranalyser
├── __init__.py
├── apps.py
├── celery.py
├── settings.py
├── templates
│ └── twitteranalyser
│ │ └── about.html
├── tests
│ ├── __init__.py
│ └── tests_views.py
├── urls.py
├── views.py
└── wsgi.py
└── users
├── __init__.py
├── admin.py
├── apps.py
├── forms.py
├── migrations
├── 0001_initial.py
├── 0002_openhumansmember_public.py
└── __init__.py
├── models.py
├── templates
└── users
│ ├── complete.html
│ ├── dashboard.html
│ ├── index.html
│ ├── partials
│ └── upload_form.html
│ ├── public_data.html
│ └── upload_old.html
├── tests
├── __init__.py
└── tests_views.py
├── urls.py
└── views.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | */__init__.py
4 | */tests/*
5 | */migration/*
6 | manage.py
7 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | .git,
4 | __pycache__,
5 | docs,
6 | migrations
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv
2 | *.pyc
3 | staticfiles
4 | .env
5 | .env.staging
6 | .ipynb_checkpoints
7 | twitter_archive/
8 | db.sqlite3
9 | dump.rdb
10 | .DS_Store
11 | data/
12 |
--------------------------------------------------------------------------------
/.hound.yml:
--------------------------------------------------------------------------------
1 | python:
2 | enabled: true
3 | config_file: .flake8
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | services:
4 | - redis-server
5 | - postgresql
6 |
7 |
8 | python:
9 | - "3.5"
10 | - "3.6"
11 |
12 | install:
13 | - pipenv install --dev
14 |
15 | before_script:
16 | - mkdir bin
17 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > bin/cc-test-reporter
18 | - chmod +x bin/cc-test-reporter
19 | - bin/cc-test-reporter before-build
20 |
21 | script:
22 | - python manage.py test
23 | - coverage run --source="." manage.py test
24 |
25 | after_success:
26 | - coverage xml
27 | - bin/cc-test-reporter after-build -t coverage.py
28 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## 1. Purpose
4 |
5 | A primary goal of TwArχiv is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
6 |
7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.
8 |
9 | We invite all those who participate in TwArχiv to help us create safe and positive experiences for everyone.
10 |
11 | ## 2. Open Source Citizenship
12 |
13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
14 |
15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
16 |
17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.
18 |
19 | ## 3. Expected Behavior
20 |
21 | The following behaviors are expected and requested of all community members:
22 |
23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
24 | * Exercise consideration and respect in your speech and actions.
25 | * Attempt collaboration before conflict.
26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech.
27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
29 |
30 | ## 4. Unacceptable Behavior
31 |
32 | The following behaviors are considered harassment and are unacceptable within our community:
33 |
34 | * Violence, threats of violence or violent language directed against another person.
35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
36 | * Posting or displaying sexually explicit or violent material.
37 | * Posting or threatening to post other people’s personally identifying information ("doxing").
38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
39 | * Inappropriate photography or recording.
40 | * Inappropriate physical contact. You should have someone’s consent before touching them.
41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
42 | * Deliberate intimidation, stalking or following (online or in person).
43 | * Advocating for, or encouraging, any of the above behavior.
44 | * Sustained disruption of community events, including talks and presentations.
45 |
46 | ## 5. Consequences of Unacceptable Behavior
47 |
48 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
49 |
50 | Anyone asked to stop unacceptable behavior is expected to comply immediately.
51 |
52 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).
53 |
54 | ## 6. Reporting Guidelines
55 |
56 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. bgreshake@googlemail.com.
57 |
58 |
59 |
60 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
61 |
62 | ## 7. Addressing Grievances
63 |
64 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Bastian Greshake Tzovaras with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
65 |
66 |
67 |
68 | ## 8. Scope
69 |
70 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business.
71 |
72 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.
73 |
74 | ## 9. Contact info
75 |
76 | bgreshake@googlemail.com
77 |
78 | ## 10. License and attribution
79 |
80 | This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
81 |
82 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
83 |
84 | Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/)
85 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 | 
3 |
4 | 🎉 Thanks so much for your interest in contributing! You're the best! 🎈
5 |
6 | Right now we don't have a detailed plan of what we want to work on next. But here are some important things to know.
7 |
8 | ### Overview
9 | 1. We do [have a Code of Conduct](https://github.com/gedankenstuecke/twitter-analyser/blob/master/CODE_OF_CONDUCT.md) that you should have read. 👍
10 | 2. Have a look at the open issues to see what could be improved.
11 |
12 | ### Missing things
13 | 1. We don't have any tests so far. If you feel that that is something you enjoy: Please go ahead! 😎
14 | 2. More data analysis!
15 |
16 |
17 | ### Making some $£¥€
18 | In case you have a great idea for how to improve this project: Open Humans is [offering $5,000 as project grants for ideas that improve/grow the Open Humans eco-system](https://www.openhumans.org/grants/). And guess what: This project would totally qualify. So if you want to work on this, you could even be paid. 😂
19 |
--------------------------------------------------------------------------------
/INSTALL.md:
--------------------------------------------------------------------------------
1 | # Install
2 | [TwArχiv](http://twarxiv.org) is a Django application that interfaces with [Open Humans](https://openhumans.org)
3 | for the file storage and user management and that is designed to be deployed to *Heroku*. As such there are some dependencies and intricacies that need to be taken into account.
4 | NOTE: We recommend installation on python3.
5 |
6 | ## Dependencies
7 |
8 | ### Database(s)
9 | TwArχiv uses two kinds of databases for short- and long-term storage. The short term storage (for managing tasks that are not run on the webserver) is done with `redis` while the long-term storage is done with `postgresql`. If you are deploying to the heroku production environment you just have to click the appropriate add-ons.
10 |
11 | For your development environment you have to install both `redis` and `postgresql` on your local machine.
12 | If you are running macOS and using `brew` (or are a user of `linuxbrew`) you can install both with the following commands:
13 |
14 | ```
15 | brew install redis
16 | brew install postgresql
17 | ```
18 |
19 | You can then run `redis-server` from your command line to start an instance of `redis`.
20 | The configuration of `postgres` can be a bit more involved. [Check out this blogpost for some tips](https://www.codementor.io/devops/tutorial/getting-started-postgresql-server-mac-osx).
21 |
22 | ### Python modules
23 | Django in general and TwArχiv in particular requires a larger set of `python` libraries. The current list can be found in the `requirements.txt` in this repository. Right now the requirements are the following:
24 |
25 | ```
26 | gunicorn==19.7.1
27 | pytz==2017.2
28 | gender_guesser==0.4.0
29 | pandas==0.20.3
30 | tzwhere==3.0.3
31 | Django==1.11.3
32 | dj-database-url==0.4.2
33 | whitenoise==3.3.1
34 | psycopg2==2.7.3.1
35 | redis==2.10.6
36 | celery==4.1.0
37 | requests==2.18.4
38 | timezonefinder==2.1.2
39 | geojson==2.3.0
40 | arrow==0.12.0
41 | ```
42 |
43 | If you are using `heroku` in your development environment it should take care of installing the modules listed in `requirements.txt` automatically.
44 |
45 | ## Create a Project on Open Humans
46 | We want to interface with Open Humans for our project. For this reason we need to create a research project on openhumans.org. After creating an account go to https://www.openhumans.org/direct-sharing/projects/manage/
47 | and generate a new _OAuth_ project. The most important parts to get right are the `enrollment URL` and the `redirect URL`. For your development environment these should be the right URLs:
48 |
49 | ```
50 | enrollment: http://127.0.0.1:5000/users/
51 | redirect: http://127.0.0.1:5000/users/complete # no trailing slash!
52 | ```
53 |
54 | ## Start development environment
55 | All good so far? Then we can now start developing in our local environment.
56 | I recommend using the `heroku-cli` interface to boot up both the `celery`-worker as well as the `gunicorn` webserver. You can install the CLI using `brew install heroku/brew/heroku` (or however that works on non-macs). If you are in the root directory of this repository and run `heroku local:run python manage.py migrate`, it will perform migrations to the database. `heroku local` will then use the `Procfile` to spawn local web & celery servers. If you've configured everything correctly you should be able to point your browser to `http://127.0.0.1:5000/` and see your very own copy of TwArχiv
57 |
58 | #### Heroku configuration
59 | `heroku` will try to read environment variables from `.env` for your local environment. Make sure you have such a file. It should contain the following keys:
60 |
61 | ```
62 | REDIS_URL=redis:// # where is your redis server located? most likely at this url if in dev
63 | DATABASE_URL=postgres:///username # where does your postgres DB live?
64 | SECRET_KEY=foobar # the Django Secret Key
65 | ON_HEROKU=False # is our app deployed on heroku?
66 | OH_CLIENT_ID=NOT_A_KEY_EITHER # the client ID for your Open Humans project
67 | OH_CLIENT_SECRET=NOTAREALKEY # the secret key you get from Open Humans when creating a project.
68 | OH_ACTIVITY_PAGE=https://www.openhumans.org/activity/your-activity-name/ # What is your Project on Open Humans?
69 | APP_BASE_URL=http://127.0.0.1:5000/users # where is our app located? Open Humans wants to know
70 |
71 | PYTHONUNBUFFERED=true # make sure we can print to console
72 | ```
73 |
74 | This file contains private data that would allow other parties to take over your project. So make sure that you **don't commit this file to your Git repository**.
75 |
76 | ## Deploy to `heroku` production
77 | Once it's set up it is as easy as running `git push heroku master`. For obvious reasons (see above) it won't have the `.env` file for setting the environment variables. For that reason you have to manually specify them for the production environment. The `heroku cli` makes this easy:
78 |
79 | ```
80 | heroku config:set SECRET_KEY=foobar
81 | heroku config:set APP_BASE_URL=http://www.example.com
82 | ```
83 |
84 | **Important:** Don't forget to set `ON_HEROKU=True`. Otherwise the automatic setup of your database environment will not work and you will wonder why the database migrations & queries won't work.
85 |
86 | **You don't have to set the `REDIS_URL` and `DATABASE_URL` in production. This will be done by heroku!**
87 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Bastian Greshake Tzovaras
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | coverage = "*"
8 | flake8 = "*"
9 | vcrpy = "*"
10 | requests-mock = "*"
11 |
12 |
13 | [packages]
14 | gunicorn = "*"
15 | pytz = "*"
16 | gender-guesser = "*"
17 | pandas = "*"
18 | tzwhere = "*"
19 | dj-database-url = "*"
20 | whitenoise = "*"
21 | psycopg2 = "*"
22 | redis = "*"
23 | celery = "*"
24 | requests = "*"
25 | timezonefinder = "*"
26 | geojson = "*"
27 | arrow = "*"
28 | kombu = "*"
29 | ijson = "*"
30 | Django = "*"
31 |
32 | [requires]
33 | python_version = "3.6"
34 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn twitteranalyser.wsgi --log-file=-
2 | worker: celery -A twitteranalyser worker --concurrency=1
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Welcome to the Tw Arχiv.
2 |
3 | [](https://travis-ci.org/gedankenstuecke/twitter-analyser)
4 | [](https://codeclimate.com/github/gedankenstuecke/twitter-analyser/maintainability)
5 | [](https://codeclimate.com/github/gedankenstuecke/twitter-analyser/test_coverage)
6 |
7 | [](http://twarxiv.org)
8 | [](http://twarxiv.org)
9 |
10 | The [TwArχiv](http://twarxiv.org) is a *Twitter Archive Analyzer* that is designed to take in a complete *Twitter archive* as downloaded from Twitter.com and subsequently analyse it with respect to the number of tweets/replies etc. On the most basic level it displays how a person's tweeting frequency changes over time and how the proportion of tweets/retweets/replies changes over time.
11 | The [TwArχiv](http://twarxiv.org) additionally tries to predict the gender of the people that a given user retweets and replies to, potentially uncovering unconscious bias when it comes to online interactions.
12 | Furthermore, the [TwArχiv](http://twarxiv.org) uses geolocation to display a Tweet-location and movement profile for the user.
13 |
14 | ## Usage / Installation
15 | [TwArχiv](http://twarxiv.org) is a *Python* / *Django* application that has been designed to be deployed on *Heroku*. It is recommended to work with Python3 for installation. For the file storage and user management it interfaces with [Open Humans](https://openhumans.org) through *OAuth*. A deployed version can be seen live in action at [twarxiv.org](http://twarxiv.org). Read [the *INSTALL.md* for detailed installation instructions](https://github.com/gedankenstuecke/twitter-analyser/blob/master/INSTALL.md) 👍.
16 |
17 | ## Contributing
18 | We are always happy about new contributors! [Open an issue](https://github.com/gedankenstuecke/twitter-analyser/issues) if you find a bug or have feature ideas. If you want to contribute code please [head to our *CONTRIBUTING.md*](https://github.com/gedankenstuecke/twitter-analyser/blob/master/CONTRIBUTING.md). Also: You have a larger feature/analysis idea that's not in Tw Arχiv yet? [Open Humans is giving out grants of up to $5,000](https://www.openhumans.org/grants) for projects that help to grow their eco-system that are a perfect match for this!
19 |
--------------------------------------------------------------------------------
/analyser.py:
--------------------------------------------------------------------------------
1 | import json
2 | import datetime
3 | import pytz
4 | import gender_guesser.detector as gender
5 | import pandas as pd
6 | from tzwhere import tzwhere
7 | gender_guesser = gender.Detector(case_sensitive=False)
8 | tzwhere_ = tzwhere.tzwhere()
9 |
10 | # READ JSON FILES FROM TWITTER ARCHIVE!
11 |
12 |
13 | def check_hashtag(single_tweet):
14 | '''check whether tweet has any hashtags'''
15 | return len(single_tweet['entities']['hashtags']) > 0
16 |
17 |
18 | def check_media(single_tweet):
19 | '''check whether tweet has any media attached'''
20 | return len(single_tweet['entities']['media']) > 0
21 |
22 |
23 | def check_url(single_tweet):
24 | '''check whether tweet has any urls attached'''
25 | return len(single_tweet['entities']['urls']) > 0
26 |
27 |
28 | def check_retweet(single_tweet):
29 | '''
30 | check whether tweet is a RT. If yes:
31 | return name & user name of the RT'd user.
32 | otherwise just return nones
33 | '''
34 | if 'retweeted_status' in single_tweet.keys():
35 | return (single_tweet['retweeted_status']['user']['screen_name'],
36 | single_tweet['retweeted_status']['user']['name'])
37 | else:
38 | return (None, None)
39 |
40 |
41 | def check_coordinates(single_tweet):
42 | '''
43 | check whether tweet has coordinates attached.
44 | if yes return the coordinates
45 | otherwise just return nones
46 | '''
47 | if 'coordinates' in single_tweet['geo'].keys():
48 | return (single_tweet['geo']['coordinates'][0],
49 | single_tweet['geo']['coordinates'][1])
50 | else:
51 | return (None, None)
52 |
53 |
54 | def check_reply_to(single_tweet):
55 | '''
56 | check whether tweet is a reply. If yes:
57 | return name & user name of the user that's replied to.
58 | otherwise just return nones
59 | '''
60 | if 'in_reply_to_screen_name' in single_tweet.keys():
61 | name = None
62 | for user in single_tweet['entities']['user_mentions']:
63 | if user['screen_name'] == single_tweet['in_reply_to_screen_name']:
64 | name = user['name']
65 | break
66 | return (single_tweet['in_reply_to_screen_name'], name)
67 | else:
68 | return (None, None)
69 |
70 |
71 | def convert_time(coordinates, time_utc):
72 | '''
73 | Does this tweet have a geo location? if yes
74 | we can easily convert the UTC timestamp to true local time!
75 | otherwise return nones
76 | '''
77 | if coordinates[0] and coordinates[1]:
78 | timezone_str = tzwhere_.tzNameAt(coordinates[0], coordinates[1])
79 | if timezone_str:
80 | timezone = pytz.timezone(timezone_str)
81 | time_obj_local = datetime.datetime.astimezone(time_utc, timezone)
82 | return time_obj_local
83 |
84 |
85 | def create_dataframe(tweets):
86 | '''
87 | create a pandas dataframe from our tweet jsons
88 | '''
89 |
90 | # initalize empty lists
91 | utc_time = []
92 | longitude = []
93 | latitude = []
94 | local_time = []
95 | hashtag = []
96 | media = []
97 | url = []
98 | retweet_user_name = []
99 | retweet_name = []
100 | reply_user_name = []
101 | reply_name = []
102 | text = []
103 | # iterate over all tweets and extract data
104 | for single_tweet in tweets:
105 | utc_time.append(datetime.datetime.strptime(
106 | single_tweet['created_at'], '%Y-%m-%d %H:%M:%S %z'))
107 | coordinates = check_coordinates(single_tweet)
108 | latitude.append(coordinates[0])
109 | longitude.append(coordinates[1])
110 | local_time.append(convert_time(coordinates, datetime.datetime.strptime(
111 | single_tweet['created_at'], '%Y-%m-%d %H:%M:%S %z')))
112 | hashtag.append(check_hashtag(single_tweet))
113 | media.append(check_media(single_tweet))
114 | url.append(check_url(single_tweet))
115 | retweet = check_retweet(single_tweet)
116 | retweet_user_name.append(retweet[0])
117 | retweet_name.append(retweet[1])
118 | reply = check_reply_to(single_tweet)
119 | reply_user_name.append(reply[0])
120 | reply_name.append(reply[1])
121 | text.append(single_tweet['text'])
122 | # convert the whole shebang into a pandas dataframe
123 | dataframe = pd.DataFrame(data={
124 | 'utc_time': utc_time,
125 | 'local_time': local_time,
126 | 'latitude': latitude,
127 | 'longitude': longitude,
128 | 'hashtag': hashtag,
129 | 'media': media,
130 | 'url': url,
131 | 'retweet_user_name': retweet_user_name,
132 | 'retweet_name': retweet_name,
133 | 'reply_user_name': reply_user_name,
134 | 'reply_name': reply_name,
135 | 'text': text
136 | })
137 | return dataframe
138 |
139 |
140 | def read_file_index(index_file):
141 | '''
142 | read file that lists all
143 | tweet-containing json files
144 | '''
145 | with open(index_file) as f:
146 | d = f.readlines()[1:]
147 | d = "".join(d)
148 | d = "[{" + d
149 | files = json.loads(d)
150 | return files
151 |
152 |
153 | def read_single_file(fpath):
154 | '''
155 | read in the json of a single tweet.json
156 | '''
157 | with open(fpath) as f:
158 | d = f.readlines()[1:]
159 | d = "".join(d)
160 | tweets = json.loads(d)
161 | return tweets
162 |
163 |
164 | def read_files(file_list, base_path):
165 | '''
166 | use the file list as generated by
167 | read_file_index() to read in the json
168 | of all tweet.json files and convert them
169 | into individual data frames.
170 | Returns them so far not concatenated
171 | '''
172 | data_frames = []
173 | for single_file in file_list:
174 | tweets = read_single_file(base_path + '/' + single_file['file_name'])
175 | df_tweets = create_dataframe(tweets)
176 | data_frames.append(df_tweets)
177 | return data_frames
178 |
179 |
180 | def create_main_dataframe(tweet_index='twitter_archive/data/js/tweet_index.js',
181 | base_directory='twitter_archive'):
182 | file_index = read_file_index(tweet_index)
183 | dataframes = read_files(file_index, base_directory)
184 | dataframe = pd.concat(dataframes)
185 | dataframe = dataframe.sort_values('utc_time', ascending=False)
186 | dataframe = dataframe.set_index('utc_time')
187 | dataframe = dataframe.replace(to_replace={
188 | 'url': {False: None},
189 | 'hashtag': {False: None},
190 | 'media': {False: None}
191 | })
192 | return dataframe
193 |
194 | # GENERATE JSON FOR GRAPHING ON THE WEB
195 |
196 |
197 | def create_all_tweets(dataframe, rolling_frame='180d'):
198 | dataframe_grouped = dataframe.groupby(dataframe.index.date).count()
199 | dataframe_grouped.index = pd.to_datetime(dataframe_grouped.index)
200 | dataframe_mean_week = dataframe_grouped.rolling(rolling_frame).mean()
201 |
202 |
203 | def create_hourly_stats(dataframe):
204 | def get_hour(x): return x.hour
205 |
206 | def get_weekday(x): return x.weekday()
207 |
208 | local_times = dataframe.copy()
209 | local_times = local_times.loc[dataframe['local_time'].notnull()]
210 |
211 | local_times['weekday'] = local_times['local_time'].apply(get_weekday)
212 | local_times['hour'] = local_times['local_time'].apply(get_hour)
213 |
214 | local_times = local_times.replace(to_replace={'weekday':
215 | {0: 'Weekday',
216 | 1: 'Weekday',
217 | 2: 'Weekday',
218 | 3: 'Weekday',
219 | 4: 'Weekday',
220 | 5: 'Weekend',
221 | 6: 'Weekend',
222 | }
223 | })
224 |
225 | local_times = local_times.groupby(
226 | [local_times['hour'], local_times['weekday']]).size().reset_index()
227 | local_times['values'] = local_times[0]
228 | local_times = local_times.set_index(local_times['hour'])
229 |
230 | return local_times.pivot(columns='weekday', values='values').reset_index()
231 |
232 |
233 | def predict_gender(dataframe, column_name, rolling_frame='180d'):
234 | '''
235 | take full dataframe w/ tweets and extract
236 | gender for a name-column where applicable
237 | returns two-column df w/ timestamp & gender
238 | '''
239 | def splitter(x): return x.split()[0]
240 | temp = dataframe[column_name].notnull()
241 | gender_column = dataframe.loc[temp][column_name].apply(
242 | splitter).apply(
243 | gender_guesser.get_gender)
244 |
245 | gender_dataframe = pd.DataFrame(data={
246 | 'time': list(gender_column.index),
247 | 'gender': list(gender_column)
248 | })
249 |
250 | gender_dataframe = gender_dataframe.set_index('time')
251 | group = [gender_dataframe.index.date, gender_dataframe['gender']]
252 | gender_dataframe_tab = gender_dataframe.groupby(group).size().reset_index()
253 | gender_dataframe_tab['date'] = gender_dataframe_tab['level_0']
254 | gender_dataframe_tab['count'] = gender_dataframe_tab[0]
255 | gender_dataframe_tab = gender_dataframe_tab.drop([0, 'level_0'], axis=1)
256 | gender_dataframe_tab = gender_dataframe_tab.set_index('date')
257 | gender_dataframe_tab.index = pd.to_datetime(gender_dataframe_tab.index)
258 | gdf_pivot = gender_dataframe_tab.pivot(columns='gender', values='count')
259 | gdf_pivot = gdf_pivot.rolling(rolling_frame).mean()
260 | gdf_pivot = gdf_pivot.reset_index()
261 | gdf_pivot['date'] = gdf_pivot['date'].astype(str)
262 | gdf_pivot = gdf_pivot.drop(
263 | ['mostly_male', 'mostly_female', 'andy', 'unknown'], axis=1)
264 | return gdf_pivot
265 |
266 | # DUMP JSON FOR GRAPHING
267 |
268 |
269 | def write_json_for_graph(dataframe,
270 | outfile='graph.json',
271 | format='records'):
272 | json_object = dataframe.to_json(orient=format)
273 | with open(outfile, 'w') as f:
274 | f.write(json_object)
275 |
276 |
277 | def __main__():
278 | dataframe = create_main_dataframe()
279 | retweet_gender = predict_gender(dataframe,'retweet_name','180d')
280 | write_json_for_graph(retweet_gender,'gender_rt.json')
281 | reply_gender = predict_gender(dataframe,'reply_name','180d')
282 | write_json_for_graph(reply_gender, 'gender_reply.json')
283 |
284 | if __name__ == "__main__":
285 | main()
286 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | coverage==4.5.1
2 | flake8==3.5.0
3 | vcrpy==1.11.1
4 | requests-mock==1.4
5 |
--------------------------------------------------------------------------------
/docs/gender_reply.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gedankenstuecke/twitter-analyser/91e3172bed6f786b34c237548ab81c347fd5b146/docs/gender_reply.png
--------------------------------------------------------------------------------
/docs/tweet_class.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gedankenstuecke/twitter-analyser/91e3172bed6f786b34c237548ab81c347fd5b146/docs/tweet_class.png
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "twitteranalyser.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError:
10 | # The above import may fail for some other reason. Ensure that the
11 | # issue is really that Django is missing to avoid masking other
12 | # exceptions on Python 2.
13 | try:
14 | import django
15 | except ImportError:
16 | raise ImportError(
17 | "Couldn't import Django. Are you sure it's installed and "
18 | "available on your PYTHONPATH environment variable? Did you "
19 | "forget to activate a virtual environment?"
20 | )
21 | raise
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/new_tweet_subset.js:
--------------------------------------------------------------------------------
1 | window.YTD.tweet.part0 = [ {
2 | "retweeted" : false,
3 | "source" : "Tweetbot for iΟS ",
4 | "entities" : {
5 | "hashtags" : [ ],
6 | "symbols" : [ ],
7 | "user_mentions" : [ {
8 | "name" : "Philipp Bayer",
9 | "screen_name" : "PhilippBayer",
10 | "indices" : [ "16", "29" ],
11 | "id_str" : "121777206",
12 | "id" : "121777206"
13 | } ],
14 | "urls" : [ {
15 | "url" : "https://t.co/w8pz21j6Vv",
16 | "expanded_url" : "https://www.goodreads.com/review/show/652888163",
17 | "display_url" : "goodreads.com/review/show/65…",
18 | "indices" : [ "68", "91" ]
19 | } ]
20 | },
21 | "display_text_range" : [ "0", "91" ],
22 | "favorite_count" : "1",
23 | "in_reply_to_status_id_str" : "1159965221924024320",
24 | "id_str" : "1159965503051485184",
25 | "in_reply_to_user_id" : "14286491",
26 | "truncated" : false,
27 | "retweet_count" : "0",
28 | "id" : "1159965503051485184",
29 | "in_reply_to_status_id" : "1159965221924024320",
30 | "possibly_sensitive" : false,
31 | "created_at" : "Fri Aug 09 23:11:41 +0000 2019",
32 | "favorited" : false,
33 | "full_text" : "Read the review @PhilippBayer wrote on the mentioned autobiography. https://t.co/w8pz21j6Vv",
34 | "lang" : "en",
35 | "in_reply_to_screen_name" : "gedankenstuecke",
36 | "in_reply_to_user_id_str" : "14286491"
37 | }, {
38 | "retweeted" : false,
39 | "source" : "Tweetbot for iΟS ",
40 | "entities" : {
41 | "hashtags" : [ ],
42 | "symbols" : [ ],
43 | "user_mentions" : [ ],
44 | "urls" : [ {
45 | "url" : "https://t.co/Vqhq5TAdBV",
46 | "expanded_url" : "https://twitter.com/DavidBLowry/status/1159836767958138880",
47 | "display_url" : "twitter.com/DavidBLowry/st…",
48 | "indices" : [ "114", "137" ]
49 | } ]
50 | },
51 | "display_text_range" : [ "0", "137" ],
52 | "favorite_count" : "2",
53 | "id_str" : "1159965221924024320",
54 | "truncated" : false,
55 | "retweet_count" : "0",
56 | "id" : "1159965221924024320",
57 | "possibly_sensitive" : false,
58 | "created_at" : "Fri Aug 09 23:10:34 +0000 2019",
59 | "favorited" : false,
60 | "full_text" : "He and the glowing raccoons are finally reunited, reading horoscopes and denying any link between HIV & AIDS. https://t.co/Vqhq5TAdBV",
61 | "lang" : "en"
62 | }, {
63 | "retweeted" : false,
64 | "source" : "Tweetbot for iΟS ",
65 | "entities" : {
66 | "hashtags" : [ ],
67 | "symbols" : [ ],
68 | "user_mentions" : [ {
69 | "name" : "Nazeefa \uD83C\uDF40☄",
70 | "screen_name" : "NazeefaFatima",
71 | "indices" : [ "0", "14" ],
72 | "id_str" : "37054704",
73 | "id" : "37054704"
74 | } ],
75 | "urls" : [ ]
76 | },
77 | "display_text_range" : [ "0", "57" ],
78 | "favorite_count" : "1",
79 | "in_reply_to_status_id_str" : "1159823677703282688",
80 | "id_str" : "1159852950254227461",
81 | "in_reply_to_user_id" : "37054704",
82 | "truncated" : false,
83 | "retweet_count" : "0",
84 | "id" : "1159852950254227461",
85 | "in_reply_to_status_id" : "1159823677703282688",
86 | "created_at" : "Fri Aug 09 15:44:27 +0000 2019",
87 | "favorited" : false,
88 | "full_text" : "@NazeefaFatima It was so great to finally meet in person!",
89 | "lang" : "en",
90 | "in_reply_to_screen_name" : "NazeefaFatima",
91 | "in_reply_to_user_id_str" : "37054704"
92 | }, {
93 | "retweeted" : false,
94 | "source" : "Twitter Web App ",
95 | "entities" : {
96 | "user_mentions" : [ {
97 | "name" : "Philip Ellis",
98 | "screen_name" : "Philip_Ellis",
99 | "indices" : [ "3", "16" ],
100 | "id_str" : "222444337",
101 | "id" : "222444337"
102 | } ],
103 | "urls" : [ ],
104 | "symbols" : [ ],
105 | "media" : [ {
106 | "expanded_url" : "https://twitter.com/zbgolia/status/1154777994252161026/video/1",
107 | "source_status_id" : "1154777994252161026",
108 | "indices" : [ "88", "111" ],
109 | "url" : "https://t.co/kiSlwXQ9Fn",
110 | "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/1154777848395304961/pu/img/DCzgIYqzvou0VeLs.jpg",
111 | "id_str" : "1154777848395304961",
112 | "source_user_id" : "259113321",
113 | "id" : "1154777848395304961",
114 | "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/1154777848395304961/pu/img/DCzgIYqzvou0VeLs.jpg",
115 | "source_user_id_str" : "259113321",
116 | "sizes" : {
117 | "thumb" : {
118 | "w" : "150",
119 | "h" : "150",
120 | "resize" : "crop"
121 | },
122 | "medium" : {
123 | "w" : "1200",
124 | "h" : "675",
125 | "resize" : "fit"
126 | },
127 | "small" : {
128 | "w" : "680",
129 | "h" : "383",
130 | "resize" : "fit"
131 | },
132 | "large" : {
133 | "w" : "1280",
134 | "h" : "720",
135 | "resize" : "fit"
136 | }
137 | },
138 | "type" : "photo",
139 | "source_status_id_str" : "1154777994252161026",
140 | "display_url" : "pic.twitter.com/kiSlwXQ9Fn"
141 | } ],
142 | "hashtags" : [ ]
143 | },
144 | "display_text_range" : [ "0", "111" ],
145 | "favorite_count" : "0",
146 | "id_str" : "1159146575622475776",
147 | "truncated" : false,
148 | "retweet_count" : "0",
149 | "id" : "1159146575622475776",
150 | "possibly_sensitive" : false,
151 | "created_at" : "Wed Aug 07 16:57:34 +0000 2019",
152 | "favorited" : false,
153 | "full_text" : "RT @Philip_Ellis: [JOB INTERVIEW]\n\nInterviewer: Do you have any questions for us?\n\nMe: https://t.co/kiSlwXQ9Fn",
154 | "lang" : "en",
155 | "extended_entities" : {
156 | "media" : [ {
157 | "expanded_url" : "https://twitter.com/zbgolia/status/1154777994252161026/video/1",
158 | "source_status_id" : "1154777994252161026",
159 | "indices" : [ "88", "111" ],
160 | "url" : "https://t.co/kiSlwXQ9Fn",
161 | "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/1154777848395304961/pu/img/DCzgIYqzvou0VeLs.jpg",
162 | "id_str" : "1154777848395304961",
163 | "video_info" : {
164 | "aspect_ratio" : [ "16", "9" ],
165 | "duration_millis" : "45412",
166 | "variants" : [ {
167 | "bitrate" : "256000",
168 | "content_type" : "video/mp4",
169 | "url" : "https://video.twimg.com/ext_tw_video/1154777848395304961/pu/vid/480x270/9dXRMkihwUJrcedP.mp4?tag=10"
170 | }, {
171 | "bitrate" : "832000",
172 | "content_type" : "video/mp4",
173 | "url" : "https://video.twimg.com/ext_tw_video/1154777848395304961/pu/vid/640x360/XI7BsQJUDBaKeBz9.mp4?tag=10"
174 | }, {
175 | "content_type" : "application/x-mpegURL",
176 | "url" : "https://video.twimg.com/ext_tw_video/1154777848395304961/pu/pl/EzF1MVQbpeN4oXWg.m3u8?tag=10"
177 | }, {
178 | "bitrate" : "2176000",
179 | "content_type" : "video/mp4",
180 | "url" : "https://video.twimg.com/ext_tw_video/1154777848395304961/pu/vid/1280x720/g60m28vGjo1uKFrj.mp4?tag=10"
181 | } ]
182 | },
183 | "source_user_id" : "259113321",
184 | "additional_media_info" : {
185 | "monetizable" : false
186 | },
187 | "id" : "1154777848395304961",
188 | "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/1154777848395304961/pu/img/DCzgIYqzvou0VeLs.jpg",
189 | "source_user_id_str" : "259113321",
190 | "sizes" : {
191 | "thumb" : {
192 | "w" : "150",
193 | "h" : "150",
194 | "resize" : "crop"
195 | },
196 | "medium" : {
197 | "w" : "1200",
198 | "h" : "675",
199 | "resize" : "fit"
200 | },
201 | "small" : {
202 | "w" : "680",
203 | "h" : "383",
204 | "resize" : "fit"
205 | },
206 | "large" : {
207 | "w" : "1280",
208 | "h" : "720",
209 | "resize" : "fit"
210 | }
211 | },
212 | "type" : "video",
213 | "source_status_id_str" : "1154777994252161026",
214 | "display_url" : "pic.twitter.com/kiSlwXQ9Fn"
215 | } ]
216 | }
217 | }, {
218 | "retweeted" : false,
219 | "source" : "Tweetbot for iΟS ",
220 | "entities" : {
221 | "hashtags" : [ ],
222 | "symbols" : [ ],
223 | "user_mentions" : [ {
224 | "name" : "liubov",
225 | "screen_name" : "luyibov",
226 | "indices" : [ "0", "8" ],
227 | "id_str" : "2889619139",
228 | "id" : "2889619139"
229 | }, {
230 | "name" : "Jake Wintermute",
231 | "screen_name" : "SynBio1",
232 | "indices" : [ "9", "17" ],
233 | "id_str" : "2570913493",
234 | "id" : "2570913493"
235 | }, {
236 | "name" : "marc santolini",
237 | "screen_name" : "msantolini",
238 | "indices" : [ "18", "29" ],
239 | "id_str" : "299603744",
240 | "id" : "299603744"
241 | }, {
242 | "name" : "Roberto Toro",
243 | "screen_name" : "R3RT0",
244 | "indices" : [ "30", "36" ],
245 | "id_str" : "2231179117",
246 | "id" : "2231179117"
247 | }, {
248 | "name" : "katja heuer",
249 | "screen_name" : "katjaQheuer",
250 | "indices" : [ "37", "49" ],
251 | "id_str" : "2981013099",
252 | "id" : "2981013099"
253 | }, {
254 | "name" : "Jon Tennant",
255 | "screen_name" : "Protohedgehog",
256 | "indices" : [ "50", "64" ],
257 | "id_str" : "352650591",
258 | "id" : "352650591"
259 | } ],
260 | "urls" : [ ]
261 | },
262 | "display_text_range" : [ "0", "126" ],
263 | "favorite_count" : "3",
264 | "in_reply_to_status_id_str" : "1159132637795160069",
265 | "id_str" : "1159140448377724929",
266 | "in_reply_to_user_id" : "2889619139",
267 | "truncated" : false,
268 | "retweet_count" : "0",
269 | "id" : "1159140448377724929",
270 | "in_reply_to_status_id" : "1159132637795160069",
271 | "created_at" : "Wed Aug 07 16:33:13 +0000 2019",
272 | "favorited" : false,
273 | "full_text" : "@luyibov @SynBio1 @msantolini @R3RT0 @katjaQheuer @Protohedgehog I’m not an expert. But I think these are called “stripes”. :p",
274 | "lang" : "en",
275 | "in_reply_to_screen_name" : "luyibov",
276 | "in_reply_to_user_id_str" : "2889619139"
277 | }, {
278 | "retweeted" : false,
279 | "source" : "Tweetbot for iΟS ",
280 | "entities" : {
281 | "hashtags" : [ ],
282 | "symbols" : [ ],
283 | "user_mentions" : [ {
284 | "name" : "Bastian Greshake Tzovaras",
285 | "screen_name" : "gedankenstuecke",
286 | "indices" : [ "3", "19" ],
287 | "id_str" : "14286491",
288 | "id" : "14286491"
289 | } ],
290 | "urls" : [ {
291 | "url" : "https://t.co/Xq0JEgsRhW",
292 | "expanded_url" : "https://twitter.com/Michael__Rera/status/1158374078996242432",
293 | "display_url" : "twitter.com/Michael__Rera/…",
294 | "indices" : [ "76", "99" ]
295 | } ]
296 | },
297 | "display_text_range" : [ "0", "99" ],
298 | "favorite_count" : "0",
299 | "id_str" : "1159080024080887809",
300 | "truncated" : false,
301 | "retweet_count" : "0",
302 | "id" : "1159080024080887809",
303 | "possibly_sensitive" : false,
304 | "created_at" : "Wed Aug 07 12:33:07 +0000 2019",
305 | "favorited" : false,
306 | "full_text" : "RT @gedankenstuecke: That would be me, looking for an apartment in Paris. \uD83D\uDE00 https://t.co/Xq0JEgsRhW",
307 | "lang" : "en"
308 | }, {
309 | "retweeted" : false,
310 | "source" : "Tweetbot for iΟS ",
311 | "entities" : {
312 | "hashtags" : [ ],
313 | "symbols" : [ ],
314 | "user_mentions" : [ {
315 | "name" : "marc santolini",
316 | "screen_name" : "msantolini",
317 | "indices" : [ "0", "11" ],
318 | "id_str" : "299603744",
319 | "id" : "299603744"
320 | }, {
321 | "name" : "Jon Tennant",
322 | "screen_name" : "Protohedgehog",
323 | "indices" : [ "12", "26" ],
324 | "id_str" : "352650591",
325 | "id" : "352650591"
326 | } ],
327 | "urls" : [ ]
328 | },
329 | "display_text_range" : [ "0", "75" ],
330 | "favorite_count" : "0",
331 | "in_reply_to_status_id_str" : "1158848834556051456",
332 | "id_str" : "1158849711681523713",
333 | "in_reply_to_user_id" : "299603744",
334 | "truncated" : false,
335 | "retweet_count" : "0",
336 | "id" : "1158849711681523713",
337 | "in_reply_to_status_id" : "1158848834556051456",
338 | "created_at" : "Tue Aug 06 21:17:56 +0000 2019",
339 | "favorited" : false,
340 | "full_text" : "@msantolini @Protohedgehog I should have put the scare quotes: “Restaurant”",
341 | "lang" : "en",
342 | "in_reply_to_screen_name" : "msantolini",
343 | "in_reply_to_user_id_str" : "299603744"
344 | }, {
345 | "retweeted" : false,
346 | "source" : "Tweetbot for iΟS ",
347 | "entities" : {
348 | "hashtags" : [ ],
349 | "symbols" : [ ],
350 | "user_mentions" : [ {
351 | "name" : "marc santolini",
352 | "screen_name" : "msantolini",
353 | "indices" : [ "0", "11" ],
354 | "id_str" : "299603744",
355 | "id" : "299603744"
356 | }, {
357 | "name" : "Jon Tennant",
358 | "screen_name" : "Protohedgehog",
359 | "indices" : [ "12", "26" ],
360 | "id_str" : "352650591",
361 | "id" : "352650591"
362 | } ],
363 | "urls" : [ ]
364 | },
365 | "display_text_range" : [ "0", "107" ],
366 | "favorite_count" : "4",
367 | "in_reply_to_status_id_str" : "1158836352387100677",
368 | "id_str" : "1158836779841150978",
369 | "in_reply_to_user_id" : "299603744",
370 | "truncated" : false,
371 | "retweet_count" : "0",
372 | "id" : "1158836779841150978",
373 | "in_reply_to_status_id" : "1158836352387100677",
374 | "created_at" : "Tue Aug 06 20:26:33 +0000 2019",
375 | "favorited" : false,
376 | "full_text" : "@msantolini @Protohedgehog Yep, traditionally the only vegetarian thing you can eat in German restaurants \uD83D\uDE02",
377 | "lang" : "en",
378 | "in_reply_to_screen_name" : "msantolini",
379 | "in_reply_to_user_id_str" : "299603744"
380 | }, {
381 | "retweeted" : false,
382 | "source" : "Tweetbot for iΟS ",
383 | "entities" : {
384 | "hashtags" : [ ],
385 | "symbols" : [ ],
386 | "user_mentions" : [ {
387 | "name" : "Michael Rera",
388 | "screen_name" : "Michael__Rera",
389 | "indices" : [ "0", "14" ],
390 | "id_str" : "4075485214",
391 | "id" : "4075485214"
392 | }, {
393 | "name" : "KIEZ Bistro Allemand",
394 | "screen_name" : "KIEZ_Bistro",
395 | "indices" : [ "15", "27" ],
396 | "id_str" : "2514706255",
397 | "id" : "2514706255"
398 | } ],
399 | "urls" : [ ]
400 | },
401 | "display_text_range" : [ "0", "177" ],
402 | "favorite_count" : "1",
403 | "in_reply_to_status_id_str" : "1158835594744799239",
404 | "id_str" : "1158836067337940992",
405 | "in_reply_to_user_id" : "4075485214",
406 | "truncated" : false,
407 | "retweet_count" : "1",
408 | "id" : "1158836067337940992",
409 | "in_reply_to_status_id" : "1158835594744799239",
410 | "created_at" : "Tue Aug 06 20:23:43 +0000 2019",
411 | "favorited" : false,
412 | "full_text" : "@Michael__Rera @KIEZ_Bistro In good German tradition we were called ‘boring people’ (Langweiler) all evening for eating the vegetarian version of Käsespätzle. Would go again! :D",
413 | "lang" : "en",
414 | "in_reply_to_screen_name" : "Michael__Rera",
415 | "in_reply_to_user_id_str" : "4075485214"
416 | }, {
417 | "retweeted" : false,
418 | "source" : "Tweetbot for iΟS ",
419 | "entities" : {
420 | "user_mentions" : [ {
421 | "name" : "Michael Rera",
422 | "screen_name" : "Michael__Rera",
423 | "indices" : [ "5", "19" ],
424 | "id_str" : "4075485214",
425 | "id" : "4075485214"
426 | } ],
427 | "urls" : [ ],
428 | "symbols" : [ ],
429 | "media" : [ {
430 | "expanded_url" : "https://twitter.com/gedankenstuecke/status/1158806049874436096/photo/1",
431 | "indices" : [ "100", "123" ],
432 | "url" : "https://t.co/UYfuNLdOmD",
433 | "media_url" : "http://pbs.twimg.com/media/EBTn72vWsAA9L4H.jpg",
434 | "id_str" : "1158806019633491968",
435 | "id" : "1158806019633491968",
436 | "media_url_https" : "https://pbs.twimg.com/media/EBTn72vWsAA9L4H.jpg",
437 | "sizes" : {
438 | "thumb" : {
439 | "w" : "150",
440 | "h" : "150",
441 | "resize" : "crop"
442 | },
443 | "medium" : {
444 | "w" : "1200",
445 | "h" : "900",
446 | "resize" : "fit"
447 | },
448 | "small" : {
449 | "w" : "680",
450 | "h" : "510",
451 | "resize" : "fit"
452 | },
453 | "large" : {
454 | "w" : "2048",
455 | "h" : "1536",
456 | "resize" : "fit"
457 | }
458 | },
459 | "type" : "photo",
460 | "display_url" : "pic.twitter.com/UYfuNLdOmD"
461 | } ],
462 | "hashtags" : [ ]
463 | },
464 | "display_text_range" : [ "0", "123" ],
465 | "favorite_count" : "4",
466 | "id_str" : "1158806049874436096",
467 | "truncated" : false,
468 | "retweet_count" : "1",
469 | "id" : "1158806049874436096",
470 | "possibly_sensitive" : false,
471 | "created_at" : "Tue Aug 06 18:24:26 +0000 2019",
472 | "favorited" : false,
473 | "full_text" : "When @Michael__Rera picks where you should go for dinner and he directs you to a German beer place. https://t.co/UYfuNLdOmD",
474 | "lang" : "en",
475 | "extended_entities" : {
476 | "media" : [ {
477 | "expanded_url" : "https://twitter.com/gedankenstuecke/status/1158806049874436096/photo/1",
478 | "indices" : [ "100", "123" ],
479 | "url" : "https://t.co/UYfuNLdOmD",
480 | "media_url" : "http://pbs.twimg.com/media/EBTn72vWsAA9L4H.jpg",
481 | "id_str" : "1158806019633491968",
482 | "id" : "1158806019633491968",
483 | "media_url_https" : "https://pbs.twimg.com/media/EBTn72vWsAA9L4H.jpg",
484 | "sizes" : {
485 | "thumb" : {
486 | "w" : "150",
487 | "h" : "150",
488 | "resize" : "crop"
489 | },
490 | "medium" : {
491 | "w" : "1200",
492 | "h" : "900",
493 | "resize" : "fit"
494 | },
495 | "small" : {
496 | "w" : "680",
497 | "h" : "510",
498 | "resize" : "fit"
499 | },
500 | "large" : {
501 | "w" : "2048",
502 | "h" : "1536",
503 | "resize" : "fit"
504 | }
505 | },
506 | "type" : "photo",
507 | "display_url" : "pic.twitter.com/UYfuNLdOmD"
508 | } ]
509 | }
510 | }, {
511 | "retweeted" : false,
512 | "source" : "Tweetbot for Mac ",
513 | "entities" : {
514 | "hashtags" : [ ],
515 | "symbols" : [ ],
516 | "user_mentions" : [ {
517 | "name" : "Hervé Ménager",
518 | "screen_name" : "rvmngr",
519 | "indices" : [ "0", "7" ],
520 | "id_str" : "632591287",
521 | "id" : "632591287"
522 | } ],
523 | "urls" : [ ]
524 | },
525 | "display_text_range" : [ "0", "32" ],
526 | "favorite_count" : "1",
527 | "in_reply_to_status_id_str" : "1158467245313581056",
528 | "id_str" : "1158520189102825475",
529 | "in_reply_to_user_id" : "632591287",
530 | "truncated" : false,
531 | "retweet_count" : "0",
532 | "id" : "1158520189102825475",
533 | "in_reply_to_status_id" : "1158467245313581056",
534 | "created_at" : "Mon Aug 05 23:28:32 +0000 2019",
535 | "favorited" : false,
536 | "full_text" : "@rvmngr Awesome, thanks so much!",
537 | "lang" : "en",
538 | "in_reply_to_screen_name" : "rvmngr",
539 | "in_reply_to_user_id_str" : "632591287"
540 | }]
541 |
--------------------------------------------------------------------------------
/static/foo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gedankenstuecke/twitter-analyser/91e3172bed6f786b34c237548ab81c347fd5b146/static/foo
--------------------------------------------------------------------------------
/tasks.py:
--------------------------------------------------------------------------------
1 | import celery
2 | import os
3 | app = celery.Celery('example')
4 | app.conf.update(BROKER_URL=os.environ['REDIS_URL'],
5 | CELERY_RESULT_BACKEND=os.environ['REDIS_URL'])
6 |
7 |
8 | @app.task
9 | def add(x, y):
10 | return x + y
11 |
--------------------------------------------------------------------------------
/test_archive_2016_2017_2_months.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gedankenstuecke/twitter-analyser/91e3172bed6f786b34c237548ab81c347fd5b146/test_archive_2016_2017_2_months.zip
--------------------------------------------------------------------------------
/tweet_display/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gedankenstuecke/twitter-analyser/91e3172bed6f786b34c237548ab81c347fd5b146/tweet_display/__init__.py
--------------------------------------------------------------------------------
/tweet_display/admin.py:
--------------------------------------------------------------------------------
1 | # Register your models here.
2 |
--------------------------------------------------------------------------------
/tweet_display/analyse_data.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import gender_guesser.detector as gender
3 | import geojson
4 | gender_guesser = gender.Detector(case_sensitive=False)
5 |
6 |
7 | def predict_gender(dataframe, column_name, rolling_frame='180d'):
8 | '''
9 | take full dataframe w/ tweets and extract
10 | gender for a name-column where applicable
11 | returns two-column df w/ timestamp & gender
12 | '''
13 | def splitter(x): return ''.join(x.split()[:1])
14 | temp = dataframe[column_name].notnull()
15 | gender_column = dataframe.loc[temp][column_name].apply(
16 | splitter).apply(
17 | gender_guesser.get_gender)
18 |
19 | gender_dataframe = pd.DataFrame(data={
20 | 'time': list(gender_column.index),
21 | 'gender': list(gender_column)
22 | })
23 |
24 | gender_dataframe = gender_dataframe.set_index('time')
25 | group = [gender_dataframe.index.date, gender_dataframe['gender']]
26 | gender_dataframe_tab = gender_dataframe.groupby(group).size().reset_index()
27 | gender_dataframe_tab['date'] = gender_dataframe_tab['level_0']
28 | gender_dataframe_tab['count'] = gender_dataframe_tab[0]
29 | gender_dataframe_tab = gender_dataframe_tab.drop([0, 'level_0'], axis=1)
30 | gender_dataframe_tab = gender_dataframe_tab.set_index('date')
31 | gender_dataframe_tab.index = pd.to_datetime(gender_dataframe_tab.index)
32 | gdf_pivot = gender_dataframe_tab.pivot(columns='gender', values='count')
33 | gdf_pivot = gdf_pivot.rolling(rolling_frame).mean()
34 | gdf_pivot = gdf_pivot.reset_index()
35 | gdf_pivot['date'] = gdf_pivot['date'].astype(str)
36 | gdf_pivot = gdf_pivot.drop(
37 | ['mostly_male', 'mostly_female', 'andy', 'unknown'], axis=1)
38 | return gdf_pivot
39 |
40 |
41 | def create_hourly_stats(dataframe):
42 | def get_hour(x): return x.hour
43 |
44 | def get_weekday(x): return x.weekday()
45 |
46 | local_times = dataframe.copy()
47 | local_times = local_times.loc[dataframe['local_time'].notnull()]
48 |
49 | local_times['weekday'] = local_times['local_time'].apply(get_weekday)
50 | local_times['hour'] = local_times['local_time'].apply(get_hour)
51 |
52 | local_times = local_times.replace(to_replace={'weekday':
53 | {0: 'Weekday',
54 | 1: 'Weekday',
55 | 2: 'Weekday',
56 | 3: 'Weekday',
57 | 4: 'Weekday',
58 | 5: 'Weekend',
59 | 6: 'Weekend',
60 | }
61 | })
62 |
63 | local_times = local_times.groupby(
64 | [local_times['hour'], local_times['weekday']]).size().reset_index()
65 | local_times['values'] = local_times[0]
66 | local_times = local_times.set_index(local_times['hour'])
67 |
68 | local_times = local_times.pivot(
69 | columns='weekday', values='values').reset_index()
70 | local_times['Weekday'] = local_times['Weekday'] / 5
71 | local_times['Weekend'] = local_times['Weekend'] / 2
72 |
73 | return local_times.reset_index()
74 |
75 |
76 | def create_tweet_types(dataframe):
77 | dataframe_grouped = dataframe.groupby(dataframe.index.date).count()
78 | dataframe_grouped.index = pd.to_datetime(dataframe_grouped.index)
79 |
80 | dataframe_mean_week = dataframe_grouped.rolling('180d').mean()
81 | dataframe_mean_week['p_url'] = (
82 | dataframe_mean_week['url'] / dataframe_mean_week['text']) * 100
83 | dataframe_mean_week['p_media'] = (
84 | dataframe_mean_week['media'] / dataframe_mean_week['text']) * 100
85 | dataframe_mean_week['p_reply'] = (
86 | dataframe_mean_week['reply_name'] / dataframe_mean_week['text']) * 100
87 | dataframe_mean_week['p_rt'] = (
88 | dataframe_mean_week['retweet_name'] / dataframe_mean_week['text']) * 100
89 | dataframe_mean_week['p_hash'] = (
90 | dataframe_mean_week['hashtag'] / dataframe_mean_week['text']) * 100
91 | dataframe_mean_week['p_other'] = 100 - \
92 | (dataframe_mean_week['p_reply'] + dataframe_mean_week['p_rt'])
93 |
94 | dataframe_mean_week = dataframe_mean_week.reset_index()
95 | dataframe_mean_week['date'] = dataframe_mean_week['index'].astype(str)
96 | dataframe_mean_week = dataframe_mean_week.drop(['reply_user_name',
97 | 'retweet_user_name',
98 | 'latitude',
99 | 'longitude',
100 | 'local_time',
101 | 'url',
102 | 'media',
103 | 'reply_name',
104 | 'retweet_name',
105 | 'hashtag',
106 | 'index',
107 | ],
108 | axis=1)
109 |
110 | return dataframe_mean_week.reset_index()
111 |
112 |
113 | def create_top_replies(dataframe):
114 | top_replies = dataframe[dataframe['reply_user_name'].isin(
115 | list(dataframe['reply_user_name'].value_counts()[:5].reset_index()['index']))]
116 | top_replies = top_replies.reset_index()[['reply_user_name', 'utc_time']]
117 | top_replies['utc_time'] = top_replies['utc_time'].dt.date
118 | top_replies = top_replies.groupby(["utc_time", "reply_user_name"]).size()
119 | top_replies = top_replies.reset_index()
120 | top_replies['date'] = top_replies['utc_time'].astype(str)
121 | top_replies['value'] = top_replies[0]
122 | top_replies = top_replies.drop([0, 'utc_time'], axis=1)
123 | top_replies['date'] = pd.to_datetime(top_replies['date'])
124 | group = ['reply_user_name', pd.Grouper(key='date', freq='QS')]
125 | top_replies = top_replies.groupby(
126 | group)['value'].sum().reset_index().sort_values('date')
127 | top_replies['date'] = top_replies['date'].astype(str)
128 | return top_replies.reset_index().pivot(index='date', columns='reply_user_name', values='value').fillna(value=0).reset_index()
129 |
130 |
131 | def create_heatmap(dataframe):
132 | latitudes = dataframe['latitude'].notnull()
133 | return dataframe[latitudes][['latitude', 'longitude']]
134 |
135 |
136 | def create_overall(dataframe):
137 | dataframe_grouped = dataframe.groupby(dataframe.index.date).count()['text']
138 | dataframe_grouped.index = pd.to_datetime(dataframe_grouped.index)
139 | dataframe_mean_week = dataframe_grouped.rolling('180d').mean()
140 | dataframe_mean_week = dataframe_mean_week.reset_index()
141 | dataframe_mean_week['tweets'] = dataframe_mean_week['text']
142 | dataframe_mean_week['date'] = dataframe_mean_week['index'].dt.date.astype(
143 | str)
144 | return dataframe_mean_week[['date', 'tweets']]
145 |
146 |
147 | def create_timeline(dataframe):
148 | latitudes = dataframe['latitude'].notnull()
149 | timeline = dataframe[latitudes][['latitude', 'longitude']]
150 | timeline['start'] = timeline.index.date
151 | timeline['end'] = pd.Series(index=timeline.index).tshift(
152 | periods=28, freq='D').index.date
153 | features = []
154 | timeline.apply(lambda X: features.append(
155 | geojson.Feature(geometry=geojson.Point((float(X["longitude"]),
156 | float(X["latitude"]),)),
157 | properties=dict(start=str(X["start"]),
158 | end=str(X["end"])))
159 | ), axis=1)
160 |
161 | return geojson.dumps(geojson.FeatureCollection(features))
162 |
--------------------------------------------------------------------------------
/tweet_display/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TweetDisplayConfig(AppConfig):
5 | name = 'tweet_display'
6 |
--------------------------------------------------------------------------------
/tweet_display/helper.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from users.models import OpenHumansMember
3 | from .models import Graph
4 |
5 |
6 | def grant_access(request, oh_id):
7 | if oh_id is None:
8 | if request.user.is_authenticated:
9 | return request.user.openhumansmember.oh_id
10 | else:
11 | if (OpenHumansMember.objects.get(oh_id=oh_id).public or
12 | (request.user.is_authenticated and
13 | request.user.openhumansmember.oh_id == oh_id)):
14 | return oh_id
15 | return False
16 |
17 |
18 | def get_file_url(oh_id):
19 | oh_member = OpenHumansMember.objects.get(oh_id=oh_id)
20 | token = oh_member.get_access_token()
21 | req = requests.get(
22 | 'https://www.openhumans.org/api/direct-sharing/'
23 | 'project/exchange-member/', params={'access_token': token})
24 | if req.status_code == 200 and 'data' in req.json():
25 | data = req.json()['data']
26 | # WARNING! This is assumes the first file encountered is what you want!
27 | if len(data) > 0:
28 | return data[0]['download_url']
29 | return None
30 |
31 |
32 | def get_current_user(request):
33 | if request.user.is_authenticated:
34 | return request.user.openhumansmember.oh_id
35 | return None
36 |
37 |
38 | def check_graphs(graph_types, oh_id):
39 | graphs_ready = []
40 | for graph in graph_types:
41 | found = Graph.objects.filter(graph_type__exact=graph,
42 | open_humans_member__oh_id=oh_id)
43 | if found:
44 | graphs_ready.append(graph)
45 | return graphs_ready
46 |
47 |
48 | def message_success(oh_user):
49 | print('trying to send message for {}'.format(oh_user.oh_id))
50 | subject = 'Your graphs are ready!'
51 | message = 'Dear TwArxiv user,\nthe graphs generated from your Twitter \
52 | archive are now ready for you.\nGo over to \
53 | https://twtr-analyser.herokuapp.com/tweet_display/index/{} to \
54 | view them'.format(oh_user.oh_id)
55 | message_url = 'https://www.openhumans.org/api/direct-sharing/project/message/?access_token={}'.format(
56 | oh_user.get_access_token())
57 | response = requests.post(message_url, data={'subject': subject,
58 | 'message': message})
59 | print(response)
60 | print(response.json())
61 |
--------------------------------------------------------------------------------
/tweet_display/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.3 on 2017-11-27 23:57
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Graph',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('graph_type', models.CharField(max_length=200)),
21 | ('graph_description', models.CharField(max_length=200)),
22 | ('graph_data', models.TextField()),
23 | ],
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/tweet_display/migrations/0002_graph_open_humans_member.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.3 on 2017-12-12 23:11
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('users', '0001_initial'),
13 | ('tweet_display', '0001_initial'),
14 | ]
15 |
16 | operations = [
17 | migrations.AddField(
18 | model_name='graph',
19 | name='open_humans_member',
20 | field=models.ForeignKey(blank=True,
21 | null=True,
22 | on_delete=django.db.models.deletion.CASCADE,
23 | to='users.OpenHumansMember'),
24 | preserve_default=False,
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/tweet_display/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gedankenstuecke/twitter-analyser/91e3172bed6f786b34c237548ab81c347fd5b146/tweet_display/migrations/__init__.py
--------------------------------------------------------------------------------
/tweet_display/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from users.models import OpenHumansMember
3 |
4 |
5 | class Graph(models.Model):
6 | graph_type = models.CharField(max_length=200)
7 | graph_description = models.CharField(max_length=200)
8 | graph_data = models.TextField()
9 | open_humans_member = models.ForeignKey(OpenHumansMember,
10 | blank=True, null=True,
11 | on_delete=models.CASCADE)
12 |
13 | def __str__(self):
14 | return self.graph_type + ': ' + self.graph_description
15 |
--------------------------------------------------------------------------------
/tweet_display/read_data.py:
--------------------------------------------------------------------------------
1 | from timezonefinder import TimezoneFinder
2 | import tempfile
3 | import zipfile
4 | import json
5 | import datetime
6 | import pytz
7 | import ijson
8 | import io
9 | import pandas as pd
10 | import requests
11 | import os
12 |
13 | # tzwhere_ = tzwhere.tzwhere()
14 | tzf = TimezoneFinder()
15 |
16 |
17 | # READ JSON FILES FROM TWITTER ARCHIVE!
18 |
19 | def check_hashtag(single_tweet):
20 | '''check whether tweet has any hashtags'''
21 | return len(single_tweet['entities']['hashtags']) > 0
22 |
23 |
24 | def check_media(single_tweet):
25 | '''check whether tweet has any media attached'''
26 | if 'media' in single_tweet['entities'].keys():
27 | return len(single_tweet['entities']['media']) > 0
28 | else:
29 | return False
30 |
31 |
32 | def check_url(single_tweet):
33 | '''check whether tweet has any urls attached'''
34 | return len(single_tweet['entities']['urls']) > 0
35 |
36 |
37 | def check_retweet(single_tweet):
38 | '''
39 | check whether tweet is a RT. If yes:
40 | return name & user name of the RT'd user.
41 | otherwise just return nones
42 | '''
43 | if 'full_text' in single_tweet.keys():
44 | if single_tweet['full_text'].startswith("RT @"):
45 | if len(single_tweet['entities']['user_mentions']) > 0:
46 | return (
47 | single_tweet['entities']['user_mentions'][0]['screen_name'],
48 | single_tweet['entities']['user_mentions'][0]['name'])
49 | if 'retweeted_status' in single_tweet.keys():
50 | return (single_tweet['retweeted_status']['user']['screen_name'],
51 | single_tweet['retweeted_status']['user']['name'])
52 | return (None, None)
53 |
54 |
55 | def check_coordinates(single_tweet):
56 | '''
57 | check whether tweet has coordinates attached.
58 | if yes return the coordinates
59 | otherwise just return nones
60 | '''
61 | if 'geo' in single_tweet.keys():
62 | if 'coordinates' in single_tweet['geo'].keys():
63 | return (float(single_tweet['geo']['coordinates'][0]),
64 | float(single_tweet['geo']['coordinates'][1]))
65 | else:
66 | return (None, None)
67 | else:
68 | return (None, None)
69 |
70 |
71 | def check_reply_to(single_tweet):
72 | '''
73 | check whether tweet is a reply. If yes:
74 | return name & user name of the user that's replied to.
75 | otherwise just return nones
76 | '''
77 | if 'in_reply_to_screen_name' in single_tweet.keys():
78 | name = None
79 | for user in single_tweet['entities']['user_mentions']:
80 | if user['screen_name'] == single_tweet['in_reply_to_screen_name']:
81 | name = user['name']
82 | break
83 | return (single_tweet['in_reply_to_screen_name'], name)
84 | else:
85 | return (None, None)
86 |
87 |
88 | def convert_time(coordinates, time_utc):
89 | '''
90 | Does this tweet have a geo location? if yes
91 | we can easily convert the UTC timestamp to true local time!
92 | otherwise return nones
93 | '''
94 | if coordinates[0] and coordinates[1]:
95 | timezone_str = tzf.timezone_at(lat=coordinates[0], lng=coordinates[1])
96 | if timezone_str:
97 | timezone = pytz.timezone(timezone_str)
98 | time_obj_local = datetime.datetime.astimezone(time_utc, timezone)
99 | return time_obj_local
100 |
101 |
102 | def create_dataframe(tweets):
103 | '''
104 | create a pandas dataframe from our tweet jsons
105 | '''
106 |
107 | # initalize empty lists
108 | utc_time = []
109 | longitude = []
110 | latitude = []
111 | local_time = []
112 | hashtag = []
113 | media = []
114 | url = []
115 | retweet_user_name = []
116 | retweet_name = []
117 | reply_user_name = []
118 | reply_name = []
119 | text = []
120 | # iterate over all tweets and extract data
121 | for single_tweet in tweets:
122 | try:
123 | utc_time.append(
124 | datetime.datetime.strptime(
125 | single_tweet['tweet']['created_at'],
126 | '%a %b %d %H:%M:%S %z %Y'))
127 | except ValueError:
128 | utc_time.append(
129 | datetime.datetime.strptime(
130 | single_tweet['tweet']['created_at'],
131 | '%Y-%m-%d %H:%M:%S %z'))
132 | coordinates = check_coordinates(single_tweet['tweet'])
133 | latitude.append(coordinates[0])
134 | longitude.append(coordinates[1])
135 | try:
136 | creation_time = datetime.datetime.strptime(
137 | single_tweet['tweet']['created_at'],
138 | '%a %b %d %H:%M:%S %z %Y')
139 | except ValueError:
140 | creation_time = datetime.datetime.strptime(
141 | single_tweet['tweet']['created_at'],
142 | '%Y-%m-%d %H:%M:%S %z')
143 | converted_time = convert_time(coordinates, creation_time)
144 | local_time.append(converted_time)
145 | hashtag.append(check_hashtag(single_tweet['tweet']))
146 | media.append(check_media(single_tweet['tweet']))
147 | url.append(check_url(single_tweet['tweet']))
148 | retweet = check_retweet(single_tweet['tweet'])
149 | retweet_user_name.append(retweet[0])
150 | retweet_name.append(retweet[1])
151 | reply = check_reply_to(single_tweet['tweet'])
152 | reply_user_name.append(reply[0])
153 | reply_name.append(reply[1])
154 | if 'full_text' in single_tweet['tweet'].keys():
155 | text.append(single_tweet['tweet']['full_text'])
156 | else:
157 | text.append(single_tweet['tweet']['text'])
158 | # convert the whole shebang into a pandas dataframe
159 | dataframe = pd.DataFrame(data={
160 | 'utc_time': utc_time,
161 | 'local_time': local_time,
162 | 'latitude': latitude,
163 | 'longitude': longitude,
164 | 'hashtag': hashtag,
165 | 'media': media,
166 | 'url': url,
167 | 'retweet_user_name': retweet_user_name,
168 | 'retweet_name': retweet_name,
169 | 'reply_user_name': reply_user_name,
170 | 'reply_name': reply_name,
171 | 'text': text,
172 | })
173 | return dataframe
174 |
175 |
176 | def fetch_zip_file(zip_url):
177 | tf = tempfile.NamedTemporaryFile()
178 | print('downloading files')
179 | tf.write(requests.get(zip_url).content)
180 | tf.flush()
181 | if zipfile.is_zipfile(tf.name):
182 | return (zipfile.ZipFile(tf.name), 'zipped')
183 | else:
184 | return (open(tf.name, 'r'), 'json')
185 |
186 |
187 | def read_old_zip_archive(zf):
188 | with zf.open('data/js/tweet_index.js', 'r') as f:
189 | f = io.TextIOWrapper(f)
190 | d = f.readlines()[1:]
191 | d = "[{" + "".join(d)
192 | json_files = json.loads(d)
193 | data_frames = []
194 | print('iterate over individual files')
195 | for single_file in json_files:
196 | print('read ' + single_file['file_name'])
197 | with zf.open(single_file['file_name']) as f:
198 | f = io.TextIOWrapper(f)
199 | d = f.readlines()[1:]
200 | d = "".join(d)
201 | tweets = json.loads(d)
202 | df_tweets = create_dataframe(tweets)
203 | data_frames.append(df_tweets)
204 | return data_frames
205 |
206 |
207 | def read_files(zf, filetype):
208 | if filetype == 'zipped':
209 | if 'data/js/tweet_index.js' in zf.namelist():
210 | print('reading index')
211 | data_frames = read_old_zip_archive(zf)
212 | return data_frames
213 | elif 'tweet.js' in zf.namelist():
214 | with zf.open('tweet.js') as f:
215 | f = io.TextIOWrapper(f)
216 | tweet_string = f.readlines()
217 | tweet_string = "".join([i.strip() for i in tweet_string])
218 | tweet_string = tweet_string[25:]
219 |
220 | elif filetype == 'json':
221 | tweet_string = zf.readlines()
222 | tweet_string = "".join([i.strip() for i in tweet_string])
223 | tweet_string = tweet_string[25:]
224 | correct_json = tempfile.NamedTemporaryFile(mode='w')
225 | correct_json.write(tweet_string)
226 | correct_json.flush()
227 | tweets = ijson.items(open(correct_json.name, 'r'), 'item')
228 | data_frame = create_dataframe(tweets)
229 | return [data_frame]
230 |
231 |
232 | def create_main_dataframe(zip_url='http://ruleofthirds.de/test_archive.zip'):
233 | if zip_url.startswith('http'):
234 | print('reading zip file from web')
235 | zip_file, filetype = fetch_zip_file(zip_url)
236 | elif os.path.isfile(zip_url):
237 | print('reading zip file from disk')
238 | zip_file = zipfile.ZipFile(zip_url)
239 | filetype = 'zipped'
240 | else:
241 | raise ValueError('zip_url is not an URL nor a file in disk')
242 |
243 | dataframes = read_files(zip_file, filetype)
244 | print('concatenating...')
245 | dataframe = pd.concat(dataframes)
246 | dataframe = dataframe.sort_values('utc_time', ascending=False)
247 | dataframe = dataframe.set_index('utc_time')
248 | dataframe = dataframe.replace(to_replace={
249 | 'url': {False: None},
250 | 'hashtag': {False: None},
251 | 'media': {False: None}
252 | })
253 | return dataframe
254 |
--------------------------------------------------------------------------------
/tweet_display/static/css/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
23 |
26 |
33 |
34 |
35 |
55 |
57 |
58 |
60 | image/svg+xml
61 |
63 |
64 |
65 |
66 |
67 |
71 |
76 |
86 |
96 |
106 |
117 |
126 | Tw Arχiv
141 |
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/tweet_display/static/css/metricsgraphics.css:
--------------------------------------------------------------------------------
1 | .mg-active-datapoint {
2 | fill: black;
3 | font-size: 1.3rem;
4 | font-weight: 400;
5 | opacity: 0.8;
6 | }
7 |
8 | .mg-area1-color {
9 | fill: #0000ff;
10 | }
11 |
12 | .mg-area2-color {
13 | fill: #05b378;
14 | }
15 |
16 | .mg-area3-color {
17 | fill: #db4437;
18 | }
19 |
20 | .mg-area4-color {
21 | fill: #f8b128;
22 | }
23 |
24 | .mg-area5-color {
25 | fill: #5c5c5c;
26 | }
27 |
28 | text.mg-barplot-group-label {
29 | font-weight:900;
30 | }
31 |
32 | .mg-barplot rect.mg-bar {
33 | shape-rendering: auto;
34 | }
35 |
36 | .mg-barplot rect.mg-bar.default-bar {
37 | fill: #b6b6fc;
38 | }
39 |
40 | .mg-barplot rect.mg-bar.default-active {
41 | fill: #9e9efc;
42 | }
43 |
44 | .mg-barplot .mg-bar-prediction {
45 | fill: #5b5b5b;
46 | }
47 |
48 | .mg-barplot .mg-bar-baseline {
49 | stroke: #5b5b5b;
50 | stroke-width: 2;
51 | }
52 |
53 | .mg-bar-target-element {
54 | font-size:11px;
55 | padding-left:5px;
56 | padding-right:5px;
57 | font-weight:300;
58 | }
59 |
60 | .mg-baselines line {
61 | opacity: 1;
62 | shape-rendering: auto;
63 | stroke: #b3b2b2;
64 | stroke-width: 1px;
65 | }
66 |
67 | .mg-baselines text {
68 | fill: black;
69 | font-size: 0.9rem;
70 | opacity: 0.6;
71 | stroke: none;
72 | }
73 |
74 | .mg-baselines-small text {
75 | font-size: 0.6rem;
76 | }
77 |
78 | .mg-category-guides line {
79 | stroke: #b3b2b2;
80 | }
81 |
82 | .mg-header {
83 | cursor: default;
84 | font-size: 1.6rem;
85 | }
86 |
87 | .mg-header .mg-chart-description {
88 | fill: #ccc;
89 | font-family: FontAwesome;
90 | font-size: 1.6rem;
91 | }
92 |
93 | .mg-header .mg-warning {
94 | fill: #ccc;
95 | font-family: FontAwesome;
96 | font-size: 1.2rem;
97 | }
98 |
99 | .mg-points circle {
100 | opacity: 0.65;
101 | }
102 |
103 | .mg-popover {
104 | font-size: 1.3rem;
105 | }
106 |
107 | .mg-popover-content {
108 | cursor: auto;
109 | line-height: 17px;
110 | }
111 |
112 | .mg-data-table {
113 | margin-top: 30px;
114 | }
115 |
116 | .mg-data-table thead tr th {
117 | border-bottom: 1px solid darkgray;
118 | cursor: default;
119 | font-size: 1.1rem;
120 | font-weight: normal;
121 | padding: 5px 5px 8px 5px;
122 | text-align: right;
123 | }
124 |
125 | .mg-data-table thead tr th .fa {
126 | color: #ccc;
127 | padding-left: 4px;
128 | }
129 |
130 | .mg-data-table thead tr th .popover {
131 | font-size: 1.4rem;
132 | font-weight: normal;
133 | }
134 |
135 | .mg-data-table .secondary-title {
136 | color: darkgray;
137 | }
138 |
139 | .mg-data-table tbody tr td {
140 | margin: 2px;
141 | padding: 5px;
142 | vertical-align: top;
143 | }
144 |
145 | .mg-data-table tbody tr td.table-text {
146 | opacity: 0.8;
147 | padding-left: 30px;
148 | }
149 |
150 | .mg-y-axis line.mg-extended-yax-ticks {
151 | opacity: 0.4;
152 | }
153 |
154 | .mg-x-axis line.mg-extended-xax-ticks {
155 | opacity: 0.4;
156 | }
157 |
158 | .mg-histogram .axis path,
159 | .mg-histogram .axis line {
160 | fill: none;
161 | opacity: 0.7;
162 | shape-rendering: auto;
163 | stroke: #ccc;
164 | }
165 |
166 | tspan.hist-symbol {
167 | fill: #9e9efc;
168 | }
169 |
170 | .mg-histogram .mg-bar rect {
171 | fill: #b6b6fc;
172 | shape-rendering: auto;
173 | }
174 |
175 | .mg-histogram .mg-bar rect.active {
176 | fill: #9e9efc;
177 | }
178 |
179 | .mg-least-squares-line {
180 | stroke: red;
181 | stroke-width: 1px;
182 | }
183 |
184 | .mg-lowess-line {
185 | fill: none;
186 | stroke: red;
187 | }
188 |
189 | .mg-line1-color {
190 | stroke: #4040e8;
191 | }
192 |
193 | .mg-hover-line1-color {
194 | fill: #4040e8;
195 | }
196 |
197 | .mg-line2-color {
198 | stroke: #05b378;
199 | }
200 |
201 | .mg-hover-line2-color {
202 | fill: #05b378;
203 | }
204 |
205 | .mg-line3-color {
206 | stroke: #db4437;
207 | }
208 |
209 | .mg-hover-line3-color {
210 | fill: #db4437;
211 | }
212 |
213 | .mg-line4-color {
214 | stroke: #f8b128;
215 | }
216 |
217 | .mg-hover-line4-color {
218 | fill: #f8b128;
219 | }
220 |
221 | .mg-line5-color {
222 | stroke: #5c5c5c;
223 | }
224 |
225 | .mg-hover-line5-color {
226 | fill: #5c5c5c;
227 | }
228 |
229 | .mg-line-legend text {
230 | font-size: 1.5rem;
231 | font-weight: 300;
232 | stroke: none;
233 | }
234 |
235 | .mg-line1-legend-color {
236 | color: #4040e8;
237 | fill: #4040e8;
238 | }
239 |
240 | .mg-line2-legend-color {
241 | color: #05b378;
242 | fill: #05b378;
243 | }
244 |
245 | .mg-line3-legend-color {
246 | color: #db4437;
247 | fill: #db4437;
248 | }
249 |
250 | .mg-line4-legend-color {
251 | color: #f8b128;
252 | fill: #f8b128;
253 | }
254 |
255 | .mg-line5-legend-color {
256 | color: #5c5c5c;
257 | fill: #5c5c5c;
258 | }
259 |
260 | .mg-main-area-solid svg .mg-main-area {
261 | fill: #ccccff;
262 | opacity: 1;
263 | }
264 |
265 | .mg-markers line {
266 | opacity: 1;
267 | shape-rendering: auto;
268 | stroke: #b3b2b2;
269 | stroke-width: 1px;
270 | }
271 |
272 | .mg-markers text {
273 | fill: black;
274 | font-size: 1.2rem;
275 | opacity: 0.6;
276 | }
277 |
278 | .mg-missing-text {
279 | opacity: 0.9;
280 | }
281 |
282 | .mg-missing-background {
283 | stroke: blue;
284 | fill: none;
285 | stroke-dasharray: 10,5;
286 | stroke-opacity: 0.05;
287 | stroke-width: 2;
288 | }
289 |
290 | .mg-missing .mg-main-line {
291 | opacity: 0.1;
292 | }
293 |
294 | .mg-missing .mg-main-area {
295 | opacity: 0.03;
296 | }
297 |
298 | path.mg-main-area {
299 | opacity: 0.2;
300 | stroke: none;
301 | }
302 |
303 | path.mg-confidence-band {
304 | fill: #ccc;
305 | opacity: 0.4;
306 | stroke: none;
307 | }
308 |
309 | path.mg-main-line {
310 | fill: none;
311 | opacity: 0.8;
312 | stroke-width: 1.5px;
313 | }
314 |
315 | .mg-points circle {
316 | fill-opacity: 0.4;
317 | stroke-opacity: 1;
318 | }
319 |
320 | circle.mg-points-mono {
321 | fill: #0000ff;
322 | stroke: #0000ff;
323 | }
324 |
325 | tspan.mg-points-mono {
326 | fill: #0000ff;
327 | stroke: #0000ff;
328 | }
329 |
330 | /* a selected point in a scatterplot */
331 | .mg-points circle.selected {
332 | fill-opacity: 1;
333 | stroke-opacity: 1;
334 | }
335 |
336 | .mg-voronoi path {
337 | fill: none;
338 | pointer-events: all;
339 | stroke: none;
340 | stroke-opacity: 0.1;
341 | }
342 |
343 | .mg-x-rug-mono,
344 | .mg-y-rug-mono {
345 | stroke: black;
346 | }
347 |
348 | .mg-x-axis line,
349 | .mg-y-axis line {
350 | opacity: 1;
351 | shape-rendering: auto;
352 | stroke: #b3b2b2;
353 | stroke-width: 2px;
354 | }
355 |
356 | .mg-x-axis text,
357 | .mg-y-axis text,
358 | .mg-histogram .axis text {
359 | fill: black;
360 | font-size: 1.2rem;
361 | opacity: 0.6;
362 | }
363 |
364 | .mg-x-axis .label,
365 | .mg-y-axis .label,
366 | .mg-axis .label {
367 | font-size: 1.3rem;
368 | text-transform: uppercase;
369 | font-weight: 400;
370 | }
371 |
372 | .mg-x-axis-small text,
373 | .mg-y-axis-small text,
374 | .mg-active-datapoint-small {
375 | font-size: 0.6rem;
376 | }
377 |
378 | .mg-x-axis-small .label,
379 | .mg-y-axis-small .label {
380 | font-size: 0.65rem;
381 | }
382 |
383 | .mg-european-hours {
384 | }
385 |
386 | .mg-year-marker text {
387 | fill: black;
388 | font-size: 0.7rem;
389 | opacity: 0.6;
390 | }
391 |
392 | .mg-year-marker line {
393 | opacity: 1;
394 | shape-rendering: auto;
395 | stroke: #b3b2b2;
396 | stroke-width: 1.2px;
397 | }
398 |
399 | .mg-year-marker-small text {
400 | font-size: 0.6rem;
401 | }
402 |
--------------------------------------------------------------------------------
/tweet_display/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gedankenstuecke/twitter-analyser/91e3172bed6f786b34c237548ab81c347fd5b146/tweet_display/static/favicon.ico
--------------------------------------------------------------------------------
/tweet_display/static/javascripts/leaflet.timeline.js:
--------------------------------------------------------------------------------
1 | !function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n=e();for(var i in n)("object"==typeof exports?exports:t)[i]=n[i]}}(this,function(){return function(t){function e(i){if(n[i])return n[i].exports;var r=n[i]={i:i,l:!1,exports:{}};return t[i].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,i){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:i})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=7)}([function(t,e,n){"use strict";function i(t){return t&&t.__esModule?t:{default:t}}var r=n(3),o=i(r);L.Timeline=L.GeoJSON.extend({times:null,ranges:null,initialize:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.times=[],this.ranges=new o.default;var i={drawOnSetTime:!0};L.GeoJSON.prototype.initialize.call(this,null,n),L.Util.setOptions(this,i),L.Util.setOptions(this,n),this.options.getInterval&&(this._getInterval=function(){var t;return(t=e.options).getInterval.apply(t,arguments)}),t&&this._process(t)},_getInterval:function(t){var e="start"in t.properties,n="end"in t.properties;return!(!e||!n)&&{start:new Date(t.properties.start).getTime(),end:new Date(t.properties.end).getTime()}},_process:function(t){var e=this,n=1/0,i=-(1/0);t.features.forEach(function(t){var r=e._getInterval(t);r&&(e.ranges.insert(r.start,r.end,t),e.times.push(r.start),e.times.push(r.end),n=Math.min(n,r.start),i=Math.max(i,r.end))}),this.start=this.options.start||n,this.end=this.options.end||i,this.time=this.start,0!==this.times.length&&(this.times.sort(function(t,e){return t-e}),this.times=this.times.reduce(function(t,e,n){if(0===n)return t;var i=t[t.length-1];return i!==e&&t.push(e),t},[this.times[0]]))},setTime:function(t){this.time="number"==typeof t?t:new Date(t).getTime(),this.options.drawOnSetTime&&this.updateDisplayedLayers(),this.fire("change")},updateDisplayedLayers:function(){for(var t=this,e=this.ranges.lookup(this.time),n=0;n0&&void 0!==arguments[0]?arguments[0]:{},e={duration:1e4,enableKeyboardControls:!1,enablePlayback:!0,formatOutput:function(t){return""+(t||"")},showTicks:!0,waitToUpdateMap:!1,position:"bottomleft",steps:1e3};this.timelines=[],L.Util.setOptions(this,e),L.Util.setOptions(this,t),"undefined"!=typeof t.start&&(this.start=t.start),"undefined"!=typeof t.end&&(this.end=t.end)},_getTimes:function(){var t=this,e=[];if(this.timelines.forEach(function(n){var r=n.times.filter(function(e){return e>=t.start&&e<=t.end});e.push.apply(e,i(r))}),e.length){e.sort(function(t,e){return t-e});var n=[e[0]];return e.reduce(function(t,e){return t!==e&&n.push(e),e}),n}return e},_recalculate:function(){var t="undefined"!=typeof this.options.start,e="undefined"!=typeof this.options.end,n=this.options.duration,i=1/0,r=-(1/0);this.timelines.forEach(function(t){t.startr&&(r=t.end)}),t||(this.start=i,this._timeSlider.min=i===1/0?0:i,this._timeSlider.value=this._timeSlider.min),e||(this.end=r,this._timeSlider.max=r===-(1/0)?0:r),this._stepSize=Math.max(1,(this.end-this.start)/this.options.steps),this._stepDuration=Math.max(1,n/this.options.steps)},_nearestEventTime:function(t){for(var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=this._getTimes(),i=!1,r=n[0],o=1;o=t){if(e===-1)return r;if(a!==t)return a;i=!0}r=a}return r},_createDOM:function(){var t=["leaflet-control-layers","leaflet-control-layers-expanded","leaflet-timeline-control"],e=L.DomUtil.create("div",t.join(" "));if(this.container=e,this.options.enablePlayback){var n=L.DomUtil.create("div","sldr-ctrl-container",e),i=L.DomUtil.create("div","button-container",n);this._makeButtons(i),this.options.enableKeyboardControls&&this._addKeyListeners(),this._makeOutput(n)}this._makeSlider(e),this.options.showTicks&&this._buildDataList(e)},_addKeyListeners:function(){var t=this;this._listener=function(){return t._onKeydown.apply(t,arguments)},document.addEventListener("keydown",this._listener)},_removeKeyListeners:function(){document.removeEventListener("keydown",this._listener)},_buildDataList:function(t){this._datalist=L.DomUtil.create("datalist","",t);var e=Math.floor(1e6*Math.random());this._datalist.id="timeline-datalist-"+e,this._timeSlider.setAttribute("list",this._datalist.id),this._rebuildDataList()},_rebuildDataList:function(){for(var t=this._datalist;t.firstChild;)t.removeChild(t.firstChild);var e=L.DomUtil.create("select","",this._datalist);this._getTimes().forEach(function(t){L.DomUtil.create("option","",e).value=t})},_makeButton:function(t,e){var n=this,i=L.DomUtil.create("button",e,t);i.addEventListener("click",function(){return n[e]()}),L.DomEvent.disableClickPropagation(i)},_makeButtons:function(t){this._makeButton(t,"prev"),this._makeButton(t,"play"),this._makeButton(t,"pause"),this._makeButton(t,"next")},_makeSlider:function(t){var e=this,n=L.DomUtil.create("input","time-slider",t);n.type="range",n.min=this.start||0,n.max=this.end||0,n.value=this.start||0,n.addEventListener("change",function(t){return e._sliderChanged(t)}),n.addEventListener("input",function(t){return e._sliderChanged(t)}),n.addEventListener("pointerdown",function(){return e.map.dragging.disable()}),document.addEventListener("pointerup",function(){return e.map.dragging.enable()}),this._timeSlider=n},_makeOutput:function(t){this._output=L.DomUtil.create("output","time-text",t),this._output.innerHTML=this.options.formatOutput(this.start)},_onKeydown:function(t){switch(t.keyCode||t.which){case 37:this.prev();break;case 39:this.next();break;case 32:this.toggle();break;default:return}t.preventDefault()},_sliderChanged:function(t){var e=parseFloat(t.target.value,10);this.time=e,this.options.waitToUpdateMap&&"change"!==t.type||this.timelines.forEach(function(t){return t.setTime(e)}),this._output&&(this._output.innerHTML=this.options.formatOutput(e))},_resetIfTimelinesChanged:function(t){this.timelines.length!==t&&(this._recalculate(),this.options.showTicks&&this._rebuildDataList(),this.setTime(this.start))},addTimelines:function(){var t=this;this.pause();for(var e=this.timelines.length,n=arguments.length,i=Array(n),r=0;r=t&&e.push(n.data),e.push.apply(e,i(this.lookup(t,n.right)))),e)}}]),t}();e.default=s},function(t,e,n){e=t.exports=n(5)(),e.push([t.i,'.leaflet-control.leaflet-timeline-control{width:96%;box-sizing:border-box;margin:2%;margin-bottom:20px;text-align:center}.leaflet-control.leaflet-timeline-control *{vertical-align:middle}.leaflet-control.leaflet-timeline-control input[type=range]{width:80%}.leaflet-control.leaflet-timeline-control .sldr-ctrl-container{float:left;width:15%;box-sizing:border-box}.leaflet-control.leaflet-timeline-control .button-container button{position:relative;width:20%;height:20px}.leaflet-control.leaflet-timeline-control .button-container button:after,.leaflet-control.leaflet-timeline-control .button-container button:before{content:"";position:absolute}.leaflet-control.leaflet-timeline-control .button-container button.play:before{border:7px solid transparent;border-width:7px 0 7px 10px;border-left-color:#000;margin-top:-7px;background:transparent;margin-left:-5px}.leaflet-control.leaflet-timeline-control .button-container button.pause{display:none}.leaflet-control.leaflet-timeline-control .button-container button.pause:before{width:4px;height:14px;border:4px solid #000;border-width:0 4px;margin-top:-7px;margin-left:-6px;background:transparent}.leaflet-control.leaflet-timeline-control .button-container button.prev:after,.leaflet-control.leaflet-timeline-control .button-container button.prev:before{margin:-8px 0 0;background:#000}.leaflet-control.leaflet-timeline-control .button-container button.prev:before{width:2px;height:14px;margin-top:-7px;margin-left:-7px}.leaflet-control.leaflet-timeline-control .button-container button.prev:after{border:7px solid transparent;border-width:7px 10px 7px 0;border-right-color:#000;margin-top:-7px;margin-left:-5px;background:transparent}.leaflet-control.leaflet-timeline-control .button-container button.next:after,.leaflet-control.leaflet-timeline-control .button-container button.next:before{margin:-8px 0 0;background:#000}.leaflet-control.leaflet-timeline-control .button-container button.next:before{width:2px;height:14px;margin-top:-7px;margin-left:5px}.leaflet-control.leaflet-timeline-control .button-container button.next:after{border:7px solid transparent;border-width:7px 0 7px 10px;border-left-color:#000;margin-top:-7px;margin-left:-5px;background:transparent}.leaflet-control.leaflet-timeline-control.playing button.pause{display:inline-block}.leaflet-control.leaflet-timeline-control.playing button.play{display:none}',""])},function(t,e){t.exports=function(){var t=[];return t.toString=function(){for(var t=[],e=0;e=0&&b.splice(e,1)}function a(t){var e=document.createElement("style");return e.type="text/css",r(t,e),e}function s(t){var e=document.createElement("link");return e.rel="stylesheet",r(t,e),e}function l(t,e){var n,i,r;if(e.singleton){var l=g++;n=v||(v=a(e)),i=u.bind(null,n,l,!1),r=u.bind(null,n,l,!0)}else t.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=s(e),i=f.bind(null,n),r=function(){o(n),n.href&&URL.revokeObjectURL(n.href)}):(n=a(e),i=h.bind(null,n),r=function(){o(n)});return i(t),function(e){if(e){if(e.css===t.css&&e.media===t.media&&e.sourceMap===t.sourceMap)return;i(t=e)}else r()}}function u(t,e,n,i){var r=n?"":i.css;if(t.styleSheet)t.styleSheet.cssText=y(e,r);else{var o=document.createTextNode(r),a=t.childNodes;a[e]&&t.removeChild(a[e]),a.length?t.insertBefore(o,a[e]):t.appendChild(o)}}function h(t,e){var n=e.css,i=e.media;if(i&&t.setAttribute("media",i),t.styleSheet)t.styleSheet.cssText=n;else{for(;t.firstChild;)t.removeChild(t.firstChild);t.appendChild(document.createTextNode(n))}}function f(t,e){var n=e.css,i=e.sourceMap;i&&(n+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(i))))+" */");var r=new Blob([n],{type:"text/css"}),o=t.href;t.href=URL.createObjectURL(r),o&&URL.revokeObjectURL(o)}var c={},p=function(t){var e;return function(){return"undefined"==typeof e&&(e=t.apply(this,arguments)),e}},d=p(function(){return/msie [6-9]\b/.test(window.navigator.userAgent.toLowerCase())}),m=p(function(){return document.head||document.getElementsByTagName("head")[0]}),v=null,g=0,b=[];t.exports=function(t,e){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");e=e||{},"undefined"==typeof e.singleton&&(e.singleton=d()),"undefined"==typeof e.insertAt&&(e.insertAt="bottom");var r=i(t);return n(r,e),function(t){for(var o=[],a=0;a
2 |
3 |
4 |
5 |
6 |
7 |
8 | TwArχiv
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {% load static %}
27 |
29 |
30 |
31 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
97 |
98 |
99 |
100 | Home
101 |
102 |
103 | Public Visualizations
104 |
105 | {% if graph_section %}
106 |
107 | {% if link_target %}
108 | General
109 | {% else %}
110 | General
111 | {% endif %}
112 |
113 |
114 | {% if link_target %}
115 | Interactions
116 | {% else %}
117 | Interactions
118 | {% endif %}
119 |
120 |
121 | {% if link_target %}
122 | Location
123 | {% else %}
124 | Location
125 | {% endif %}
126 |
127 | {%endif%}
128 |
129 |
130 |
131 | About
132 |
133 |
134 |
135 |
136 |
137 |
138 | {% block content %}
139 | {% endblock %}
140 |
141 |
142 |
151 |
152 |