Created
December 2, 2010 13:58
-
-
Save abhin4v/725331 to your computer and use it in GitHub Desktop.
Shows Stack Overflow reputations, upvotes, downvotes and accepted answers for a user against the tags for the answers. See http://stackapps.com/questions/1828/socharts-charts-by-tags
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; Copyright (c) Abhinav Sarkar. All rights reserved. | |
; The use and distribution terms for this software are covered by the | |
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) | |
; which can be found in the file epl-v10.html at the root of this distribution. | |
; By using this software in any fashion, you are agreeing to be bound by | |
; the terms of this license. | |
; You must not remove this notice, or any other, from this software. | |
(ns socharts.socharts | |
(:import [java.util.zip GZIPInputStream] | |
[javax.swing JPanel JFrame JButton JTextField JLabel JOptionPane | |
JProgressBar UIManager WindowConstants] | |
[java.awt GridBagLayout Insets Toolkit Font Dimension]) | |
(:use [clojure.java.io :only (as-url input-stream)] | |
[clojure.string :only (join)] | |
[clojure.java.browse :only (browse-url)] | |
[clojure.contrib.json :only (read-json)] | |
[clojure.contrib.math :only (ceil)] | |
[clojure.contrib.core :only (-?>)] | |
[clojure.contrib.swing-utils :only | |
(do-swing add-action-listener add-key-typed-listener)]) | |
(:gen-class)) | |
;; Core | |
(defn wrap-text | |
[text columns] | |
(if (> (.length text) columns) | |
(str (first (re-seq (re-pattern (str ".{0," columns "} ")) text)) "...") | |
text)) | |
(defn make-gzipped-input-stream | |
[url] | |
(GZIPInputStream. (input-stream (as-url url)))) | |
(defn get-data | |
[url] | |
(with-open [is (make-gzipped-input-stream url)] | |
(read-json (slurp is) true))) | |
(def user-name-atm (atom nil)) | |
(def status-msg-atm (atom "")) | |
(def total-answers-atm (atom 0)) | |
(def tags-fetched-count-atm (atom 0)) | |
(let [api-url-format "http://api.stackoverflow.com/1.0/users/%s?key=7eDzkIpVYEmJQj9z4NyE_w"] | |
(defn get-user-name | |
[user-id] | |
(let [data (get-data (format api-url-format user-id)) | |
user-name (-?> data :users first :display_name)] | |
(println "Fetching username for id" user-id) | |
(reset! status-msg-atm "Fetching username") | |
(if (nil? user-name) | |
(throw (IllegalArgumentException.)) | |
(reset! user-name-atm user-name))))) | |
(let [api-url-format | |
"http://api.stackoverflow.com/1.0/users/%s/answers?key=7eDzkIpVYEmJQj9z4NyE_w&pagesize=100" | |
paged-api-url-format | |
"http://api.stackoverflow.com/1.0/users/%s/answers?key=7eDzkIpVYEmJQj9z4NyE_w&pagesize=100&page=%s"] | |
(defn answer-seq | |
([user-id user-name] | |
(lazy-seq | |
(let [data (get-data (format api-url-format user-id)) | |
total (reset! total-answers-atm (-> data :total)) | |
page (-> data :page) | |
pagesize (-> data :pagesize) | |
pages (ceil (/ total pagesize)) | |
answers (->> data :answers)] | |
(println "Fetching" total "answers by" user-name) | |
(println "Fetching answers" | |
(str (inc (* 100 (dec page))) "-" (min (* 100 page) total)) | |
"by" user-name) | |
(reset! status-msg-atm "Fetching answers") | |
(if (= page pages) | |
answers | |
(lazy-cat answers | |
(answer-seq user-id user-name (inc page) pages total)))))) | |
([user-id user-name page pages total] | |
(lazy-seq | |
(let [data (get-data (format paged-api-url-format user-id page)) | |
answers (->> data :answers)] | |
(println "Fetching answers" | |
(str (inc (* 100 (dec page))) "-" (min (* 100 page) total)) | |
"by" user-name) | |
(reset! status-msg-atm "Fetching answers") | |
(if (= page pages) | |
answers | |
(lazy-cat answers | |
(answer-seq user-id user-name (inc page) pages total)))))))) | |
(let [api-url-format | |
"http://api.stackoverflow.com/1.0/questions/%s?key=7eDzkIpVYEmJQj9z4NyE_w"] | |
(defn get-tags | |
[answer] | |
(let [question-id (-> answer :question_id) | |
data (get-data (format api-url-format question-id))] | |
(println | |
(str "Fetching tags for answer to \"" (-> answer :title (wrap-text 60)) "\"")) | |
(reset! status-msg-atm "Fetching tags") | |
(swap! tags-fetched-count-atm inc) | |
(->> data :questions first :tags (map keyword))))) | |
(defn add-reputation | |
[vote] | |
(-> vote | |
(assoc :reputation | |
(if (:community vote) 0 | |
(+ (* 15 (:accept vote)) (* 10 (:up vote)) (* (- 5) (:down vote))))) | |
(dissoc :community))) | |
(defn get-votes | |
[user-id user-name] | |
(reduce | |
(fn [acc ans] | |
(do (Thread/sleep 250) | |
(merge-with | |
#(merge-with + %1 %2) | |
acc | |
(reduce #(assoc %1 %2 | |
(add-reputation | |
{:up (:up_vote_count ans) | |
:down (:down_vote_count ans) | |
:accept (if (:accepted ans) 1 0) | |
:community (:community_owned ans)})) | |
{} (get-tags ans))))) | |
{} (answer-seq user-id user-name))) | |
(defn top-votes | |
[votes vote-type] | |
(take 15 | |
(sort-by second > (map (fn [[k v]] (vector k (vote-type v))) votes)))) | |
(let [chart-url-format | |
(str "http://chart.apis.google.com/chart?cht=bhs&chs=700x420&" | |
"chm=N,000000,0,-1,11,,e:2&" | |
"chd=t:%s&chds=0,%s&chxr=1,0,%s&chtt=%s&chxt=y,x,x&chxl=0:|%s|2:|%s&" | |
"chco=00A5C6|DEBDDE|C6D9FD&chxp=2,50&chbh=a,5&chma=55,25,40,40")] | |
(defn make-chart-url | |
[tags-votes user-name chart-type] | |
(let [tags (reverse (map #(-> % first name) tags-votes)) | |
votes (map second tags-votes) | |
max-vote (apply max votes)] | |
(-> chart-url-format | |
(format | |
(join "," votes) | |
max-vote | |
max-vote | |
(str chart-type "+for+" | |
(clojure.string/replace user-name " " "+") | |
"+on+Stack+Overflow+by+Tags") | |
(join "|" tags) | |
chart-type) | |
(clojure.string/replace "|" "%7c"))))) | |
(defn make-charts | |
[user-id] | |
(when-not (empty? (str user-id)) | |
(let [user-name (get-user-name user-id) | |
votes (get-votes user-id user-name)] | |
(println "Opening charts in the browser") | |
(reset! status-msg-atm "Opening charts") | |
(browse-url | |
(make-chart-url (top-votes votes :up) user-name "Upvotes")) | |
(browse-url | |
(make-chart-url (top-votes votes :down) user-name "Downvotes")) | |
(browse-url | |
(make-chart-url (top-votes votes :accept) user-name "Accepted+Answers")) | |
(browse-url | |
(make-chart-url (top-votes votes :reputation) user-name "Reputations"))))) | |
;;Core | |
;;GUI | |
(defmacro set-grid! [constraints field value] | |
`(set! (. ~constraints ~(symbol (name field))) | |
~(if (keyword? value) | |
`(. java.awt.GridBagConstraints ~(symbol (name value))) | |
value))) | |
(defmacro grid-bag-layout [container & body] | |
(let [c (gensym "c") | |
cntr (gensym "cntr")] | |
`(let [~c (new java.awt.GridBagConstraints) | |
~cntr ~container] | |
~@(loop [result '() body body] | |
(if (empty? body) | |
(reverse result) | |
(let [expr (first body)] | |
(if (keyword? expr) | |
(recur (cons `(set-grid! ~c ~expr ~(second body)) result) | |
(next (next body))) | |
(recur (cons `(.add ~cntr ~expr ~c) result) | |
(next body))))))))) | |
(defn center-window! [^java.awt.Component window] | |
(let [dim (.. Toolkit getDefaultToolkit getScreenSize) | |
w (.. window getSize width) | |
h (.. window getSize height) | |
window-x (/ (- (.width dim) w) 2) | |
window-y (/ (- (.height dim) h) 2)] | |
(doto window (.setLocation window-x window-y)))) | |
(defn show-error-msg | |
[frame msg title] | |
(JOptionPane/showMessageDialog frame msg title JOptionPane/ERROR_MESSAGE)) | |
(defn create-gui | |
[] | |
(let [frame (JFrame. "Stack Overflow Charts") | |
user-id-tf (JTextField. 25) | |
user-name-tf (doto (JTextField. 25) (.setEnabled false)) | |
progress-bar (doto (JProgressBar.) | |
(.setMinimum 0) | |
(.setPreferredSize (Dimension. 200 23)) | |
(.setStringPainted true)) | |
status-lbl (JLabel. "Input your User Id and press <ENTER>") | |
reset-gui #((do-swing | |
(.setEnabled user-id-tf true) | |
(.setValue progress-bar 0)) | |
(reset! total-answers-atm 1) | |
(reset! tags-fetched-count-atm 0) | |
(reset! status-msg-atm "Done!"))] | |
(add-watch user-name-atm :user-name-tf | |
(fn [_ _ _ n] (do-swing (.setText user-name-tf n)))) | |
(add-watch total-answers-atm :progress-bar | |
(fn [_ _ _ n] (do-swing (.setMaximum progress-bar n)))) | |
(add-watch tags-fetched-count-atm :progress-bar | |
(fn [_ _ _ n] (do-swing (.setValue progress-bar n)))) | |
(add-watch status-msg-atm :status-lbl | |
(fn [_ _ _ n] (do-swing (.setText status-lbl n)))) | |
(add-key-typed-listener user-id-tf | |
(fn [e] | |
(when (= (.getKeyChar e) \newline) | |
(let [user-id (.getText user-id-tf)] | |
(try | |
(Long/parseLong user-id) | |
(future | |
(try | |
(make-charts user-id) | |
(catch IllegalArgumentException e | |
(do-swing | |
(show-error-msg frame "Invalid User Id!" "Error"))) | |
(catch Exception e | |
(do-swing | |
(show-error-msg | |
frame "Error Occured. Please try agin." "Error"))) | |
(finally (reset-gui)))) | |
(.setEnabled user-id-tf false) | |
(catch NumberFormatException e | |
(do-swing | |
(show-error-msg frame "Invalid User Id!" "Error")))))))) | |
(doto frame | |
(.setResizable false) | |
(.setDefaultCloseOperation WindowConstants/EXIT_ON_CLOSE) | |
(.setContentPane | |
(doto (JPanel. (GridBagLayout.)) | |
(grid-bag-layout | |
:insets (Insets. 5 5 5 5) | |
:fill :HORIZONTAL | |
:gridy 0 :gridx 0 (JLabel. "User Id") :gridx 1 user-id-tf | |
:gridy 1 :gridx 0 (JLabel. "User Name") :gridx 1 user-name-tf | |
:gridwidth 2 :gridx 0 | |
:gridy 2 progress-bar | |
:gridy 3 status-lbl))) | |
(.pack) | |
(.setVisible true) | |
(center-window!)))) | |
(UIManager/setLookAndFeel (UIManager/getSystemLookAndFeelClassName)) | |
;;GUI | |
;;main | |
(defn -main [& args] | |
(if-not (empty? args) | |
(make-charts (first args)) | |
(do-swing (create-gui)))) | |
;;main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment